// A11 Validation Engine (§17) — one hit-case + one no-hit-case per check,
// plus the mandated edge cases (double counting, rework 0%/100%) and the
// documented-gaps list. FIXTURE-DATEN-REGEL: every id/name/number below is
// FREE INVENTION.

import { describe, expect, it } from 'vitest'
import type { VsmConnection, VsmNode } from '@/lib/vsm-types'
import { KNOWN_VALIDATION_GAPS, runValidation, type ValidationContext } from '../internal/validation'

function makeNode(id: string, overrides: Partial<VsmNode> = {}): VsmNode {
  return { id, type: 'process', x: 0, y: 0, name: id, ...overrides }
}
function makeConn(id: string, fromNodeId: string, toNodeId: string, overrides: Partial<VsmConnection> = {}): VsmConnection {
  return { id, fromNodeId, toNodeId, ...overrides }
}
function ids(issues: { id: string }[]): string[] {
  return issues.map((i) => i.id)
}

describe('runValidation — structural (§17.1)', () => {
  it('flags a connection pointing at a non-existent node (critical)', () => {
    const result = runValidation([makeNode('a')], [makeConn('c1', 'a', 'ghost')])
    const issue = result.issues.find((i) => i.id === 'structural.dangling-connection-ref')
    expect(issue).toBeDefined()
    expect(issue?.severity).toBe('critical')
  })

  // Review-Fix F9 (adversarial review, PR #353): whatDe/whyDe must describe
  // what actually broke — a dangling materialFlow edge stays worded around
  // Timeline/Engpass/Bestand (the pre-existing, still-accurate wording); a
  // dangling information edge gets its OWN wording, never the materialFlow
  // one (which claimed calculations it doesn't affect and called it a
  // "Materialfluss" when it explicitly is not one).
  it('F9 FIX: a dangling materialFlow connection keeps the Materialfluss/Timeline wording', () => {
    const result = runValidation([makeNode('a')], [makeConn('c1', 'a', 'ghost')])
    const issue = result.issues.find((i) => i.id === 'structural.dangling-connection-ref')
    expect(issue?.whyDe).toContain('Materialfluss')
    expect(issue?.whatDe).toContain('Verbindung')
  })

  it('F9 FIX: a dangling information connection gets information-flow-specific wording, never the materialFlow one', () => {
    const result = runValidation([makeNode('a')], [makeConn('c1', 'a', 'ghost', { kind: 'information' })])
    const issue = result.issues.find((i) => i.id === 'structural.dangling-connection-ref')
    expect(issue).toBeDefined()
    expect(issue?.severity).toBe('critical')
    expect(issue?.whatDe).toContain('Informationsfluss')
    expect(issue?.whyDe).toContain('Informationsfluss')
    expect(issue?.whyDe).not.toContain('Materialfluss')
    expect(issue?.whyDe).not.toContain('Timeline')
    expect(issue?.whyDe).not.toContain('Engpass')
    expect(issue?.whyDe).not.toContain('Bestand')
  })

  it('does not flag a connection between two real nodes', () => {
    const result = runValidation([makeNode('a'), makeNode('b')], [makeConn('c1', 'a', 'b')])
    expect(ids(result.issues)).not.toContain('structural.dangling-connection-ref')
  })

  it('detects a directed cycle (a -> b -> c -> a)', () => {
    const nodes = [makeNode('a'), makeNode('b'), makeNode('c')]
    const connections = [makeConn('c1', 'a', 'b'), makeConn('c2', 'b', 'c'), makeConn('c3', 'c', 'a')]
    const result = runValidation(nodes, connections)
    expect(ids(result.issues)).toContain('structural.unexplained-loop')
  })

  it('does not flag a linear, acyclic chain', () => {
    const nodes = [makeNode('a'), makeNode('b'), makeNode('c')]
    const connections = [makeConn('c1', 'a', 'b'), makeConn('c2', 'b', 'c')]
    const result = runValidation(nodes, connections)
    expect(ids(result.issues)).not.toContain('structural.unexplained-loop')
  })

  // Wertstrom P2 (A7, KAR-878/KAR-986): findFirstCycle excludes
  // kind === 'information' edges from the cycle search (see its own doc
  // comment) — this is the exact standard VSM topology that motivated the
  // fix: customer→PPS (order) and PPS→supplier (schedule) information
  // edges close a loop against the supplier→process→customer material
  // chain, but that is not a modelling error.
  it('P2: does not flag the standard customer->PPS->supplier information-flow loop around a material-flow chain', () => {
    const nodes = [makeNode('supplier'), makeNode('process'), makeNode('customer', { type: 'customer' }), makeNode('pps')]
    const connections = [
      makeConn('material-1', 'supplier', 'process'),
      makeConn('material-2', 'process', 'customer'),
      makeConn('info-1', 'customer', 'pps', { kind: 'information' }),
      makeConn('info-2', 'pps', 'supplier', { kind: 'information' }),
    ]
    const result = runValidation(nodes, connections)
    expect(ids(result.issues)).not.toContain('structural.unexplained-loop')
  })

  it('P2: still flags a genuine materialFlow cycle even when an unrelated information edge is also present', () => {
    const nodes = [makeNode('a'), makeNode('b'), makeNode('c')]
    const connections = [
      makeConn('c1', 'a', 'b'),
      makeConn('c2', 'b', 'c'),
      makeConn('c3', 'c', 'a'), // closes a materialFlow cycle a->b->c->a
      makeConn('info', 'c', 'a', { kind: 'information' }), // present alongside, must not mask the real cycle
    ]
    const result = runValidation(nodes, connections)
    expect(ids(result.issues)).toContain('structural.unexplained-loop')
  })

  it('P2: an all-information-flow loop is not flagged (only materialFlow-kind edges are searched for cycles)', () => {
    const nodes = [makeNode('a'), makeNode('b'), makeNode('c')]
    const connections = [
      makeConn('c1', 'a', 'b', { kind: 'information' }),
      makeConn('c2', 'b', 'c', { kind: 'information' }),
      makeConn('c3', 'c', 'a', { kind: 'information' }),
    ]
    const result = runValidation(nodes, connections)
    expect(ids(result.issues)).not.toContain('structural.unexplained-loop')
  })

  it('flags a missing supplier and a missing customer independently', () => {
    const result = runValidation([makeNode('a')], [])
    expect(ids(result.issues)).toContain('structural.missing-supplier')
    expect(ids(result.issues)).toContain('structural.missing-customer')
  })

  it('does not flag supplier/customer when both exist', () => {
    const result = runValidation([makeNode('s', { type: 'supplier' }), makeNode('c', { type: 'customer' })], [])
    expect(ids(result.issues)).not.toContain('structural.missing-supplier')
    expect(ids(result.issues)).not.toContain('structural.missing-customer')
  })

  it('flags a node with zero connections', () => {
    const result = runValidation([makeNode('a'), makeNode('lonely')], [])
    const issue = result.issues.find((i) => i.id === 'structural.node-without-connection' && i.nodeId === 'lonely')
    expect(issue).toBeDefined()
    // Both 'a' and 'lonely' are isolated here (no connections at all) — check the OTHER one is also flagged.
    expect(result.issues.filter((i) => i.id === 'structural.node-without-connection')).toHaveLength(2)
  })

  it('F7 FIX — Verifier-Repro: flags nodes in a connected-but-isolated island, separate from the main Lieferant->Kunde flow', () => {
    // Main flow: s -> p1 -> c (3 nodes, 2 connections). Island: p2 -> p3 (2 nodes, 1 connection).
    const nodes = [makeNode('s', { type: 'supplier' }), makeNode('p1'), makeNode('c', { type: 'customer' }), makeNode('p2'), makeNode('p3')]
    const connections = [makeConn('c1', 's', 'p1'), makeConn('c2', 'p1', 'c'), makeConn('c3', 'p2', 'p3')]
    const result = runValidation(nodes, connections)
    const islandIssues = result.issues.filter((i) => i.id === 'structural.node-unreachable-from-main-flow')
    expect(islandIssues.map((i) => i.nodeId).sort()).toEqual(['p2', 'p3'])
    expect(islandIssues.every((i) => i.severity === 'warning')).toBe(true)
    // All 5 nodes have >= 1 connection -> the OLD zero-connection check must NOT also fire.
    expect(ids(result.issues)).not.toContain('structural.node-without-connection')
  })

  it('does not flag isolated islands when the whole graph is one connected component', () => {
    const nodes = [makeNode('a'), makeNode('b'), makeNode('c')]
    const connections = [makeConn('c1', 'a', 'b'), makeConn('c2', 'b', 'c')]
    const result = runValidation(nodes, connections)
    expect(ids(result.issues)).not.toContain('structural.node-unreachable-from-main-flow')
  })

  it('does not flag isolated islands when there are no connections at all (that is the zero-connection case, not this one)', () => {
    const result = runValidation([makeNode('a'), makeNode('b')], [])
    expect(ids(result.issues)).not.toContain('structural.node-unreachable-from-main-flow')
  })
})

