import { describe, it, expect } from 'vitest'
import {
  buildSupplierBenchmark,
  type BenchmarkSideInput,
  type BenchmarkVariantInput,
} from '../supplier-benchmark'
import type { MoneyValue } from '../types'

// Die Fixtures unten sind aus dem Anlassfall (KAR-993, zwei reale
// Lieferanten-Multi-QAFs) abgeleitet: die VERHÄLTNISSE entsprechen den dort
// gemessenen, die Absolutwerte sind gerundet und verschoben, damit keine
// echten Lieferanten-Kalkulationswerte im Repo liegen. Dieselbe Trennung, die
// SYNTHETIC_IDENTITY_FIELDS für Sachnummern etabliert hat. Der Abgleich gegen
// die Originaldateien läuft außerhalb der Suite (operator-lokal), weil die
// Dateien vertraulich sind und nicht committet werden.
//
// Seite A entspricht dem Fall "Angebot mit sehr kleinem Aufschlag auf den
// Basispreis", Seite B dem Fall "Angebot mit großem Aufschlag über eine
// Rohstoff-Position" — der Befund, der den Benchmark überhaupt wertvoll macht.

const eur = (value: number): MoneyValue => ({ value, currency: 'EUR' })
const usd = (value: number): MoneyValue => ({ value, currency: 'USD' })

function variant(id: string, o: Partial<BenchmarkVariantInput> = {}): BenchmarkVariantInput {
  return {
    variantId: id,
    materialCosts: null,
    manufacturingCosts: null,
    totalProductionCosts: null,
    scrap: null,
    otherSurcharges: null,
    setupCostAllocation: null,
    toolingAndFixtureCost: null,
    offerBasePrice: null,
    offerPrice: null,
    ...o,
  }
}

/** Seite A: Material 240 / Fertigung 44 / HK 284, Basispreis 340, Preis 342. */
const sideA: BenchmarkSideInput = {
  label: 'Seite A',
  variants: [
    variant('a1', {
      materialCosts: usd(240),
      manufacturingCosts: usd(44),
      totalProductionCosts: usd(284),
      scrap: usd(4.8),
      offerBasePrice: usd(340),
      offerPrice: usd(342),
    }),
    variant('a2', {
      materialCosts: usd(280),
      manufacturingCosts: usd(42),
      totalProductionCosts: usd(322),
      scrap: usd(5.5),
      offerBasePrice: usd(388),
      offerPrice: usd(390),
    }),
  ],
}

/** Seite B: Material 180 / Fertigung 29 / HK 209, Basispreis 258, Preis 335 —
 * der große Aufschlag zwischen Basispreis und Angebotspreis. */
const sideB: BenchmarkSideInput = {
  label: 'Seite B',
  variants: [
    variant('b1', {
      materialCosts: eur(180),
      manufacturingCosts: eur(29),
      totalProductionCosts: eur(209),
      scrap: eur(2.7),
      offerBasePrice: eur(258),
      offerPrice: eur(335),
    }),
    variant('b2', {
      materialCosts: eur(195),
      manufacturingCosts: eur(29),
      totalProductionCosts: eur(224),
      scrap: eur(2.9),
      offerBasePrice: eur(277),
      offerPrice: eur(359),
    }),
  ],
}

