import { describe, it, expect } from 'vitest'
import { mapStream, resolveCurrentStreamIndex } from '../streams'
import { parseSimVsmValue } from '../parser'
import { buildSyntheticSimVsmFile } from './fixtures'
import type { SimVsmParsedAlternative } from '../types'
import { computeNodeParameterDeviations } from '@/components/wertstrom/vsm-scenario-compare'

function alternatives() {
  const result = parseSimVsmValue(buildSyntheticSimVsmFile(), 'x.json')
  if (!result.ok) throw new Error('fixture must parse')
  return result.alternatives
}

function makeAlt(overrides: Partial<SimVsmParsedAlternative> = {}): SimVsmParsedAlternative {
  return { index: 0, name: 'Alt', isMainFlag: false, nodes: [], links: [], hasResultData: false, ...overrides }
}

describe('resolveCurrentStreamIndex', () => {
  it('falls back to index 0 when no alternative has isMain (the real-corpus case — 0/73 measured)', () => {
    expect(resolveCurrentStreamIndex(alternatives())).toBe(0)
  })

  it('prefers an explicit isMain:true when present (forward-compatible, even though unobserved in the real corpus)', () => {
    const alts = alternatives()
    alts[1].isMainFlag = true
    expect(resolveCurrentStreamIndex(alts)).toBe(1)
  })

  it('falls back to 0 for a single-alternative file too (matches every real single-alt file, isMain still false)', () => {
    expect(resolveCurrentStreamIndex([alternatives()[0]])).toBe(0)
  })

  it('signal 2: an unsuffixed name wins against its own "_N"-suffixed sibling, even when the suffixed one is array-index 0', () => {
    const alts = [makeAlt({ index: 0, name: 'Standard_1' }), makeAlt({ index: 1, name: 'Standard' })]
    expect(resolveCurrentStreamIndex(alts)).toBe(1)
  })

  it('signal 2 does not fire for an unrelated name that merely shares a prefix without the "_N" shape', () => {
    const alts = [makeAlt({ index: 0, name: 'Standard-Neu' }), makeAlt({ index: 1, name: 'Standard' })]
    // "Standard-Neu" is not "Standard_<digits>" — no suffix relationship, falls through to signal 4 (index 0).
    expect(resolveCurrentStreamIndex(alts)).toBe(0)
  })

  it('signal 3: when no isMain and no suffix relationship, the earliest parseable modificationTime wins', () => {
    const alts = [
      makeAlt({ index: 0, name: 'A', modificationTime: '2026-02-01T00:00:00.000Z' }),
      makeAlt({ index: 1, name: 'B', modificationTime: '2026-01-01T00:00:00.000Z' }),
    ]
    expect(resolveCurrentStreamIndex(alts)).toBe(1)
  })

  it('signal 3 ignores an unparseable modificationTime rather than letting it win via NaN', () => {
    const alts = [makeAlt({ index: 0, name: 'A', modificationTime: 'not-a-date' }), makeAlt({ index: 1, name: 'B', modificationTime: '2026-01-01T00:00:00.000Z' })]
    expect(resolveCurrentStreamIndex(alts)).toBe(1)
  })

  it('signal precedence: isMain beats the suffix heuristic, which beats modificationTime, which beats array order', () => {
    const alts = [
      makeAlt({ index: 0, name: 'X_1', modificationTime: '2026-01-01T00:00:00.000Z' }),
      makeAlt({ index: 1, name: 'X', modificationTime: '2026-06-01T00:00:00.000Z' }), // signal 2 winner despite a LATER modificationTime
      makeAlt({ index: 2, name: 'Y', isMainFlag: true }), // signal 1 winner despite neither of the above
    ]
    expect(resolveCurrentStreamIndex(alts)).toBe(2)
  })
})

