// Stationsmapping und Parameteridentität (Kap. 7.4, R-10/R-16).

import { describe, it, expect } from 'vitest'
import { mapStations, parametersIdentical, type MappableStation } from '../manufacturing-mapping'

const st = (
  row: number,
  name: string,
  cycleSeconds: number | null,
  costPerPiece: number | null,
  over: Partial<MappableStation> = {},
): MappableStation => ({
  row,
  name,
  cycleSeconds,
  workers: 1,
  machineRatePerHour: 9.87,
  overheadRatePerHour: 8.6583,
  costPerPiece,
  ...over,
})

describe('parametersIdentical', () => {
  it('erkennt gleiche Parameter innerhalb der Toleranz', () => {
    expect(parametersIdentical(st(1, 'A', 28, 0.28), st(2, 'B', 28.005, 0.28))).toBe(true)
  })

  it('erkennt eine geänderte Zykluszeit', () => {
    expect(parametersIdentical(st(1, 'A', 28, 0.28), st(2, 'A', 35, 0.35))).toBe(false)
  })

  it('erkennt geänderte Personalzahl und Sätze', () => {
    expect(parametersIdentical(st(1, 'A', 28, 0.28), st(2, 'A', 28, 0.28, { workers: 2 }))).toBe(false)
    expect(parametersIdentical(st(1, 'A', 28, 0.28), st(2, 'A', 28, 0.28, { machineRatePerHour: 12 }))).toBe(false)
  })

  it('wertet beidseitig fehlende Angaben als gleich', () => {
    // Zwei Stationen ohne Gemeinkostensatz unterscheiden sich darin nicht.
    const a = st(1, 'A', 28, 0.28, { overheadRatePerHour: null })
    const b = st(2, 'A', 28, 0.28, { overheadRatePerHour: null })
    expect(parametersIdentical(a, b)).toBe(true)
  })

  it('wertet eine einseitig fehlende Angabe als Unterschied', () => {
    // Dort ist eine Angabe verschwunden — das ist eine Änderung, kein Gleichstand.
    const a = st(1, 'A', 28, 0.28, { overheadRatePerHour: 8.66 })
    const b = st(2, 'A', 28, 0.28, { overheadRatePerHour: null })
    expect(parametersIdentical(a, b)).toBe(false)
  })

  it('trennt die Toleranzen von Zeiten und Sätzen', () => {
    // 0,004 EUR am Stundensatz sind Rundung, 0,004 s an der Zykluszeit auch —
    // aber 0,05 s wären es nicht mehr.
    expect(parametersIdentical(st(1, 'A', 28, 0.28), st(2, 'A', 28, 0.28, { machineRatePerHour: 9.874 }))).toBe(true)
    expect(parametersIdentical(st(1, 'A', 28, 0.28), st(2, 'A', 28.05, 0.28))).toBe(false)
  })
})