describe('Kostenstruktur (Block A)', () => {
  it('rechnet den Materialanteil an den Herstellkosten je Seite', () => {
    const r = buildSupplierBenchmark(sideA, sideB)
    // A: 240/284 = 0.8451, 280/322 = 0.8696 → Median (n=2) = Mittel der beiden
    expect(r.left.costStructure.materialShareOfProduction.median).toBeCloseTo(0.8573, 4)
    expect(r.left.costStructure.materialShareOfProduction.min).toBeCloseTo(0.8451, 4)
    expect(r.left.costStructure.materialShareOfProduction.max).toBeCloseTo(0.8696, 4)
    // B: 180/209 = 0.8612, 195/224 = 0.8705
    expect(r.right.costStructure.materialShareOfProduction.median).toBeCloseTo(0.8659, 4)
  })

  it('rechnet den Fertigungsanteil unabhängig vom Materialanteil', () => {
    const r = buildSupplierBenchmark(sideA, sideB)
    // A: 44/284 = 0.1549, 42/322 = 0.1304
    expect(r.left.costStructure.manufacturingShareOfProduction.median).toBeCloseTo(0.1427, 4)
    // B: 29/209 = 0.1388, 29/224 = 0.1295
    expect(r.right.costStructure.manufacturingShareOfProduction.median).toBeCloseTo(0.1341, 4)
  })

  it('nutzt den Median, nicht den Mittelwert — Ausreißer verschieben ihn nicht', () => {
    // Drei Varianten mit einem starken Ausreißer. Bei n=2 wären Median und
    // Mittelwert identisch, der Test könnte die Eigenschaft also nicht
    // beweisen — deshalb ausdrücklich n=3 mit auseinanderliegenden Werten:
    //   Anteile 0.5, 0.6, 0.9  →  Median 0.6, Mittelwert 0.6667
    const withOutlier: BenchmarkSideInput = {
      label: 'mit Ausreißer',
      variants: [
        variant('o1', { materialCosts: eur(50), totalProductionCosts: eur(100) }),
        variant('o2', { materialCosts: eur(60), totalProductionCosts: eur(100) }),
        variant('o3', { materialCosts: eur(90), totalProductionCosts: eur(100) }),
      ],
    }
    const r = buildSupplierBenchmark(withOutlier, sideB)
    const m = r.left.costStructure.materialShareOfProduction
    expect(m.median).toBeCloseTo(0.6, 6)
    // Der Mittelwert läge bei 0.6667 — wäre die Implementierung ein
    // Mittelwert, bricht genau diese Zeile.
    expect(m.median).not.toBeCloseTo(0.6667, 3)
    expect(m.min).toBeCloseTo(0.5, 6)
    expect(m.max).toBeCloseTo(0.9, 6)
  })

  it('meldet die Spanne über die Varianten, nicht nur einen Punktwert', () => {
    const r = buildSupplierBenchmark(sideA, sideB)
    const m = r.left.costStructure.materialShareOfProduction
    expect(m.contributingVariants).toBe(2)
    expect(m.skippedVariants).toBe(0)
    expect(m.max! - m.min!).toBeGreaterThan(0)
  })
})

describe('Aufschlag-Kette (Block C)', () => {
  it('macht den Unterschied zwischen kleinem und großem Aufschlag sichtbar', () => {
    const r = buildSupplierBenchmark(sideA, sideB)
    // A: (342-340)/340 = 0.0059, (390-388)/388 = 0.0052
    expect(r.left.markupChain.offerBaseToOfferPrice.median).toBeCloseTo(0.0055, 4)
    // B: (335-258)/258 = 0.2984, (359-277)/277 = 0.2960
    expect(r.right.markupChain.offerBaseToOfferPrice.median).toBeCloseTo(0.2972, 4)
    // Das ist der Kern des Benchmarks: derselbe Endpreis, andere Kostenbasis.
    expect(r.right.markupChain.offerBaseToOfferPrice.median!).toBeGreaterThan(
      r.left.markupChain.offerBaseToOfferPrice.median! * 10
    )
  })

  it('rechnet den Gesamtaufschlag auf die Herstellkosten', () => {
    const r = buildSupplierBenchmark(sideA, sideB)
    // A: (342-284)/284 = 0.2042, (390-322)/322 = 0.2112
    expect(r.left.markupChain.productionToOfferPrice.median).toBeCloseTo(0.2077, 4)
    // B: (335-209)/209 = 0.6029, (359-224)/224 = 0.6027
    expect(r.right.markupChain.productionToOfferPrice.median).toBeCloseTo(0.6028, 4)
  })
})

