// sync.ts tests (QVS-P5, KAR-974): all 6 Spec-21 status classes + the
// combination cases the task explicitly mandates (field locally changed +
// source changed = BOTH_CHANGED; new step; removed step; unmatched), plus
// the LOCAL_CHANGED-protection guarantee for `applySyncAdoption`.

import { describe, it, expect } from 'vitest'
import type { VsmNode } from '@/lib/vsm-types'
import { mapQafRowsToVsmNodes } from '../mapper'
import { computeSyncDelta, applySyncAdoption } from '../sync'
import type { QvsSyncAdoptionPlan, QvsStepDelta } from '../types'
import { buildQafRow } from './fixtures'

function idFactory(prefix: string) {
  let i = 0
  return () => `${prefix}-${i++}`
}

function mapRows(rows: Parameters<typeof mapQafRowsToVsmNodes>[0], prefix: string): VsmNode[] {
  return mapQafRowsToVsmNodes(rows, { idFactory: idFactory(prefix) }).nodes
}

function findStep(steps: readonly QvsStepDelta[], name: string): QvsStepDelta {
  const step = steps.find((s) => s.name === name)
  if (!step) throw new Error(`no step named "${name}" in delta`)
  return step
}

describe('computeSyncDelta', () => {
  const snapshotRows = [
    buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 }),
    buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 }),
    buildQafRow({ prozessbezeichnung: 'Prüfen', zykluszeit: 15 }),
    buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
  ]
  const snapshotNodes = mapRows(snapshotRows, 'snap')

  function liveClone(edit: (nodes: VsmNode[]) => void = () => {}): VsmNode[] {
    const clones = snapshotNodes.map((n) => ({ ...n, fieldStatus: { ...n.fieldStatus } }))
    edit(clones)
    return clones
  }

  it('classifies UNCHANGED when source, snapshot and live all agree', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone()

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Schweißen')
    expect(step.status).toBe('UNCHANGED')
    expect(step.locallyDeleted).toBe(false)
    const ctField = step.fields.find((f) => f.field === 'cycleTimeSec')!
    expect(ctField.status).toBe('UNCHANGED')
  })

  it('classifies SOURCE_CHANGED when only the source value differs from the snapshot', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 33 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone()

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Montage')
    expect(step.status).toBe('SOURCE_CHANGED')
    const ctField = step.fields.find((f) => f.field === 'cycleTimeSec')!
    expect(ctField.status).toBe('SOURCE_CHANGED')
    expect(ctField.snapshotValue).toBe(30)
    expect(ctField.sourceValue).toBe(33)
  })

  it('classifies LOCAL_CHANGED when only the live value differs from the snapshot', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const montage = nodes.find((n) => n.name === 'Montage')!
      montage.cycleTimeSec = 35
      montage.fieldStatus = { ...montage.fieldStatus, cycleTimeSec: 'modified' }
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Montage')
    expect(step.status).toBe('LOCAL_CHANGED')
    const ctField = step.fields.find((f) => f.field === 'cycleTimeSec')!
    expect(ctField.status).toBe('LOCAL_CHANGED')
    expect(ctField.liveValue).toBe(35)
  })

  it('classifies BOTH_CHANGED when the SAME field differs on both the source and local axis', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 33 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const montage = nodes.find((n) => n.name === 'Montage')!
      montage.cycleTimeSec = 35
      montage.fieldStatus = { ...montage.fieldStatus, cycleTimeSec: 'modified' }
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Montage')
    expect(step.status).toBe('BOTH_CHANGED')
    const ctField = step.fields.find((f) => f.field === 'cycleTimeSec')!
    expect(ctField.status).toBe('BOTH_CHANGED')
    expect(ctField.snapshotValue).toBe(30)
    expect(ctField.sourceValue).toBe(33)
    expect(ctField.liveValue).toBe(35)
  })

  // Review-Fix (major/minor, KAR-974 adversarial review, Findings 3+11):
  // simulates the documented partial-failure retry scenario (reimport.ts
  // module header) — the value_stream_maps update succeeded (live already
  // holds the adopted source value, fieldStatus flipped to 'imported' by
  // applyFieldValue, NOT 'modified' — this is a prior successful ADOPTION,
  // not a manual edit) but the value_stream_imports update failed, so the
  // snapshot was NEVER advanced. The next delta computation must NOT be
  // mistaken for a harmless repeat SOURCE_CHANGED (the reimport.ts module
  // header previously, incorrectly, claimed exactly that) — it genuinely
  // classifies as BOTH_CHANGED, since BOTH source and local differ from the
  // stale snapshot, even though source and live already hold the IDENTICAL
  // value. No data loss (live already holds the correct, adopted value), but
  // the user sees an apparent "conflict" that is in fact moot.
  it('classifies BOTH_CHANGED (not a harmless repeat SOURCE_CHANGED) when a field was already adopted into live but the snapshot was never advanced (partial-failure retry)', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 47, ausschuss: 2 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const schweissen = nodes.find((n) => n.name === 'Schweißen')!
      schweissen.cycleTimeSec = 47
      schweissen.fieldStatus = { ...schweissen.fieldStatus, cycleTimeSec: 'imported' }
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Schweißen')
    const ctField = step.fields.find((f) => f.field === 'cycleTimeSec')!
    expect(ctField.snapshotValue).toBe(42) // stale — the failed step-2 write never advanced this
    expect(ctField.sourceValue).toBe(47)
    expect(ctField.liveValue).toBe(47) // already matches source — the "conflict" is moot
    expect(ctField.status).toBe('BOTH_CHANGED')
    expect(step.status).toBe('BOTH_CHANGED')
  })

  it('rolls a step up to BOTH_CHANGED when DIFFERENT fields changed source-side vs. local-side', () => {
    // Source changes ausschuss only; local edit changes cycleTimeSec only —
    // no single field is individually BOTH_CHANGED, but the step needs
    // attention on both axes.
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 4 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const schweissen = nodes.find((n) => n.name === 'Schweißen')!
      schweissen.cycleTimeSec = 50
      schweissen.fieldStatus = { ...schweissen.fieldStatus, cycleTimeSec: 'modified' }
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Schweißen')
    expect(step.status).toBe('BOTH_CHANGED')
    expect(step.fields.find((f) => f.field === 'cycleTimeSec')!.status).toBe('LOCAL_CHANGED')
    expect(step.fields.find((f) => f.field === 'scrapRate')!.status).toBe('SOURCE_CHANGED')
  })

  it('treats a fieldStatus "modified" flag as a local change even when the value round-tripped back to the snapshot value', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const montage = nodes.find((n) => n.name === 'Montage')!
      // Value is numerically identical to the snapshot (30) — only the flag differs.
      montage.cycleTimeSec = 30
      montage.fieldStatus = { ...montage.fieldStatus, cycleTimeSec: 'modified' }
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Montage')
    expect(step.fields.find((f) => f.field === 'cycleTimeSec')!.status).toBe('LOCAL_CHANGED')
  })

  it('classifies NEW_IN_SOURCE for a step with no snapshot counterpart', () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 }),
      buildQafRow({ prozessbezeichnung: 'Verpacken', zykluszeit: 8 }),
    ]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone()

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Verpacken')
    expect(step.status).toBe('NEW_IN_SOURCE')
    expect(step.snapshotNode).toBeNull()
    expect(step.liveNode).toBeNull()
    expect(step.locallyDeleted).toBe(false)
    expect(step.fields).toEqual([])
    expect(step.sourceNode?.cycleTimeSec).toBe(8)
  })

  it('classifies REMOVED_FROM_SOURCE for a step with no source counterpart, live still present', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone()

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Reinigen')
    expect(step.status).toBe('REMOVED_FROM_SOURCE')
    expect(step.sourceNode).toBeNull()
    expect(step.locallyDeleted).toBe(false)
    expect(step.liveNode).not.toBeNull()
  })

  it('flags locallyDeleted without misclassifying status when a matched, otherwise-unchanged step was deleted locally', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Prüfen', zykluszeit: 15 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const idx = nodes.findIndex((n) => n.name === 'Prüfen')
      nodes.splice(idx, 1)
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Prüfen')
    expect(step.locallyDeleted).toBe(true)
    expect(step.status).toBe('UNCHANGED')
    expect(step.liveNode).toBeNull()
  })

  it('surfaces SOURCE_CHANGED even when the matched step was ALSO deleted locally (visibility, not adoptable)', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Prüfen', zykluszeit: 20 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const idx = nodes.findIndex((n) => n.name === 'Prüfen')
      nodes.splice(idx, 1)
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Prüfen')
    expect(step.locallyDeleted).toBe(true)
    expect(step.status).toBe('SOURCE_CHANGED')
  })

  it('flags locallyDeleted for a REMOVED_FROM_SOURCE step whose live node was also deleted', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 })]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const idx = nodes.findIndex((n) => n.name === 'Reinigen')
      nodes.splice(idx, 1)
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    const step = findStep(delta.steps, 'Reinigen')
    expect(step.status).toBe('REMOVED_FROM_SOURCE')
    expect(step.locallyDeleted).toBe(true)
  })

  it('pairs count-matched same-name repeats by preserved relative row order', () => {
    const snap = mapRows(
      [buildQafRow({ prozessbezeichnung: 'Sichtprüfung', zykluszeit: 10 }), buildQafRow({ prozessbezeichnung: 'Sichtprüfung', zykluszeit: 20 })],
      'snap2',
    )
    const src = mapRows(
      [buildQafRow({ prozessbezeichnung: 'Sichtprüfung', zykluszeit: 11 }), buildQafRow({ prozessbezeichnung: 'Sichtprüfung', zykluszeit: 22 })],
      'src2',
    )
    const live = snap.map((n) => ({ ...n }))

    const delta = computeSyncDelta({ sourceNodes: src, snapshotNodes: snap, liveNodes: live })
    expect(delta.unmatched).toEqual([])
    const matched = delta.steps.filter((s) => s.name === 'Sichtprüfung')
    expect(matched).toHaveLength(2)
    // First-in-order pair: 10 -> 11, second pair: 20 -> 22 (not cross-paired).
    const first = matched.find((s) => s.snapshotNode?.cycleTimeSec === 10)!
    const second = matched.find((s) => s.snapshotNode?.cycleTimeSec === 20)!
    expect(first.sourceNode?.cycleTimeSec).toBe(11)
    expect(second.sourceNode?.cycleTimeSec).toBe(22)
  })

  it('reports mismatched-count same-name groups as unmatched on BOTH sides, never guessing a pairing', () => {
    const snap = mapRows([buildQafRow({ prozessbezeichnung: 'Kontrolle', zykluszeit: 10 })], 'snap3')
    const src = mapRows(
      [buildQafRow({ prozessbezeichnung: 'Kontrolle', zykluszeit: 11 }), buildQafRow({ prozessbezeichnung: 'Kontrolle', zykluszeit: 12 })],
      'src3',
    )
    const live = snap.map((n) => ({ ...n }))

    const delta = computeSyncDelta({ sourceNodes: src, snapshotNodes: snap, liveNodes: live })
    expect(delta.steps.filter((s) => s.name === 'Kontrolle')).toHaveLength(0)
    expect(delta.unmatched.filter((u) => u.name === 'Kontrolle' && u.side === 'snapshot')).toHaveLength(1)
    expect(delta.unmatched.filter((u) => u.name === 'Kontrolle' && u.side === 'source')).toHaveLength(2)
  })

  it('produces a step-grain summary matching the classified steps', () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 }), // UNCHANGED
      buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 33 }), // SOURCE_CHANGED
      buildQafRow({ prozessbezeichnung: 'Prüfen', zykluszeit: 15 }), // UNCHANGED (live edited below -> LOCAL_CHANGED)
      buildQafRow({ prozessbezeichnung: 'Verpacken', zykluszeit: 8 }), // NEW_IN_SOURCE
      // 'Reinigen' absent -> REMOVED_FROM_SOURCE
    ]
    const sourceNodes = mapRows(sourceRows, 'src')
    const live = liveClone((nodes) => {
      const pruefen = nodes.find((n) => n.name === 'Prüfen')!
      pruefen.cycleTimeSec = 16
      pruefen.fieldStatus = { ...pruefen.fieldStatus, cycleTimeSec: 'modified' }
    })

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes: live })
    expect(delta.summary).toEqual({
      unchanged: 1,
      sourceChanged: 1,
      localChanged: 1,
      bothChanged: 0,
      newInSource: 1,
      removedFromSource: 1,
      unmatched: 0,
    })
  })

  describe('real-corpus anchor: a source diffed against itself', () => {
    it('is 100% UNCHANGED with zero unmatched steps', () => {
      const rows = [
        buildQafRow({ prozessbezeichnung: 'Stanzen', zykluszeit: 12, anzahlMA: 1, ruestkosten: 4.5 }),
        buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42, ausschuss: 2 }),
        buildQafRow({ prozessbezeichnung: 'Endkontrolle', zykluszeit: 9 }),
      ]
      const nodes = mapRows(rows, 'self')
      const live = nodes.map((n) => ({ ...n }))

      const delta = computeSyncDelta({ sourceNodes: nodes, snapshotNodes: nodes, liveNodes: live })
      expect(delta.unmatched).toEqual([])
      expect(delta.steps).toHaveLength(3)
      expect(delta.steps.every((s) => s.status === 'UNCHANGED')).toBe(true)
      expect(delta.summary.unchanged).toBe(3)
    })
  })
})