describe('runValidation — time (§17.2)', () => {
  it('flags a negative cycle time (critical)', () => {
    const result = runValidation([makeNode('a', { cycleTimeSec: -5 })], [])
    expect(result.issues.find((i) => i.id === 'time.negative-value' && i.nodeId === 'a')?.severity).toBe('critical')
  })

  it('flags a negative connection transportTimeSec', () => {
    const result = runValidation([makeNode('a'), makeNode('b')], [makeConn('c1', 'a', 'b', { transportTimeSec: -1 })])
    expect(result.issues.find((i) => i.id === 'time.negative-value' && i.connectionId === 'c1')).toBeDefined()
  })

  it('F6 FIX: flags negative machineTimeSec/manualTimeSec (previously absent from NUMERIC_TIME_FIELDS, never checked)', () => {
    const result = runValidation([makeNode('a', { machineTimeSec: -30, manualTimeSec: -10 })], [])
    const issues = result.issues.filter((i) => i.id === 'time.negative-value' && i.nodeId === 'a')
    expect(issues).toHaveLength(2)
    expect(issues.every((i) => i.severity === 'critical')).toBe(true)
  })

  it('flags an EXPLICIT zero cycle time on a process node, but not an unset one', () => {
    const withZero = runValidation([makeNode('a', { cycleTimeSec: 0 })], [])
    expect(ids(withZero.issues)).toContain('time.implausible-zero-cycle-time')
    const withUndefined = runValidation([makeNode('b')], [])
    expect(ids(withUndefined.issues)).not.toContain('time.implausible-zero-cycle-time')
  })

  it('F13 FIX: also flags an EXPLICIT zero cycle time on a "machine" node, consistent with the F1 isStationNode decision', () => {
    const result = runValidation([makeNode('m', { type: 'machine', cycleTimeSec: 0 })], [])
    expect(ids(result.issues)).toContain('time.implausible-zero-cycle-time')
  })

  it('flags setup time exceeding the net shift time WHEN a shift model is given', () => {
    // netSecPerShift = 8*3600-1800 = 27000. setupTimeSec 28000 > 27000.
    const context: ValidationContext = { shiftModel: { hoursPerShift: 8, shiftsPerDay: 1, breakMinPerShift: 30 } }
    const result = runValidation([makeNode('a', { setupTimeSec: 28000 })], [], context)
    expect(ids(result.issues)).toContain('time.setup-exceeds-shift')
  })

  it('SKIPS the setup-exceeds-shift check entirely when no shift model is given (not a fake pass)', () => {
    const result = runValidation([makeNode('a', { setupTimeSec: 999999 })], [])
    expect(ids(result.issues)).not.toContain('time.setup-exceeds-shift')
  })

  it('flags a takt value with no accompanying demand basis', () => {
    const result = runValidation([], [], { taktTimeSec: 45 })
    expect(ids(result.issues)).toContain('time.takt-without-demand')
  })

  it('does not flag takt-without-demand once a demand rate is supplied', () => {
    const result = runValidation([], [], { taktTimeSec: 45, demandUnitsPerDay: 500 })
    expect(ids(result.issues)).not.toContain('time.takt-without-demand')
  })
})

