// Registry-Vertrag für den QAF-Preisblock (V2-Spezifikation R-05/R-07/R-22).
//
// Die Fixture-Werte sind aus dem Golden Case abgeleitet, aber verschoben —
// die Additivitätsstruktur ist real, die Absolutwerte sind es nicht. Echte
// Lieferanten-Kalkulationen gehören nicht ins Repo.
//
// Jede additive Zeile trägt bewusst einen Wert ungleich 0: fällt eine aus der
// Registry heraus oder wird eine Memo-Zeile fälschlich additiv, ändert sich
// die Summe messbar. Eine Registry mit Nullwerten würde beides verschlucken.

import { describe, it, expect } from 'vitest'
import { SUMMARY_ROW_REGISTRY, additiveRowSpecs, roleOfMetric, registrySpecFor } from '../summary-row-registry'
import { SUMMARY_METRIC_KEYS, type SummaryMetricKey } from '../summary-metrics'

/**
 * Preisblock eines Vergabestands, Struktur wie im QAF-Template:
 * ANGEBOTSBASISPREIS = 1.+2.+3.+4.+5., ANGEBOTSPREIS = Summe 1. bis 8.
 * Die beiden "Enthaltene …"-Zeilen sind Memo-Unterzeilen von Block 1+2 und
 * dürfen in keiner Summe auftauchen (Befund F-01).
 */
const BLOCK: Partial<Record<SummaryMetricKey, number>> = {
  materialCosts: 300.0, // 1.
  manufacturingCosts: 10.0, // 2.
  devicesAndTools: 2.0, // 3.
  scrapMaterial: 0.2, // 4. (obere Blattzeile)
  scrapManufacturing: 0.05, // 4. (untere Blattzeile, ohne eigenes Label)
  otherSurcharges: 40.0, // 5.
  rawMaterialPriceShareMaterial: 0.3, // 6. (obere Blattzeile)
  rawMaterialPriceShareEnergy: 0.2, // 6. (untere Blattzeile, nur V9-Template)
  customsSupplierToBMW: 0.8, // 7. — im Blatt "JIS"; der Key ist historisch, siehe Registry-Notiz
  transportSupplierToBMW: 1.5, // 8.
  // Memo — nicht additiv:
  packagingTransportIncluded: 0.54,
  customsIncluded: 0.12,
}

const EXPECTED_QUOTATION_PRICE = 355.05

describe('SUMMARY_ROW_REGISTRY', () => {
  it('deckt jede Metrik des Summary-Parsers ab', () => {
    const covered = new Set(SUMMARY_ROW_REGISTRY.map((s) => s.metricKey))
    const missing = SUMMARY_METRIC_KEYS.filter((k) => !covered.has(k))
    expect(missing).toEqual([])
  })

  it('vergibt jeden row_key genau einmal', () => {
    const keys = SUMMARY_ROW_REGISTRY.map((s) => s.rowKey)
    expect(new Set(keys).size).toBe(keys.length)
  })

  it('trägt Blocknummern nur auf additiven Zeilen und deckt 1 bis 8 ab', () => {
    const byBlock = new Map<string, string[]>()
    for (const spec of SUMMARY_ROW_REGISTRY) {
      if (spec.blockNo === null) continue
      expect(spec.role, `Block ${spec.blockNo} (${spec.rowKey}) muss additiv sein`).toBe('additive')
      byBlock.set(spec.blockNo, [...(byBlock.get(spec.blockNo) ?? []), spec.rowKey])
    }
    expect([...byBlock.keys()].sort()).toEqual(['1', '2', '3', '4', '5', '6', '7', '8'])
    // Ausschuss und Rohstoff-Preisanteil stehen je auf zwei Blattzeilen.
    expect(byBlock.get('4')).toHaveLength(2)
    expect(byBlock.get('6')).toHaveLength(2)
  })

  it('summiert die additiven Zeilen exakt zum Angebotspreis', () => {
    const sum = additiveRowSpecs().reduce((acc, spec) => acc + (BLOCK[spec.metricKey] ?? 0), 0)
    expect(sum).toBeCloseTo(EXPECTED_QUOTATION_PRICE, 6)
  })

  it('hält die Memo-Zeilen aus der Summe heraus (F-01)', () => {
    expect(roleOfMetric('packagingTransportIncluded')).toBe('memo')
    expect(roleOfMetric('customsIncluded')).toBe('memo')
    const additiveKeys = additiveRowSpecs().map((s) => s.metricKey)
    expect(additiveKeys).not.toContain('packagingTransportIncluded')
    expect(additiveKeys).not.toContain('customsIncluded')
  })

  it('kennzeichnet Zwischensummen und die Endsumme als nicht additiv', () => {
    expect(roleOfMetric('totalProductionCosts')).toBe('subtotal')
    expect(roleOfMetric('totalCosts')).toBe('subtotal')
    expect(roleOfMetric('quotationBasePrice')).toBe('subtotal')
    expect(roleOfMetric('quotationPrice')).toBe('total')
  })

  it('markiert die labellose Ausschusszeile als positionsbestimmt', () => {
    // scrapManufacturing steht im Blatt unter scrapMaterial und trägt kein
    // eigenes Label in Spalte G — eine Label-Verifikation ist dort strukturell
    // unmöglich und darf deshalb kein DQ-Finding auslösen.
    expect(registrySpecFor('scrapManufacturing')?.labelSource).toBe('positional')
    expect(registrySpecFor('materialCosts')?.labelSource).toBe('label')
  })

  it('führt die Einmalzahlungen als eigene Rollen, nicht im Preisblock', () => {
    expect(roleOfMetric('oneTimeDevelopment')).toBe('one_time')
    expect(roleOfMetric('oneTimeTools')).toBe('one_time')
    expect(roleOfMetric('totalOneTimePayment')).toBe('one_time_total')
    const additiveKeys = additiveRowSpecs().map((s) => s.metricKey)
    expect(additiveKeys).not.toContain('oneTimeTools')
  })

  it('gibt die Zeilen in Blattreihenfolge aus', () => {
    const order = SUMMARY_ROW_REGISTRY.map((s) => s.metricKey)
    expect(order.indexOf('materialCosts')).toBeLessThan(order.indexOf('manufacturingCosts'))
    expect(order.indexOf('manufacturingCosts')).toBeLessThan(order.indexOf('totalProductionCosts'))
    expect(order.indexOf('quotationBasePrice')).toBeLessThan(order.indexOf('quotationPrice'))
    expect(order.indexOf('quotationPrice')).toBeLessThan(order.indexOf('oneTimeTools'))
  })
})