describe('mapStream: node-id correlation across scenarios (§9.4 prerequisite)', () => {
  it('the same SimVSM key gets the SAME VsmNode id across two alternatives of the same file when a shared correlation map is passed', () => {
    const ist = makeAlt({
      index: 0,
      name: 'Ist-Zustand',
      nodes: [{ key: '1', category: 'item', simvsmClass: 'singleProcess', nodeName: 'Stanzen', params: new Map() }],
      links: [],
    })
    const alt = makeAlt({
      index: 1,
      name: 'Alternative A',
      nodes: [{ key: '1', category: 'item', simvsmClass: 'singleProcess', nodeName: 'Stanzen', params: new Map() }],
      links: [],
    })
    const nodeIdByKeyAcrossFile = new Map<string, string>()
    let counter = 0
    const idFactory = () => `id-${++counter}`
    const istStream = mapStream(ist, 0, idFactory, nodeIdByKeyAcrossFile)
    const altStream = mapStream(alt, 0, idFactory, nodeIdByKeyAcrossFile)
    expect(istStream.nodes[0].id).toBe(altStream.nodes[0].id)
    // A genuinely new key (not present in the Ist stream) still gets its own fresh id.
    expect(counter).toBe(1)
  })

  it('WITHOUT a shared correlation map (omitted), two independent mapStream calls give the same key two DIFFERENT ids (documents the pre-fix/isolated-call behavior)', () => {
    const node = { key: '1', category: 'item', simvsmClass: 'singleProcess', nodeName: 'Stanzen', params: new Map() }
    const a = mapStream(makeAlt({ index: 0, nodes: [node] }), 0)
    const b = mapStream(makeAlt({ index: 1, nodes: [node] }), 0)
    expect(a.nodes[0].id).not.toBe(b.nodes[0].id)
  })

  it('a real key introduced only in the Alternative (not in the Ist) still gets its own fresh, uncorrelated id', () => {
    const ist = makeAlt({ index: 0, nodes: [{ key: '1', category: 'item', simvsmClass: 'singleProcess', nodeName: 'A', params: new Map() }] })
    const alt = makeAlt({
      index: 1,
      nodes: [
        { key: '1', category: 'item', simvsmClass: 'singleProcess', nodeName: 'A', params: new Map() },
        { key: '2', category: 'item', simvsmClass: 'singleProcess', nodeName: 'Neu', params: new Map() },
      ],
    })
    const nodeIdByKeyAcrossFile = new Map<string, string>()
    const istStream = mapStream(ist, 0, undefined, nodeIdByKeyAcrossFile)
    const altStream = mapStream(alt, 0, undefined, nodeIdByKeyAcrossFile)
    const key1IdInAlt = altStream.nodeMappings.find((m) => m.simvsmKey === '1')!.vsmNode!.id
    const key2IdInAlt = altStream.nodeMappings.find((m) => m.simvsmKey === '2')!.vsmNode!.id
    expect(key1IdInAlt).toBe(istStream.nodes[0].id)
    expect(key2IdInAlt).not.toBe(istStream.nodes[0].id)
  })

  it('§9.4 integration: computeNodeParameterDeviations on an imported Ist/Alternative pair reports "changed", not added+removed, for an unedited shared node', () => {
    const ist = makeAlt({
      index: 0,
      nodes: [
        {
          key: '1',
          category: 'item',
          simvsmClass: 'singleProcess',
          nodeName: 'Stanzen',
          params: new Map([['ProductTable', { class: 'ProductTable', type: 'tableGrid', value: { rows: [{ PlannedCycleTime: { durationInSec: 45 } }] } }]]),
        },
      ],
    })
    const alt = makeAlt({
      index: 1,
      nodes: [
        {
          key: '1',
          category: 'item',
          simvsmClass: 'singleProcess',
          nodeName: 'Stanzen',
          params: new Map([['ProductTable', { class: 'ProductTable', type: 'tableGrid', value: { rows: [{ PlannedCycleTime: { durationInSec: 30 } }] } }]]),
        },
      ],
    })
    const nodeIdByKeyAcrossFile = new Map<string, string>()
    const istStream = mapStream(ist, 0, undefined, nodeIdByKeyAcrossFile)
    const altStream = mapStream(alt, 0, undefined, nodeIdByKeyAcrossFile)

    const deviations = computeNodeParameterDeviations(istStream.nodes, altStream.nodes)
    expect(deviations.added).toEqual([])
    expect(deviations.removed).toEqual([])
    expect(deviations.changed).toHaveLength(1)
    expect(deviations.changed[0].fields.find((f) => f.key === 'cycleTimeSec')).toMatchObject({ baselineValue: 45, scenarioValue: 30 })
  })
})

describe('mapStream', () => {
  it('assigns scenarioRole "current" to the resolved current index and "alternative" to every other stream', () => {
    const alts = alternatives()
    const currentIndex = resolveCurrentStreamIndex(alts)
    const streams = alts.map((a) => mapStream(a, currentIndex))
    expect(streams[0].scenarioRole).toBe('current')
    expect(streams[1].scenarioRole).toBe('alternative')
  })

  it('drops unsupported nodes (noteVSM, foreignOrders) from .nodes but keeps them in .nodeMappings', () => {
    const alts = alternatives()
    const stream = mapStream(alts[0], 0)
    expect(stream.nodeMappings).toHaveLength(7)
    expect(stream.nodes.length).toBeLessThan(7)
    expect(stream.nodes.some((n) => n.name.includes('Kaizen'))).toBe(false)
  })

  it('drops the informationFlow link into the unsupported noteVSM node as a missing relationship', () => {
    const alts = alternatives()
    const stream = mapStream(alts[0], 0)
    const droppedForMissingNode = stream.linkMappings.filter((l) => !l.supported && l.reasonDe?.includes('nicht übernommenen Knoten'))
    expect(droppedForMissingNode.length).toBeGreaterThanOrEqual(1)
  })

  it('positions every mapped node via computeAutoLayout (never leaves SimVSM loc coordinates in place)', () => {
    const alts = alternatives()
    const stream = mapStream(alts[0], 0)
    // The synthetic fixture's raw `loc` for the supplier node is "-200 0" —
    // auto-layout's START_X is a small positive number, so a real transform
    // happened rather than a passthrough of the raw negative coordinate.
    for (const node of stream.nodes) {
      expect(node.x).toBeGreaterThanOrEqual(0)
    }
  })

  it('produces valid materialFlow chain connections between mapped nodes', () => {
    const alts = alternatives()
    const stream = mapStream(alts[0], 0)
    const nodeIds = new Set(stream.nodes.map((n) => n.id))
    for (const c of stream.connections) {
      expect(nodeIds.has(c.fromNodeId)).toBe(true)
      expect(nodeIds.has(c.toNodeId)).toBe(true)
    }
  })

  it('preserves alternative name and hasResultData', () => {
    const alts = alternatives()
    const stream = mapStream(alts[0], 0)
    expect(stream.name).toBe('Ist-Zustand')
    expect(stream.hasResultData).toBe(false) // resultData: [] in fixture
  })

  it('generates unique node ids across a stream via the id factory', () => {
    const alts = alternatives()
    const stream = mapStream(alts[0], 0)
    const ids = stream.nodes.map((n) => n.id)
    expect(new Set(ids).size).toBe(ids.length)
  })
})