describe('runValidation — capacity (§17.3)', () => {
  it('flags OEE above 100 percent', () => {
    const result = runValidation([makeNode('a', { oee: 150 })], [])
    expect(result.issues.find((i) => i.id === 'capacity.oee-out-of-range')?.severity).toBe('critical')
  })

  it('DOUBLE-COUNTING: flags a node with BOTH OEE and a standalone availability override', () => {
    const context: ValidationContext = { capacityOverridesByNodeId: { a: { availabilityPct: 50 } } }
    const result = runValidation([makeNode('a', { oee: 50 })], [], context)
    expect(result.issues.find((i) => i.id === 'capacity.double-counted-availability')?.severity).toBe('critical')
  })

  it('does NOT flag double counting when only OEE (no standalone override) is set', () => {
    const result = runValidation([makeNode('a', { oee: 50 })], [])
    expect(ids(result.issues)).not.toContain('capacity.double-counted-availability')
  })

  it('flags an out-of-range availability override', () => {
    const context: ValidationContext = { capacityOverridesByNodeId: { a: { availabilityPct: 120 } } }
    const result = runValidation([makeNode('a')], [], context)
    expect(ids(result.issues)).toContain('capacity.availability-out-of-range')
  })

  it('F14 FIX: a NEGATIVE parallel-units override is critical (physically impossible); 0 stays a warning', () => {
    const negative = runValidation([makeNode('a')], [], { capacityOverridesByNodeId: { a: { numParallelUnits: -1 } } })
    const negIssue = negative.issues.find((i) => i.id === 'capacity.invalid-parallel-units')
    expect(negIssue?.severity).toBe('critical')

    const zero = runValidation([makeNode('a')], [], { capacityOverridesByNodeId: { a: { numParallelUnits: 0 } } })
    const zeroIssue = zero.issues.find((i) => i.id === 'capacity.invalid-parallel-units')
    expect(zeroIssue?.severity).toBe('warning')
  })

  it('flags a process whose effective cycle time exceeds takt (capacity below demand / utilization above 100%, merged check)', () => {
    const result = runValidation([makeNode('a', { cycleTimeSec: 80, oee: 100 })], [], { taktTimeSec: 60 })
    expect(ids(result.issues)).toContain('capacity.utilization-above-takt')
  })

  it('does not flag utilization when the process comfortably beats takt', () => {
    const result = runValidation([makeNode('a', { cycleTimeSec: 30, oee: 100 })], [], { taktTimeSec: 60 })
    expect(ids(result.issues)).not.toContain('capacity.utilization-above-takt')
  })

  it('F1 FIX: also flags a "machine" node whose effective cycle time exceeds takt (previously silently excluded)', () => {
    const result = runValidation([makeNode('m', { type: 'machine', cycleTimeSec: 80, oee: 100 })], [], { taktTimeSec: 60 })
    const issue = result.issues.find((i) => i.id === 'capacity.utilization-above-takt')
    expect(issue).toBeDefined()
    expect(issue?.nodeId).toBe('m')
  })
})

