// Werkzeuge, Zweitwerkzeuge und die Einmalzahlungs-Brücke (Kap. 7.5, R-11).

import { describe, it, expect } from 'vitest'
import { analyseTooling, stripSequenceSuffix, type MappableTool } from '../tooling-analysis'

const tool = (row: number, part: string, oneTimeCost: number | null, extra: Partial<MappableTool> = {}): MappableTool => ({
  row,
  part,
  toolType: 'Spritzgießwerkzeug',
  cavityDesignation: '1-fach',
  lifetimeCycles: 1_000_000,
  oneTimeCost,
  ...extra,
})

describe('stripSequenceSuffix', () => {
  it('erkennt die durchnummerierte Wiederholung', () => {
    expect(stripSequenceSuffix('Halter unten (KF1) 2')).toBe('Halter unten (KF1)')
    expect(stripSequenceSuffix('Halter-2')).toBe('Halter')
    expect(stripSequenceSuffix('Halter (2)')).toBe('Halter')
    expect(stripSequenceSuffix('Halter II')).toBe('Halter')
  })

  it('liest eine Typbezeichnung nicht als Zählung', () => {
    // Ohne die Trenner-Bedingung würde „M12" zu „M" plus Zählung 12.
    expect(stripSequenceSuffix('Ventilkörper M12')).toBeNull()
    expect(stripSequenceSuffix('Kappe NW 20')).toBeNull()
    expect(stripSequenceSuffix('Platte-A')).toBeNull()
  })

  it('gibt null ohne Suffix zurück', () => {
    expect(stripSequenceSuffix('Halter unten')).toBeNull()
    expect(stripSequenceSuffix('2')).toBeNull()
  })
})

describe('analyseTooling — Zuordnung', () => {
  it('paart namensgleiche Positionen und meldet sie unverändert', () => {
    const r = analyseTooling([tool(10, 'Halter', 50_000)], [tool(10, 'Halter', 50_000)])
    expect(r.mappings).toHaveLength(1)
    expect(r.mappings[0]).toMatchObject({ status: 'unchanged', classification: 'unchanged', delta: 0 })
  })

  it('erkennt ein Zweitwerkzeug am Zählsuffix', () => {
    const r = analyseTooling(
      [tool(10, 'Halter', 50_000)],
      [tool(10, 'Halter', 50_000), tool(40, 'Halter 2', 60_000)],
    )
    const zweit = r.mappings.find((m) => m.part === 'Halter 2')!
    expect(zweit.classification).toBe('second_tool')
    expect(zweit.delta).toBe(60_000)
    expect(zweit.evidenceDe).toContain('Zeile 10')
    expect(r.counts.byClassification.second_tool).toBe(1)
  })

  it('erkennt ein zweites Werkzeug gleichen Namens ohne Suffix', () => {
    const r = analyseTooling(
      [tool(10, 'Halter', 50_000)],
      [tool(10, 'Halter', 50_000), tool(41, 'Halter', 70_000)],
    )
    expect(r.mappings.filter((m) => m.classification === 'second_tool')).toHaveLength(1)
  })

  it('verwechselt das verschobene Original nicht mit dem Zweitwerkzeug', () => {
    // Der Fall aus dem Anlassfall: das ursprüngliche Werkzeug rutscht ans
    // Listenende, das Zweitwerkzeug steht davor. Würde der Suffix-Durchgang
    // zuerst laufen, schnappte „Halter 2" sich die Basisposition — und das
    // wirtschaftlich unveränderte Original wäre plötzlich das Zweitwerkzeug.
    const r = analyseTooling(
      [tool(10, 'Halter', 250_000)],
      [tool(40, 'Halter 2', 280_000), tool(55, 'Halter', 250_000)],
    )
    const original = r.mappings.find((m) => m.part === 'Halter')!
    const zweit = r.mappings.find((m) => m.part === 'Halter 2')!

    expect(original.classification).toBe('unchanged')
    expect(original.rowMoved).toBe(true)
    expect(original.delta).toBe(0)
    expect(zweit.classification).toBe('second_tool')
    expect(zweit.delta).toBe(280_000)
  })

  it('nennt eine verschobene Zeile mit ihrer alten Position', () => {
    const r = analyseTooling([tool(10, 'Halter', 50_000)], [tool(55, 'Halter', 50_000)])
    expect(r.mappings[0].rowMoved).toBe(true)
    expect(r.mappings[0].evidenceDe).toContain('Zeile 10')
    expect(r.mappings[0].evidenceDe).toContain('Position 55')
  })

  it('nennt einen Ersatz nicht Zweitwerkzeug', () => {
    // Ist die Basisposition im aktuellen Stand verschwunden, wurde nichts
    // verdoppelt — als „zusätzliches Werkzeug" gemeldet wäre es eine
    // Verhandlungsbehauptung ohne Grundlage.
    const r = analyseTooling([tool(10, 'Halter', 50_000)], [tool(40, 'Halter 2', 60_000)])
    expect(r.mappings.find((m) => m.part === 'Halter 2')!.classification).toBe('new_scope')
    expect(r.mappings.find((m) => m.part === 'Halter')!.status).toBe('removed')
  })

  it('erkennt neuen Umfang ohne Vorgänger', () => {
    const r = analyseTooling([], [tool(40, 'Abdeckung', 60_500)])
    expect(r.mappings[0]).toMatchObject({ classification: 'new_scope', status: 'new', delta: 60_500 })
  })

  it('erkennt eine Preisänderung', () => {
    const r = analyseTooling([tool(10, 'Gehäuse', 200_000)], [tool(10, 'Gehäuse', 249_000)])
    expect(r.mappings[0]).toMatchObject({ classification: 'price_change', status: 'changed', delta: 49_000 })
  })

  it('lässt Summenzeilen des Formblatts aus jeder Zählung heraus', () => {
    // Sie tragen dieselben Beträge noch einmal; mitgezählt verdoppelt sich alles.
    const r = analyseTooling(
      [tool(10, 'Halter', 50_000), tool(11, 'SUMME Baugruppe', 50_000, { isTotalLine: true })],
      [tool(10, 'Halter', 50_000), tool(11, 'SUMME Baugruppe', 50_000, { isTotalLine: true })],
    )
    expect(r.counts).toMatchObject({ toolsAward: 1, toolsCurrent: 1 })
    expect(r.mappings).toHaveLength(1)
  })

  it('hält Vorrichtungen mit Stückumlage aus der Einmalzahlung heraus', () => {
    // Entschieden an der Verrechnung, nicht am Namen: keine Einmalzahlung,
    // sondern eine Umlage je Stück.
    const r = analyseTooling([], [tool(60, 'Werkstückträger', 0, { perPieceCost: 0.75 })], 0, 0)
    expect(r.mappings[0].classification).toBe('fixture')
    expect(r.oneTimeBridge.steps).toEqual([])
    expect(r.oneTimeBridge.passed).toBe(true)
  })
})