describe('applySyncAdoption', () => {
  const snapshotRows = [
    buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42 }),
    buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 }),
    buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
  ]
  const snapshotNodes = mapRows(snapshotRows, 'asnap')
  const IMPORT_ID = 'import-fixture-1'

  function baseline() {
    const liveNodes = snapshotNodes.map((n) => ({ ...n, fieldStatus: { ...n.fieldStatus } }))
    const connections = [
      { id: 'c0', fromNodeId: liveNodes[0].id, toNodeId: liveNodes[1].id },
      { id: 'c1', fromNodeId: liveNodes[1].id, toNodeId: liveNodes[2].id },
    ]
    return { liveNodes, connections }
  }

  it("adopting a SOURCE_CHANGED field also refreshes that field's own qafSource.fields provenance entry (cell/original), never the OLD snapshot-era one", () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 47 })]
    const sourceNodes = mapRows(sourceRows, 'aprov')
    const { liveNodes, connections } = baseline()
    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })
    const step = findStep(delta.steps, 'Schweißen')

    const result = applySyncAdoption(delta, liveNodes, connections, { mode: 'all' }, IMPORT_ID)
    const schweissen = result.nodes.find((n) => n.name === 'Schweißen')!
    expect(schweissen.qafSource?.fields?.zykluszeit?.original).toBe(47)
    expect(schweissen.qafSource?.fields?.zykluszeit).toEqual(step.sourceNode?.qafSource?.fields?.zykluszeit)
    // Untouched fields keep their ORIGINAL (snapshot-era) provenance, not the new source's.
    expect(schweissen.qafSource?.fields?.zykluszeit).not.toEqual(snapshotNodes[0].qafSource?.fields?.zykluszeit)
  })

  it("mode:'all' adopts every SOURCE_CHANGED field and every NEW_IN_SOURCE step", () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 45 }),
      buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 }),
      buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
      buildQafRow({ prozessbezeichnung: 'Verpacken', zykluszeit: 8 }),
    ]
    const sourceNodes = mapRows(sourceRows, 'asrc')
    const { liveNodes, connections } = baseline()
    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })

    const plan: QvsSyncAdoptionPlan = { mode: 'all' }
    const result = applySyncAdoption(delta, liveNodes, connections, plan, IMPORT_ID)

    const schweissen = result.nodes.find((n) => n.name === 'Schweißen')!
    expect(schweissen.cycleTimeSec).toBe(45)
    expect(schweissen.fieldStatus?.cycleTimeSec).toBe('imported')
    expect(result.nodes.some((n) => n.name === 'Verpacken')).toBe(true)
    expect(result.summary.fieldsAdopted).toBe(1)
    expect(result.summary.stepsAdded).toBe(1)
  })

  it("mode:'all' NEVER touches LOCAL_CHANGED or an unresolved BOTH_CHANGED field", () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 50 }), // source changed too -> BOTH_CHANGED with local edit below
      buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 }), // unchanged source
      buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
    ]
    const sourceNodes = mapRows(sourceRows, 'asrc2')
    const { liveNodes, connections } = baseline()
    liveNodes[0] = { ...liveNodes[0], cycleTimeSec: 47, fieldStatus: { ...liveNodes[0].fieldStatus, cycleTimeSec: 'modified' } } // Schweißen: BOTH_CHANGED
    liveNodes[1] = { ...liveNodes[1], cycleTimeSec: 31, fieldStatus: { ...liveNodes[1].fieldStatus, cycleTimeSec: 'modified' } } // Montage: LOCAL_CHANGED only

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })
    expect(findStep(delta.steps, 'Schweißen').status).toBe('BOTH_CHANGED')
    expect(findStep(delta.steps, 'Montage').status).toBe('LOCAL_CHANGED')

    const plan: QvsSyncAdoptionPlan = { mode: 'all' }
    const result = applySyncAdoption(delta, liveNodes, connections, plan, IMPORT_ID)

    // Neither the BOTH_CHANGED nor the LOCAL_CHANGED field was silently
    // overwritten by the bulk 'all' adoption — local values survive untouched.
    expect(result.nodes.find((n) => n.name === 'Schweißen')!.cycleTimeSec).toBe(47)
    expect(result.nodes.find((n) => n.name === 'Montage')!.cycleTimeSec).toBe(31)
    expect(result.summary.fieldsAdopted).toBe(0)
    expect(result.summary.conflictsResolvedTakeSource).toBe(0)
    expect(result.summary.conflictsResolvedKeepLocal).toBe(0)
    expect(result.summary.fieldsKeptLocal).toBe(1)
  })

  it("mode:'selected' only adopts explicitly selected fields and steps", () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 44, ausschuss: 3 }),
      buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 32 }),
      buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
    ]
    const sourceNodes = mapRows(sourceRows, 'asrc3')
    const { liveNodes, connections } = baseline()
    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })
    const schweissenStep = findStep(delta.steps, 'Schweißen')

    const plan: QvsSyncAdoptionPlan = {
      mode: 'selected',
      selectedFieldAdoptions: [{ stepMatchId: schweissenStep.stepMatchId, field: 'cycleTimeSec' }],
    }
    const result = applySyncAdoption(delta, liveNodes, connections, plan, IMPORT_ID)

    expect(result.nodes.find((n) => n.name === 'Schweißen')!.cycleTimeSec).toBe(44)
    expect(result.nodes.find((n) => n.name === 'Schweißen')!.scrapRate).toBeUndefined() // NOT selected, left alone
    expect(result.nodes.find((n) => n.name === 'Montage')!.cycleTimeSec).toBe(30) // SOURCE_CHANGED but not selected
    expect(result.summary.fieldsAdopted).toBe(1)
  })

  it('conflictResolutions apply regardless of mode, in either direction', () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 50 }),
      buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 33 }),
      buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
    ]
    const sourceNodes = mapRows(sourceRows, 'asrc4')
    const { liveNodes, connections } = baseline()
    liveNodes[0] = { ...liveNodes[0], cycleTimeSec: 47, fieldStatus: { ...liveNodes[0].fieldStatus, cycleTimeSec: 'modified' } }
    liveNodes[1] = { ...liveNodes[1], cycleTimeSec: 31, fieldStatus: { ...liveNodes[1].fieldStatus, cycleTimeSec: 'modified' } }

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })
    const schweissenStep = findStep(delta.steps, 'Schweißen')
    const montageStep = findStep(delta.steps, 'Montage')
    expect(schweissenStep.status).toBe('BOTH_CHANGED')
    expect(montageStep.status).toBe('BOTH_CHANGED')

    const plan: QvsSyncAdoptionPlan = {
      mode: 'none',
      conflictResolutions: [
        { stepMatchId: schweissenStep.stepMatchId, field: 'cycleTimeSec', resolution: 'take_source' },
        { stepMatchId: montageStep.stepMatchId, field: 'cycleTimeSec', resolution: 'keep_local' },
      ],
    }
    const result = applySyncAdoption(delta, liveNodes, connections, plan, IMPORT_ID)

    expect(result.nodes.find((n) => n.name === 'Schweißen')!.cycleTimeSec).toBe(50) // took source
    expect(result.nodes.find((n) => n.name === 'Montage')!.cycleTimeSec).toBe(31) // kept local
    expect(result.summary.conflictsResolvedTakeSource).toBe(1)
    expect(result.summary.conflictsResolvedKeepLocal).toBe(1)
  })

  it('removedStepDecisions default to keep, and only remove on explicit action — dropping (not re-bridging) its connections, matching editor delete-node behaviour', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42 }), buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 })]
    const sourceNodes = mapRows(sourceRows, 'asrc5')
    const { liveNodes, connections } = baseline()
    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })
    const reinigenStep = findStep(delta.steps, 'Reinigen')
    expect(reinigenStep.status).toBe('REMOVED_FROM_SOURCE')

    const keepResult = applySyncAdoption(delta, liveNodes, connections, { mode: 'all' }, IMPORT_ID)
    expect(keepResult.nodes.some((n) => n.name === 'Reinigen')).toBe(true)
    expect(keepResult.summary.stepsRemoved).toBe(0)

    const removeResult = applySyncAdoption(
      delta,
      liveNodes,
      connections,
      { mode: 'all', removedStepDecisions: [{ stepMatchId: reinigenStep.stepMatchId, action: 'remove' }] },
      IMPORT_ID,
    )
    expect(removeResult.nodes.some((n) => n.name === 'Reinigen')).toBe(false)
    expect(removeResult.summary.stepsRemoved).toBe(1)
    // The dangling connection into the removed node is dropped, not re-bridged
    // — c0 (Schweißen->Montage) survives untouched, c1 (Montage->Reinigen) is
    // dropped; no replacement Schweißen->Montage-successor edge is invented.
    const reinigenId = reinigenStep.liveNode!.id
    expect(removeResult.connections.some((c) => c.fromNodeId === reinigenId || c.toNodeId === reinigenId)).toBe(false)
    expect(removeResult.connections).toEqual([{ id: 'c0', fromNodeId: liveNodes[0].id, toNodeId: liveNodes[1].id }])
  })

  it('adopting vaClass also re-derives isValueAdded, and adopting a field resets fieldStatus to imported', () => {
    const sourceRows = [buildQafRow({ prozessbezeichnung: 'Transport zur Prüfung', zykluszeit: 42 })]
    const source = mapQafRowsToVsmNodes(sourceRows, { idFactory: idFactory('vasrc') })
    const snap = mapQafRowsToVsmNodes([buildQafRow({ prozessbezeichnung: 'Transport zur Prüfung', zykluszeit: 42 })], { idFactory: idFactory('vasnap') })
    // Force a vaClass disagreement directly (independent of conservativeVaClass's own rules).
    const sourceNodes = source.nodes.map((n) => ({ ...n, vaClass: 'nva' as const }))
    const snapshotNodesLocal = snap.nodes.map((n) => ({ ...n, vaClass: 'nnva' as const }))
    const liveNodes = snapshotNodesLocal.map((n) => ({ ...n }))
    const connections: never[] = []

    const delta = computeSyncDelta({ sourceNodes, snapshotNodes: snapshotNodesLocal, liveNodes })
    const step = findStep(delta.steps, 'Transport zur Prüfung')
    expect(step.fields.find((f) => f.field === 'vaClass')!.status).toBe('SOURCE_CHANGED')

    const result = applySyncAdoption(delta, liveNodes, connections, { mode: 'all' }, IMPORT_ID)
    const node = result.nodes[0]
    expect(node.vaClass).toBe('nva')
    expect(node.isValueAdded).toBe(false)
    expect(node.fieldStatus?.vaClass).toBe('imported')
  })

  it('appends adopted NEW_IN_SOURCE steps after existing nodes without disturbing existing (incl. non-linear) connections', () => {
    const sourceRows = [
      buildQafRow({ prozessbezeichnung: 'Schweißen', zykluszeit: 42 }),
      buildQafRow({ prozessbezeichnung: 'Montage', zykluszeit: 30 }),
      buildQafRow({ prozessbezeichnung: 'Reinigen', zykluszeit: 5 }),
      buildQafRow({ prozessbezeichnung: 'Verpacken', zykluszeit: 8 }),
    ]
    const sourceNodes = mapRows(sourceRows, 'asrc6')
    const { liveNodes } = baseline()
    // Deliberately non-linear/manual rewiring: Montage -> Schweißen (reversed).
    const customConnections = [{ id: 'manual-1', fromNodeId: liveNodes[1].id, toNodeId: liveNodes[0].id }]
    const delta = computeSyncDelta({ sourceNodes, snapshotNodes, liveNodes })

    const result = applySyncAdoption(delta, liveNodes, customConnections, { mode: 'all' }, IMPORT_ID)

    expect(result.connections.find((c) => c.id === 'manual-1')).toBeTruthy()
    const verpacken = result.nodes.find((n) => n.name === 'Verpacken')!
    expect(verpacken.qafSource?.importId).toBe(IMPORT_ID)
    const lastExisting = result.nodes[liveNodes.length - 1]
    expect(verpacken.x).toBeGreaterThan(lastExisting.x)
    expect(result.connections.some((c) => c.fromNodeId === lastExisting.id && c.toNodeId === verpacken.id)).toBe(true)
  })
})
