// A6 Bottleneck Detection v2 (§8.8) — hand-calculated tests.
// FIXTURE-DATEN-REGEL: every id/name/number below is FREE INVENTION.

import { describe, expect, it } from 'vitest'
import type { VsmNode } from '@/lib/vsm-types'
import { computeBottleneckV2 } from '../internal/bottleneck'

function makeNode(id: string, overrides: Partial<VsmNode> = {}): VsmNode {
  return { id, type: 'process', x: 0, y: 0, name: id, ...overrides }
}

describe('computeBottleneckV2', () => {
  it('hand calc: with OEE 100% (effective CT == raw CT), the node exceeding takt is primary, the other is a secondary candidate', () => {
    // effectiveCT a = 60s (oee 100% -> no derating). effectiveCT b = 80s.
    // util a = 60/70*100 = 85.71%. util b = 80/70*100 = 114.29% (primary).
    // overTaktPct b = (80-70)/70*100 = 14.2857...% -> rounds to 14.3.
    const nodes = [makeNode('a', { cycleTimeSec: 60, oee: 100 }), makeNode('b', { cycleTimeSec: 80, oee: 100 })]
    const result = computeBottleneckV2(nodes, 70)
    expect(result.degraded).toBe(false)
    expect(result.primary?.nodeId).toBe('b')
    expect(result.primary?.effectiveCycleTimeSec).toBe(80)
    expect(result.primary?.overTaktPct).toBeCloseTo(14.2857, 3)
    expect(result.secondary.map((c) => c.nodeId)).toEqual(['a']) // 85.71% >= 80% watch threshold
    // C4-Fix (Wertstrom P7): formatRound1 now renders a German decimal comma
    // (deutsches Zahlenformat doctrine) — "14,3%", never JS's own "14.3%".
    expect(result.reasonDe).toBe('b ist der primäre Engpass: die effektive Zykluszeit (80s) liegt 14,3% über dem Kundentakt (70s).')
    expect(result.confidence).toBe('high')
  })

  it('hand calc: nobody over takt -> the relative-utilization sentence branch, not the over-takt one', () => {
    // util a = 50/100*100 = 50%. util b = 40/100*100 = 40%. primary = a (higher).
    const nodes = [makeNode('a', { cycleTimeSec: 50, oee: 100 }), makeNode('b', { cycleTimeSec: 40, oee: 100 })]
    const result = computeBottleneckV2(nodes, 100)
    expect(result.primary?.nodeId).toBe('a')
    expect(result.primary?.overTaktPct).toBe(-50)
    expect(result.reasonDe).toBe('Kein Prozess- oder Maschinen-Node überschreitet den Kundentakt (100s). a hat mit 50% die höchste Auslastung und ist damit der relative Engpass.')
    expect(result.secondary).toEqual([]) // b's 40% is below the 80% watch threshold
  })

  it('DEGRADES to the legacy max-cycle-time heuristic when no takt is given, and says so honestly', () => {
    const nodes = [makeNode('a', { cycleTimeSec: 15 }), makeNode('b', { cycleTimeSec: 45 }), makeNode('c', { cycleTimeSec: 25 })]
    const result = computeBottleneckV2(nodes, null)
    expect(result.degraded).toBe(true)
    expect(result.primary?.nodeId).toBe('b') // highest raw cycleTimeSec, same as findBottleneckId
    expect(result.confidence).toBe('low')
    expect(result.secondary).toEqual([])
  })

  it('DEGRADES when fewer than 2 process nodes have a computable effective cycle time, even with a takt given', () => {
    const nodes = [makeNode('a', { cycleTimeSec: 50, oee: 100 })]
    const result = computeBottleneckV2(nodes, 40)
    expect(result.degraded).toBe(true)
    expect(result.primary).toBeNull() // legacy fallback also needs >= 2 nodes
    expect(result.reasonDe).toBeNull()
  })

  it('confidence is "medium" when takt + >=2 usable nodes exist but not every node has OEE or a measured capacity', () => {
    const nodes = [makeNode('a', { cycleTimeSec: 50 }), makeNode('b', { cycleTimeSec: 80, oee: 90 })]
    const result = computeBottleneckV2(nodes, 60)
    expect(result.degraded).toBe(false)
    expect(result.confidence).toBe('medium')
  })

  it('F1 FIX: a "machine" node IS a full bottleneck candidate (previously silently excluded)', () => {
    const nodes = [makeNode('a', { type: 'machine', cycleTimeSec: 999, oee: 100 }), makeNode('b', { cycleTimeSec: 50, oee: 100 })]
    const result = computeBottleneckV2(nodes, 40)
    // 2 usable stations ('a' machine + 'b' process) -> full v2 model runs, not degraded.
    expect(result.degraded).toBe(false)
    expect(result.primary?.nodeId).toBe('a')
  })

  it('F1 FIX — Verifier-Repro: process 50s/55s + machine 200s, Takt 60s -> the machine is the bottleneck, not silently dropped', () => {
    // p1 CT=50 oee=100 -> effCT=50, util=50/60*100=83.33%.
    // p2 CT=55 oee=100 -> effCT=55, util=55/60*100=91.67%.
    // m1 (type=machine) CT=200 oee=100 -> effCT=200, util=200/60*100=333.33% (primary).
    // overTaktPct m1 = (200-60)/60*100 = 233.33...% -> rounds to 233.3.
    const nodes = [
      makeNode('p1', { cycleTimeSec: 50, oee: 100 }),
      makeNode('p2', { cycleTimeSec: 55, oee: 100 }),
      makeNode('m1', { type: 'machine', cycleTimeSec: 200, oee: 100 }),
    ]
    const result = computeBottleneckV2(nodes, 60)
    expect(result.degraded).toBe(false)
    expect(result.primary?.nodeId).toBe('m1')
    expect(result.primary?.utilizationPct).toBeCloseTo(333.333, 2)
    expect(result.primary?.overTaktPct).toBeCloseTo(233.333, 2)
    // C4-Fix (Wertstrom P7): German decimal comma, see other hand-calc test above.
    expect(result.reasonDe).toBe('m1 ist der primäre Engpass: die effektive Zykluszeit (200s) liegt 233,3% über dem Kundentakt (60s).')
    expect(result.secondary.map((c) => c.nodeId)).toEqual(['p2', 'p1']) // both >= 80% watch threshold, sorted by utilization desc
    expect(result.confidence).toBe('high')
  })

  it('F5 FIX: a node measured ONLY via capacityPerHour (no cycleTimeSec) no longer degrades confidence to "medium"', () => {
    // a: capacityPerHour=60 (measured, no cycleTimeSec) -> effCT = 3600/60 = 60s.
    // b: cycleTimeSec=50, oee=90 -> derived effCT.
    const nodes = [makeNode('a', { cycleTimeSec: undefined, capacityPerHour: 60 }), makeNode('b', { cycleTimeSec: 50, oee: 90 })]
    const result = computeBottleneckV2(nodes, 40)
    expect(result.degraded).toBe(false)
    expect(result.primary).not.toBeNull()
    expect(result.confidence).toBe('high')
  })

  it('a station node that drops out of the analysis (oee set, but no cycleTimeSec/capacityPerHour) caps confidence at "medium" and is named in the exclusions', () => {
    // a/b are fully usable; c has an OEE but NO time/capacity source at all,
    // so capacity.ts cannot compute an effective CT for it — c is invisible
    // to the candidate ranking and must therefore block a "high" claim
    // (same invisibility class as the F1 machine-node finding).
    const nodes = [
      makeNode('a', { cycleTimeSec: 50, oee: 100 }),
      makeNode('b', { cycleTimeSec: 40, oee: 100 }),
      makeNode('c', { oee: 95 }),
    ]
    const result = computeBottleneckV2(nodes, 100)
    expect(result.degraded).toBe(false)
    expect(result.confidence).toBe('medium')
    expect(result.explain.exclusions.join(' ')).toContain('"c"')
  })

  it('carries a formula/dataBasis/exclusions explain object in both the degraded and full paths', () => {
    const degraded = computeBottleneckV2([], null).explain
    expect(degraded.formula.length).toBeGreaterThan(0)
    const full = computeBottleneckV2([makeNode('a', { cycleTimeSec: 10, oee: 100 }), makeNode('b', { cycleTimeSec: 20, oee: 100 })], 15).explain
    expect(full.formula.length).toBeGreaterThan(0)
    expect(full.exclusions.length).toBeGreaterThan(0)
  })
})