describe('runValidation — quality (§17.4)', () => {
  it('flags scrap rate above 100 percent', () => {
    const result = runValidation([makeNode('a', { scrapRate: 150 })], [])
    expect(result.issues.find((i) => i.id === 'quality.scrap-out-of-range')?.severity).toBe('critical')
  })

  it('F2 FIX: flags qi.scrapRatePct (ProcessQualityInput) out of range directly — distinct field from VsmNode.scrapRate above, previously unchecked', () => {
    const over = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', scrapRatePct: 150 }] })
    expect(over.issues.find((i) => i.id === 'quality.scrap-rate-pct-out-of-range')?.severity).toBe('critical')

    const under = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', scrapRatePct: -5 }] })
    expect(under.issues.find((i) => i.id === 'quality.scrap-rate-pct-out-of-range')?.severity).toBe('critical')
  })

  it('edge case: rework rate exactly 0% never triggers rework-out-of-range or missing-rework-time', () => {
    const result = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', reworkRatePct: 0 }] })
    expect(ids(result.issues)).not.toContain('quality.rework-out-of-range')
    expect(ids(result.issues)).not.toContain('quality.missing-rework-time')
  })

  it('edge case: rework rate 100% WITH a time is valid — no issue', () => {
    const result = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', reworkRatePct: 100, reworkTimeSec: 12 }] })
    expect(ids(result.issues)).not.toContain('quality.rework-out-of-range')
    expect(ids(result.issues)).not.toContain('quality.missing-rework-time')
  })

  it('flags a nonzero rework rate with no rework time (§17.4 "missing rework time")', () => {
    const result = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', reworkRatePct: 30 }] })
    expect(ids(result.issues)).toContain('quality.missing-rework-time')
  })

  it('flags scrap+rework summing above 100 percent (contradictory yield and scrap)', () => {
    const result = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', scrapRatePct: 70, reworkRatePct: 50 }] })
    expect(result.issues.find((i) => i.id === 'quality.contradictory-yield-scrap')?.severity).toBe('critical')
  })

  it('flags an out-of-range cumulative yield caused by an out-of-range per-process scrap rate', () => {
    const result = runValidation([makeNode('a')], [], { qualityInputs: [{ nodeId: 'a', scrapRatePct: 150 }] })
    expect(ids(result.issues)).toContain('quality.cumulative-yield-out-of-range')
  })

  it('a plausible quality chain (10%/20% scrap) triggers none of the quality checks', () => {
    const result = runValidation(
      [makeNode('a'), makeNode('b')],
      [],
      { qualityInputs: [{ nodeId: 'a', scrapRatePct: 10 }, { nodeId: 'b', scrapRatePct: 20 }] },
    )
    expect(result.issues.filter((i) => i.category === 'quality')).toEqual([])
  })
})