describe('analyseTooling — gelöschte Auslegung', () => {
  const ohneSpec = { cavityDesignation: null, lifetimeCycles: null }

  it('meldet eine Position, deren Auslegung verschwunden ist', () => {
    const r = analyseTooling([tool(10, 'Gehäuse', 200_000)], [tool(10, 'Gehäuse', 200_000, ohneSpec)])
    expect(r.mappings[0].specRemovedInCurrent).toBe(true)
    expect(r.counts.specRemoved).toBe(1)
    expect(r.mappings[0].evidenceDe).toContain('nicht mehr gefüllt')
  })

  it('zählt Zweitwerkzeuge ohne Auslegung getrennt', () => {
    // Die eigentliche Frage des Anlassfalls: ein weiteres Werkzeug wird
    // berechnet, aber womit es ausgelegt ist, steht nicht mehr da.
    const r = analyseTooling(
      [tool(10, 'Halter', 250_000)],
      [tool(10, 'Halter', 250_000), tool(40, 'Halter 2', 280_000, ohneSpec)],
    )
    expect(r.counts.secondToolsWithoutSpec).toBe(1)
    expect(r.mappings.find((m) => m.part === 'Halter 2')!.evidenceDe).toContain('Kavitäten')
  })

  it('meldet nichts, wenn die Auslegung schon vorher fehlte', () => {
    const r = analyseTooling([tool(10, 'Gehäuse', 200_000, ohneSpec)], [tool(10, 'Gehäuse', 200_000, ohneSpec)])
    expect(r.counts.specRemoved).toBe(0)
  })

  it('lässt eine der beiden Angaben genügen', () => {
    const nurStandzeit = { cavityDesignation: null, lifetimeCycles: 500_000 }
    const r = analyseTooling([tool(10, 'Gehäuse', 1, ohneSpec)], [tool(10, 'Gehäuse', 1, nurStandzeit)])
    expect(r.mappings[0].specRemovedInCurrent).toBe(false)
  })
})

describe('analyseTooling — Einmalzahlungs-Brücke', () => {
  const award = [tool(10, 'Halter', 250_000), tool(11, 'Gehäuse', 200_000)]
  const current = [
    tool(10, 'Halter', 250_000),
    tool(11, 'Gehäuse', 249_000),
    tool(40, 'Halter 2', 280_000),
    tool(41, 'Abdeckung', 60_500),
  ]

  it('schliesst die Brücke ohne Restposten', () => {
    const r = analyseTooling(award, current, 450_000, 839_500)
    expect(r.oneTimeBridge.delta).toBe(389_500)
    expect(r.oneTimeBridge.residual).toBeCloseTo(0, 6)
    expect(r.oneTimeBridge.passed).toBe(true)
  })

  it('führt jeden Betrag auf seine Ursache zurück', () => {
    const r = analyseTooling(award, current, 450_000, 839_500)
    const nach = Object.fromEntries(r.oneTimeBridge.steps.map((s) => [s.classification, s.delta]))
    expect(nach).toEqual({ new_scope: 60_500, second_tool: 280_000, price_change: 49_000 })
  })

  it('nennt die Zeilen zu jedem Schritt', () => {
    const r = analyseTooling(award, current, 450_000, 839_500)
    expect(r.oneTimeBridge.steps.find((s) => s.classification === 'second_tool')!.toolRows).toEqual([40])
  })

  it('meldet einen Restposten, statt ihn zu verstecken', () => {
    // Die ausgewiesene Summe passt nicht zu den Positionen — das ist ein Befund
    // und kein Anlass, die Differenz stillschweigend unterzubringen.
    const r = analyseTooling(award, current, 450_000, 850_000)
    expect(r.oneTimeBridge.residual).toBeCloseTo(10_500, 6)
    expect(r.oneTimeBridge.passed).toBe(false)
  })

  it('sagt „nicht geprüft" statt „bestanden", wenn keine Ausweissummen vorliegen', () => {
    const r = analyseTooling(award, current)
    expect(r.oneTimeBridge.passed).toBeNull()
    expect(r.oneTimeBridge.residual).toBeNull()
    expect(r.oneTimeBridge.steps.length).toBeGreaterThan(0)
  })

  it('lässt unveränderte Positionen aus der Brücke heraus', () => {
    const r = analyseTooling(award, current, 450_000, 839_500)
    expect(r.oneTimeBridge.steps.map((s) => s.classification)).not.toContain('unchanged')
  })
})