describe('mapStations — Zuordnung', () => {
  it('ordnet gleichnamige Stationen zu und hält die Parametergleichheit fest', () => {
    const r = mapStations([st(18, 'Unloading Belt conveyor', 28, 0.2846)], [st(18, 'Unloading Belt conveyor', 28, 0.2846)])
    expect(r.mappings[0].matchType).toBe('exact')
    expect(r.mappings[0].parametersIdentical).toBe(true)
  })

  it('meldet eine gleichnamige Station mit anderen Parametern als verändert', () => {
    // Gleicher Name heisst nicht gleiche Station — hier wurde die Taktzeit
    // erhöht, und das darf nicht als „unverändert" durchgehen.
    const r = mapStations([st(1, 'SMT', 12, 0.12)], [st(1, 'SMT', 28, 0.28)])
    expect(r.mappings[0].matchType).toBe('exact')
    expect(r.mappings[0].parametersIdentical).toBe(false)
    expect(r.mappings[0].matchEvidence.detailsDe).toContain('verändert')
  })

  it('erkennt eine Umbenennung an identischen Parametern', () => {
    // Der Name ist das schwächste Merkmal einer Station.
    const r = mapStations([st(1, 'Alte Bezeichnung', 28, 0.28)], [st(1, 'Neue Bezeichnung', 28, 0.28)])
    expect(r.mappings[0].matchType).toBe('renamed')
    expect(r.mappings[0].matchEvidence.ruleId).toBe('parameters_identical')
  })

  it('führt neue und entfallene Stationen getrennt', () => {
    const r = mapStations([st(1, 'Alt', 28, 0.28)], [st(1, 'Neu', 35, 0.35)])
    expect(r.counts.byMatchType.added).toBe(1)
    expect(r.counts.byMatchType.removed).toBe(1)
    expect(r.counts.byMatchType.renamed).toBe(0)
  })

  it('ordnet jede Station genau einmal zu', () => {
    const award = [st(1, 'A', 28, 0.28), st(2, 'B', 35, 0.35)]
    const current = [st(1, 'A', 28, 0.28), st(2, 'C', 40, 0.4)]
    const r = mapStations(award, current)

    const awardRows = r.mappings.map((m) => m.awardRow).filter((x): x is number => x !== null)
    const currentRows = r.mappings.map((m) => m.currentRow).filter((x): x is number => x !== null)
    expect(awardRows.sort()).toEqual([1, 2])
    expect(currentRows.sort()).toEqual([1, 2])
  })

  it('vergibt bei gedrehter Eingabereihenfolge dieselben Zuordnungs-IDs', () => {
    // Mehrere Treffer, damit die Vergabereihenfolge überhaupt beobachtbar ist.
    const award = [st(3, 'C', 30, 0.3), st(1, 'A', 10, 0.1), st(2, 'B', 20, 0.2)]
    const current = [st(2, 'B', 20, 0.25), st(1, 'A', 10, 0.15), st(3, 'C', 30, 0.35)]
    const ids = (a: MappableStation[], c: MappableStation[]) =>
      mapStations(a, c).mappings.map((m) => `${m.mappingId}:${m.awardName}`)

    expect(ids([...award].reverse(), [...current].reverse())).toEqual(ids(award, current))
  })
})

describe('mapStations — „Hauptlinie unverändert" belegen statt behaupten', () => {
  it('bestätigt die Aussage, wenn alle Paare parameteridentisch sind', () => {
    const r = mapStations(
      [st(1, 'A', 28, 0.28), st(2, 'B', 35, 0.35)],
      [st(1, 'A', 28, 0.28), st(2, 'B', 35, 0.35), st(3, 'Neu', 20, 0.2)],
    )
    expect(r.mainLineUnchanged).toBe(true)
    expect(r.counts.byMatchType.added).toBe(1)
  })

  it('widerlegt die Aussage, sobald ein Paar abweicht', () => {
    const r = mapStations([st(1, 'A', 28, 0.28), st(2, 'B', 35, 0.35)], [st(1, 'A', 28, 0.28), st(2, 'B', 40, 0.4)])
    expect(r.mainLineUnchanged).toBe(false)
  })

  it('behauptet nichts, wenn es gar kein Paar gibt', () => {
    // Ohne Paar ist „die Hauptlinie ist unverändert" nicht wahr, sondern
    // gegenstandslos.
    const r = mapStations([], [st(1, 'Neu', 20, 0.2)])
    expect(r.mainLineUnchanged).toBe(false)
  })
})

describe('mapStations — Rekonsiliation gegen die Blattzeile', () => {
  const award = [st(1, 'A', 28, 1.0), st(2, 'B', 35, 2.0)]
  const current = [st(1, 'A', 28, 1.0), st(2, 'B', 35, 2.0), st(3, 'Neu', 20, 0.5)]

  it('weist aus, welcher Anteil auf neue Stationen entfällt', () => {
    // Die Kernaussage des Referenzfalls: die neuen Stationen erklären das
    // gesamte Delta.
    const r = mapStations(award, current, 0.5)
    expect(r.reconciliation.newStationsSum).toBeCloseTo(0.5, 6)
    expect(r.reconciliation.sumStationDeltas).toBeCloseTo(0.5, 6)
    expect(r.reconciliation.passed).toBe(true)
  })

  it('meldet einen Widerspruch zur Blattzeile', () => {
    const r = mapStations(award, current, 1.2)
    expect(r.reconciliation.residual).toBeCloseTo(0.7, 6)
    expect(r.reconciliation.passed).toBe(false)
  })

  it('sagt „nicht geprüft" statt „bestanden", wenn die Blattzeile fehlt', () => {
    const r = mapStations(award, current, null)
    expect(r.reconciliation.passed).toBeNull()
    expect(r.reconciliation.residual).toBeNull()
  })
})