describe('runValidation — inventory (§17.5)', () => {
  it('flags a negative inventory quantity', () => {
    const result = runValidation([makeNode('inv', { type: 'inventory', quantity: -5 })], [])
    expect(result.issues.find((i) => i.id === 'inventory.negative-quantity')?.severity).toBe('critical')
  })

  it('flags an inventory quantity with no demand basis to compute coverage from', () => {
    const result = runValidation([makeNode('inv', { type: 'inventory', quantity: 500 })], [])
    expect(ids(result.issues)).toContain('inventory.missing-demand-basis')
  })

  it('does not flag missing-demand-basis once a demand rate is supplied', () => {
    const result = runValidation([makeNode('inv', { type: 'inventory', quantity: 500 })], [], { demandUnitsPerDay: 100 })
    expect(ids(result.issues)).not.toContain('inventory.missing-demand-basis')
  })

  it('F12 FIX: distinguishes demandUnitsPerDay TRULY unknown from KNOWN zero — shows the real value instead of always saying "unbekannt"', () => {
    const unknown = runValidation([makeNode('inv', { type: 'inventory', quantity: 500 })], [])
    const unknownIssue = unknown.issues.find((i) => i.id === 'inventory.missing-demand-basis')
    expect(unknownIssue?.valueDe).toContain('demandUnitsPerDay: unbekannt')

    const knownZero = runValidation([makeNode('inv', { type: 'inventory', quantity: 500 })], [], { demandUnitsPerDay: 0 })
    const zeroIssue = knownZero.issues.find((i) => i.id === 'inventory.missing-demand-basis')
    expect(zeroIssue).toBeDefined()
    expect(zeroIssue?.valueDe).toContain('demandUnitsPerDay: 0')
    expect(zeroIssue?.whatDe).not.toContain('unbekannt')
  })

  // Review-Fix F4 (adversarial review, PR #353): A8's inventoryMaxQuantity
  // ("FIFO mit Kapazität") previously flowed into no validation at all.
  it('F4 FIX: flags a negative inventoryMaxQuantity as critical', () => {
    const result = runValidation([makeNode('inv', { type: 'inventory', inventoryMaxQuantity: -1 })], [])
    const issue = result.issues.find((i) => i.id === 'inventory.negative-max-quantity')
    expect(issue).toBeDefined()
    expect(issue?.severity).toBe('critical')
  })

  it('F4 FIX: does not flag a non-negative inventoryMaxQuantity', () => {
    const result = runValidation([makeNode('inv', { type: 'inventory', inventoryMaxQuantity: 0 })], [])
    expect(ids(result.issues)).not.toContain('inventory.negative-max-quantity')
  })

  it('F4 FIX: flags FIFO quantity above max capacity as a WARNING, not critical (a real lane can overflow)', () => {
    const result = runValidation(
      [makeNode('inv', { type: 'inventory', inventoryKind: 'fifo', quantity: 60, inventoryMaxQuantity: 50 })],
      [],
    )
    const issue = result.issues.find((i) => i.id === 'inventory.fifo-capacity-exceeded')
    expect(issue).toBeDefined()
    expect(issue?.severity).toBe('warning')
  })

  it('F4 FIX: does not flag FIFO quantity exactly AT capacity (0-boundary — equal is not "above")', () => {
    const result = runValidation(
      [makeNode('inv', { type: 'inventory', inventoryKind: 'fifo', quantity: 50, inventoryMaxQuantity: 50 })],
      [],
    )
    expect(ids(result.issues)).not.toContain('inventory.fifo-capacity-exceeded')
  })

  it('F4 FIX: does not flag FIFO quantity comfortably below capacity', () => {
    const result = runValidation(
      [makeNode('inv', { type: 'inventory', inventoryKind: 'fifo', quantity: 10, inventoryMaxQuantity: 50 })],
      [],
    )
    expect(ids(result.issues)).not.toContain('inventory.fifo-capacity-exceeded')
  })

  it('F4 FIX: does NOT flag a non-FIFO inventory whose quantity exceeds inventoryMaxQuantity (field is only meaningful for fifo)', () => {
    const pushResult = runValidation(
      [makeNode('inv', { type: 'inventory', inventoryKind: 'push', quantity: 60, inventoryMaxQuantity: 50 })],
      [],
    )
    expect(ids(pushResult.issues)).not.toContain('inventory.fifo-capacity-exceeded')

    const genericResult = runValidation(
      [makeNode('inv', { type: 'inventory', quantity: 60, inventoryMaxQuantity: 50 })],
      [],
    )
    expect(ids(genericResult.issues)).not.toContain('inventory.fifo-capacity-exceeded')
  })
})

