// Stationsbegriff, Takt und Kapazität (Kap. 7.4/8.8, R-10).

import { describe, it, expect } from 'vitest'
import {
  classifyRows,
  computeTakt,
  computeCapacity,
  stationsOverPeak,
  type ManufacturingRow,
} from '../manufacturing-stations'

const row = (r: number, name: string, cycle: number | null, workers: number | null, cost: number | null): ManufacturingRow => ({
  row: r,
  name,
  cycleSeconds: cycle,
  workers,
  costPerPiece: cost,
})

describe('classifyRows — der Stationsbegriff', () => {
  it('zählt eine Zeile mit Zykluszeit als Station', () => {
    expect(classifyRows([row(1, 'Montage', 28, 1, 0.28)])[0].kind).toBe('station')
  })

  it('zählt auch ohne Zykluszeit, wenn Personal oder Kosten anfallen', () => {
    // Die Spezifikation nennt drei gleichwertige Merkmale; eines genügt.
    expect(classifyRows([row(1, 'Prüfen', null, 2, null)])[0].kind).toBe('station')
    expect(classifyRows([row(1, 'Prüfen', null, null, 0.5)])[0].kind).toBe('station')
  })

  it('zählt eine benannte Zeile ohne jeden Parameter NICHT als Station', () => {
    // Der Fall, der den Zählstreit auflöst: „Guide Pin Assy" trägt einen Namen,
    // aber weder Zeit noch Personal noch Kosten. Als Station gezählt würde sie
    // die Fertigungstiefe überzeichnen.
    const c = classifyRows([row(115, 'Guide Pin Assy', null, null, null)])[0]
    expect(c.kind).toBe('named_without_parameters')
    expect(c.kind).not.toBe('station')
  })

  it('unterscheidet die benannte Zeile von einer leeren', () => {
    // Die benannte ist ein Befund, die leere ist Rauschen.
    expect(classifyRows([row(1, '   ', null, null, null)])[0].kind).toBe('sub_operation')
  })

  it('zählt eine Zykluszeit von 0 nicht als Parameter', () => {
    expect(classifyRows([row(1, 'Warten', 0, 0, 0)])[0].kind).toBe('named_without_parameters')
  })
})

describe('computeTakt', () => {
  // Prämissen des Referenzfalls: 5400 Arbeitsstunden im Jahr.
  const base = { hoursPerYear: 5400, peakVolumePerYear: 588377, totalVolume: 2636771, lifetimeYears: 9 }

  it('rechnet Sekunden pro Jahr aus den Arbeitsstunden', () => {
    expect(computeTakt(base).secondsPerYear).toBe(19_440_000)
  })

  it('trifft die Taktzeit des Spitzenjahrs', () => {
    // Referenzwert: 33,04 s.
    expect(computeTakt(base).peakTaktSeconds).toBeCloseTo(33.04, 2)
  })

  it('rechnet die mittlere Taktzeit über die Laufzeit', () => {
    // Referenzwert: 66,35 s.
    expect(computeTakt(base).averageTaktSeconds).toBeCloseTo(66.35, 1)
  })

  it('erklärt ausdrücklich, dass kein Verfügbarkeitsfaktor enthalten ist', () => {
    // Ohne diesen Hinweis liest jemand die Auslastung als Endergebnis — sie ist
    // aber eine Untergrenze.
    expect(computeTakt(base).oeeIncluded).toBe(false)
  })

  it('gibt null statt einer Zahl, wenn eine Prämisse fehlt', () => {
    expect(computeTakt({ ...base, hoursPerYear: null }).peakTaktSeconds).toBeNull()
    expect(computeTakt({ ...base, peakVolumePerYear: null }).peakTaktSeconds).toBeNull()
    expect(computeTakt({ ...base, lifetimeYears: 0 }).averageTaktSeconds).toBeNull()
  })
})

describe('computeCapacity', () => {
  const takt = computeTakt({ hoursPerYear: 5400, peakVolumePerYear: 588377, totalVolume: 2636771, lifetimeYears: 9 })
  const stations = classifyRows([
    row(18, 'Unloading Belt conveyor', 28, 1, 0.28),
    row(51, 'Unloading Belt conveyor', 35, 1, 0.35),
    row(116, 'Pre-Assy Lower Brkt', 70, 1, 0.7),
    row(115, 'Guide Pin Assy', null, null, null),
  ])

  it('rechnet Jahreskapazität und Auslastung je Station', () => {
    const c = computeCapacity(stations, takt).find((x) => x.row === 18)!
    // Referenzwerte: 694.286 Stück, 84,7 % Auslastung im Spitzenjahr.
    expect(c.capacityPerYear).toBeCloseTo(694_286, 0)
    expect(c.utilizationPeak).toBeCloseTo(0.847, 3)
  })

  it('lässt Zeilen ohne Stationseigenschaft aus der Kapazitätsrechnung heraus', () => {
    expect(computeCapacity(stations, takt).some((c) => c.name === 'Guide Pin Assy')).toBe(false)
  })

  it('nennt die nötige Zahl paralleler Stationen', () => {
    const caps = computeCapacity(stations, takt)
    // 35 s → 105,9 % → zwei Stationen; 70 s → 211,8 % → drei.
    expect(caps.find((c) => c.row === 51)!.parallelNeededPeak).toBe(2)
    expect(caps.find((c) => c.row === 116)!.parallelNeededPeak).toBe(3)
  })

  it('nennt für eine weit unterausgelastete Station genau eine', () => {
    // Fachliche Aussage, nicht Implementierungsdetail: eine Station mit 3 %
    // Auslastung braucht kein zweites Exemplar — aber sehr wohl ein erstes.
    const caps = computeCapacity(classifyRows([row(1, 'Schnell', 1, 1, 0.1)]), takt)
    expect(caps[0].utilizationPeak).toBeLessThan(0.05)
    expect(caps[0].parallelNeededPeak).toBe(1)
  })

  it('gibt ohne Taktzeit keine Auslastung aus statt sie zu erfinden', () => {
    const ohneTakt = computeTakt({ hoursPerYear: null, peakVolumePerYear: null, totalVolume: null, lifetimeYears: null })
    const caps = computeCapacity(stations, ohneTakt)
    expect(caps[0].utilizationPeak).toBeNull()
    expect(caps[0].parallelNeededPeak).toBeNull()
  })
})

describe('stationsOverPeak', () => {
  const takt = computeTakt({ hoursPerYear: 5400, peakVolumePerYear: 588377, totalVolume: 2636771, lifetimeYears: 9 })

  it('meldet genau die Stationen, die den Spitzenbedarf allein nicht decken', () => {
    const caps = computeCapacity(
      classifyRows([
        row(18, 'A', 28, 1, 0.28), // 84,7 % — reicht
        row(51, 'B', 35, 1, 0.35), // 105,9 % — reicht nicht
        row(116, 'C', 70, 1, 0.7), // 211,8 % — reicht nicht
      ]),
      takt,
    )
    expect(stationsOverPeak(caps).map((c) => c.name)).toEqual(['B', 'C'])
  })

  it('meldet nichts, wenn alle Stationen reichen', () => {
    const caps = computeCapacity(classifyRows([row(1, 'A', 10, 1, 0.1)]), takt)
    expect(stationsOverPeak(caps)).toEqual([])
  })
})