describe('Währungsgrenze — die zentrale Ehrlichkeitsregel', () => {
  it('bildet KEIN Verhältnis über zwei Währungen und begründet das', () => {
    const mixed: BenchmarkSideInput = {
      label: 'gemischt',
      variants: [
        variant('m1', {
          materialCosts: usd(240), // Material in USD
          totalProductionCosts: eur(284), // Herstellkosten in EUR
        }),
      ],
    }
    const r = buildSupplierBenchmark(mixed, sideB)
    const m = r.left.costStructure.materialShareOfProduction
    expect(m.median).toBeNull()
    expect(m.unavailableReason).toBe('mixed_currency')
    expect(m.skippedVariants).toBe(1)
  })

  it('rechnet, wenn BEIDE Währungscodes fehlen — dieselbe Variante hat strukturell eine Währung', () => {
    // Bewusste Vertragsänderung vom 30.07.2026. Zunächst galt fail-closed
    // (ein `null`-Code = Konflikt). An Realdaten blockierte das den Benchmark
    // vollständig: der Multi-QAF-Container persistiert `currency: null` auf
    // fast allen Summary-Positionen, obwohl die Währung in der Datei steht.
    //
    // Begründung der Lockerung: Zähler und Nenner stammen immer aus DERSELBEN
    // Variante, und eine Variante ist die Spalte EINES Angebots mit genau
    // einer Angebotswährung. Ein fehlender Name ist deshalb kein Widerspruch,
    // sondern eine nicht ausgelesene Beschriftung — das Verhältnis zweier
    // Beträge derselben, unbenannten Einheit ist korrekt.
    const noCurrency: BenchmarkSideInput = {
      label: 'ohne Währungscode',
      variants: [
        variant('u1', {
          materialCosts: { value: 240, currency: null },
          totalProductionCosts: { value: 284, currency: null },
        }),
      ],
    }
    const r = buildSupplierBenchmark(noCurrency, sideB)
    const m = r.left.costStructure.materialShareOfProduction
    expect(m.median).toBeCloseTo(240 / 284, 6)
    expect(m.unavailableReason).toBeNull()
  })

  it('rechnet, wenn EIN Code bekannt und der andere nur nicht ausgelesen ist', () => {
    // Die Summary-Positionen bilden eine Summenkette (Material + Fertigung =
    // Herstellkosten, ... = Angebotspreis) — eine Kette, die addiert wird,
    // steht zwingend in einer Währung. Ein fehlender Name widerspricht einem
    // vorhandenen deshalb nicht. Genau dieser Fall trat an Realdaten auf:
    // materialCosts trug EUR, der Angebotspreis derselben Variante nicht.
    const halfKnown: BenchmarkSideInput = {
      label: 'halb bekannt',
      variants: [
        variant('h1', {
          materialCosts: eur(240),
          totalProductionCosts: { value: 284, currency: null },
        }),
      ],
    }
    const r = buildSupplierBenchmark(halfKnown, sideB)
    const m = r.left.costStructure.materialShareOfProduction
    expect(m.median).toBeCloseTo(240 / 284, 6)
    expect(m.unavailableReason).toBeNull()
  })

  it('verweigert bei zwei verschiedenen BEKANNTEN Währungen', () => {
    const twoKnown: BenchmarkSideInput = {
      label: 'zwei bekannte',
      variants: [variant('t1', { materialCosts: usd(240), totalProductionCosts: eur(284) })],
    }
    const r = buildSupplierBenchmark(twoKnown, sideB)
    expect(r.left.costStructure.materialShareOfProduction.unavailableReason).toBe('mixed_currency')
  })

  it('bildet den Aufschlag auch ohne Währungscodes als DIFFERENZ, nicht als Quote', () => {
    // Regression zum Fehler vom 30.07.: nach der Lockerung in ratioOf reichte
    // markupOf bei `currency: null` weiter in den Konflikt-Pfad — der übergibt
    // den Zielwert OHNE Differenz, wodurch 0,5 % Aufschlag als 100,5 %
    // erschienen. Beide Stellen müssen dieselbe Währungsregel benutzen.
    const noCurrency: BenchmarkSideInput = {
      label: 'ohne Code',
      variants: [
        variant('n1', {
          offerBasePrice: { value: 332.35, currency: null },
          offerPrice: { value: 334.02, currency: null },
        }),
      ],
    }
    const r = buildSupplierBenchmark(noCurrency, sideB)
    const markup = r.left.markupChain.offerBaseToOfferPrice
    // (334.02 - 332.35) / 332.35 = 0.005025…  — NICHT 1.005…
    expect(markup.median).toBeCloseTo(0.005025, 5)
    expect(markup.median).toBeLessThan(0.5)
  })

  it('weist auf verschiedene Währungen der beiden Seiten hin', () => {
    const r = buildSupplierBenchmark(sideA, sideB)
    const n = r.notices.find((x) => x.kind === 'currencies_differ_between_sides')
    expect(n).toBeDefined()
    expect(n!.messageDe).toContain('USD')
    expect(n!.messageDe).toContain('EUR')
  })

  it('meldet mehrere Währungen INNERHALB einer Seite (Fertigung an zwei Standorten)', () => {
    const twoSites: BenchmarkSideInput = {
      label: 'zwei Standorte',
      variants: [
        variant('s1', { materialCosts: eur(180), totalProductionCosts: eur(209) }),
        variant('s2', { materialCosts: usd(240), totalProductionCosts: usd(284) }),
      ],
    }
    const r = buildSupplierBenchmark(twoSites, sideB)
    expect(r.left.currencies).toEqual(['EUR', 'USD'])
    expect(r.notices.some((x) => x.kind === 'multiple_currencies_within_side')).toBe(true)
    // Beide Varianten sind je FÜR SICH konsistent, also ist die Kennzahl
    // ermittelbar — die Währungsregel greift pro Variante, nicht pro Seite.
    expect(r.left.costStructure.materialShareOfProduction.contributingVariants).toBe(2)
  })
})