describe('runValidation — documented gaps', () => {
  it('always returns the same static KNOWN_VALIDATION_GAPS list, regardless of input', () => {
    expect(runValidation([], []).gaps).toBe(KNOWN_VALIDATION_GAPS)
  })

  it('the brief\'s own named example (reorder point) is present as a documented gap, not a fake check', () => {
    expect(KNOWN_VALIDATION_GAPS.some((g) => g.id === 'inventory.reorder-point-above-max')).toBe(true)
    expect(KNOWN_VALIDATION_GAPS.every((g) => g.reasonDe.length > 0)).toBe(true)
  })

  it('F11 FIX: "broken material flows" and "inconsistent parallel resources" are now documented gaps, closing the README completeness claim', () => {
    expect(KNOWN_VALIDATION_GAPS.some((g) => g.id === 'structural.broken-material-flow')).toBe(true)
    expect(KNOWN_VALIDATION_GAPS.some((g) => g.id === 'capacity.inconsistent-parallel-resources')).toBe(true)
  })

  // Wertstrom P2 (A7, KAR-878/KAR-986): VsmConnection.kind exists now —
  // checkDanglingConnections already validated every connection regardless
  // of kind, so this gap is resolved, not just re-scoped.
  it('P2: "broken information flows" is no longer a documented gap (VsmConnection.kind now exists)', () => {
    expect(KNOWN_VALIDATION_GAPS.some((g) => g.id === 'structural.broken-information-flow')).toBe(false)
  })
})

describe('runValidation — orchestration smoke test', () => {
  it('runs all checks together on a small realistic graph without throwing, carries an explain object', () => {
    const nodes = [makeNode('s', { type: 'supplier' }), makeNode('p1', { cycleTimeSec: 45, oee: 92 }), makeNode('c', { type: 'customer', demand: 400 })]
    const connections = [makeConn('c1', 's', 'p1'), makeConn('c2', 'p1', 'c')]
    const result = runValidation(nodes, connections, { taktTimeSec: 50, demandUnitsPerDay: 400 })
    expect(Array.isArray(result.issues)).toBe(true)
    expect(result.explain.formula.length).toBeGreaterThan(0)
    expect(result.explain.dataBasis.length).toBeGreaterThan(0)
  })
})