describe('Nicht ermittelbar wird begründet, nicht auf 0 gesetzt', () => {
  it('unterscheidet fehlende Werte von Währungskonflikt', () => {
    const noValues: BenchmarkSideInput = {
      label: 'ohne Werte',
      variants: [variant('n1', { totalProductionCosts: eur(209) })],
    }
    const r = buildSupplierBenchmark(noValues, sideB)
    const m = r.left.costStructure.materialShareOfProduction
    expect(m.median).toBeNull()
    expect(m.unavailableReason).toBe('missing_values')
  })

  it('erkennt einen Nenner von 0 als eigenen Grund', () => {
    const zero: BenchmarkSideInput = {
      label: 'null-Nenner',
      variants: [variant('z1', { materialCosts: eur(180), totalProductionCosts: eur(0) })],
    }
    const r = buildSupplierBenchmark(zero, sideB)
    expect(r.left.costStructure.materialShareOfProduction.unavailableReason).toBe('zero_denominator')
  })

  it('meldet eine Seite ohne Varianten als no_variants', () => {
    const r = buildSupplierBenchmark({ label: 'leer', variants: [] }, sideB)
    expect(r.left.variantCount).toBe(0)
    expect(r.left.costStructure.materialShareOfProduction.unavailableReason).toBe('no_variants')
  })

  it('macht eine einseitig verfügbare Kennzahl explizit sichtbar', () => {
    // Bewusst so gebaut, dass NUR die Rüstkosten-Kennzahl einseitig ist:
    // materialCosts ist auf beiden Seiten vorhanden, damit der Test nicht
    // versehentlich über eine andere einseitige Kennzahl grün wird.
    const onlyLeft: BenchmarkSideInput = {
      label: 'nur links',
      variants: [
        variant('o1', {
          materialCosts: eur(180),
          manufacturingCosts: eur(29),
          setupCostAllocation: eur(5),
          totalProductionCosts: eur(209),
          scrap: eur(2.7),
          offerBasePrice: eur(258),
          offerPrice: eur(335),
        }),
      ],
    }
    const r = buildSupplierBenchmark(onlyLeft, sideB)
    // sideB trägt keine setupCostAllocation → genau diese Kennzahl ist einseitig
    expect(r.left.costStructure.setupShareOfProduction.median).not.toBeNull()
    expect(r.right.costStructure.setupShareOfProduction.median).toBeNull()
    const oneSided = r.notices.filter((x) => x.kind === 'metric_only_on_one_side')
    // Die Rüstkosten-Umlage ist auf zwei Kennzahlen bezogen (Angebotspreis und
    // Herstellkosten), beide sind hier einseitig — deshalb zwei Hinweise.
    expect(oneSided.length).toBeGreaterThanOrEqual(1)
    expect(oneSided.some((n) => n.messageDe.includes('Rüstkostenanteil'))).toBe(true)
  })
})

describe('Kein Varianten-Matching (die Design-Eigenschaft)', () => {
  it('vergleicht Seiten mit ungleicher Variantenzahl ohne Zuordnung', () => {
    const many: BenchmarkSideInput = {
      label: 'zehn Varianten',
      variants: Array.from({ length: 10 }, (_, i) =>
        variant(`v${i}`, {
          materialCosts: eur(180 + i),
          totalProductionCosts: eur(209 + i),
        })
      ),
    }
    const one: BenchmarkSideInput = {
      label: 'eine Variante',
      variants: [variant('single', { materialCosts: eur(180), totalProductionCosts: eur(209) })],
    }
    const r = buildSupplierBenchmark(many, one)
    // Beide Seiten liefern eine Kennzahl — 10 gegen 1 ist kein Hindernis,
    // weil nichts zugeordnet wird.
    expect(r.left.costStructure.materialShareOfProduction.contributingVariants).toBe(10)
    expect(r.right.costStructure.materialShareOfProduction.contributingVariants).toBe(1)
    expect(r.left.costStructure.materialShareOfProduction.median).not.toBeNull()
    expect(r.right.costStructure.materialShareOfProduction.median).not.toBeNull()
  })
})
