// Die Übersicht aus persistierten Zeilen (View-Phase V1, zweiter Eingang).
//
// Der Weg, den die Seite `/qaf-differences/[id]/v2` benutzt: Die Originaldateien
// sind nach dem Ingest nicht mehr adressierbar (`qaf_file` führt keinen
// Storage-Pfad), also baut `summaryMetricsFromRows` den Preisblock aus den
// gespeicherten Werten zurück. Geprüft wird hier, dass aus diesem Rückbau
// derselbe Lauf entsteht wie aus einer Mappe — und dass die Grenzen des
// Rückbaus sichtbar bleiben.
//
// FIXTURE-DATEN-REGEL: jede Zahl und jeder Zellbezug unten ist freie Erfindung.

import { describe, it, expect } from 'vitest'
import { buildOverviewRun } from '../overview-run'
import { summaryMetricsFromRows } from '../rehydrate'

/** Zeilen, wie sie in `qaf_summary_metric` stehen. */
const zeilen = (werte: Record<string, number | null>) =>
  Object.entries(werte).map(([metric_key, value]) => ({
    metric_key,
    value,
    currency: 'EUR',
    source_cell: 'D42',
  }))

// Die additiven Zeilen summieren sich auf den Angebotspreis: 2,00 + 1,00 +
// 0,50 − 0,20 = 3,30 gegen 2,50 + 1,40 + 0,50 = 4,40. Differenz 1,10.
const VERGABE = zeilen({
  materialCosts: 2.0,
  manufacturingCosts: 1.0,
  otherSurcharges: 0.5,
  scrapMaterial: -0.2,
  quotationPrice: 3.3,
})

const AKTUELL = zeilen({
  materialCosts: 2.5,
  manufacturingCosts: 1.4,
  otherSurcharges: 0.5,
  scrapMaterial: null, // entfallen
  quotationPrice: 4.4,
})

function lauf(alt = VERGABE, neu = AKTUELL) {
  return buildOverviewRun({
    award: summaryMetricsFromRows(alt, null),
    current: summaryMetricsFromRows(neu, null),
    awardFileNameDe: 'vergabe.xlsx',
    currentFileNameDe: 'aktuell.xlsx',
  })
}

describe('Übersicht aus persistierten Zeilen', () => {
  it('baut eine Brücke, die schliesst', () => {
    const run = lauf()

    expect(run.degradationsDe).toEqual([])
    expect(run.waterfall.state).toBe('ok')
  })

  it('führt den entfallenen Posten als Senkung', () => {
    // Ausschusskosten Material standen im Vergabestand auf −0,20 und fehlen
    // jetzt. Der Wegfall eines negativen Postens ist ein Anstieg um 0,20 —
    // das Vorzeichen dreht sich, und genau daran scheitert eine Brücke, die
    // solche Zeilen wegfiltert.
    const run = lauf()
    const entfallen = run.waterfall.bars.find((b) => b.labelDe.includes('(entfallen)'))

    expect(entfallen).toBeDefined()
    expect(entfallen!.value).toBeCloseTo(0.2, 6)
    expect(entfallen!.kind).toBe('increase')
  })

  it('meldet einen fehlenden Preisblock, statt eine leere Ansicht zu zeigen', () => {
    const run = buildOverviewRun({
      award: null,
      current: summaryMetricsFromRows(AKTUELL, null),
      awardFileNameDe: 'vergabe.xlsx',
      currentFileNameDe: 'aktuell.xlsx',
    })

    expect(run.degradationsDe).toHaveLength(1)
    expect(run.degradationsDe[0]).toContain('vergabe.xlsx')
    expect(run.waterfall.state).toBe('not_computed')
    // Solange der Block fehlt, darf niemand den Lauf zusammenfassen.
    expect(run.validation.summaryBlocked).toBe(true)
    expect(run.tiles).toEqual([])
  })

  it('zeigt die Grenze des Rückbaus: keine Formeln aus der Datenbank', () => {
    // Der Rückbau liefert Werte und Zellen, aber keine Formeln — die Spalte
    // führt sie nicht. Wer hier später einen Zellnachweis mit Formel erwartet,
    // soll an dieser Stelle stolpern statt in der Anzeige.
    const block = summaryMetricsFromRows(VERGABE, null)

    expect(block.formulas).toBeUndefined()
    expect(block.metrics.materialCosts.labelFile).toBeNull()
  })

  it('rechnet die Kacheln aus den gespeicherten Ständen', () => {
    const run = lauf()
    const preis = run.tiles.find((t) => t.key === 'quotationPrice')

    expect(preis?.value).toBeCloseTo(4.4, 6)
    expect(preis?.deltaAbsolute).toBeCloseTo(1.1, 6)
  })

  it('jede veränderte Kachel referenziert eine Differenz, die der Katalog führt', () => {
    // Exit-Kriterium 1 von Loop 3 — und zwar im Format der Vergabe
    // (`assignDifferenceIds`), nicht in dem der Referenzseite.
    const run = lauf()
    const preis = run.tiles.find((t) => t.key === 'quotationPrice')!

    expect(preis.sourceRef.differenceIds).toHaveLength(1)
    expect(preis.sourceRef.differenceIds[0]).toMatch(/^D-SUM-\d{3,}$/)
  })

  it('die Brücken-Schritte tragen ihre Herkunft', () => {
    const run = lauf()
    const schritte = run.waterfall.bars.filter((b) => b.kind === 'increase' || b.kind === 'decrease')

    expect(schritte.length).toBeGreaterThan(0)
    for (const b of schritte) {
      expect(b.differenceIds.length).toBeGreaterThan(0)
      for (const id of b.differenceIds) expect(id).toMatch(/^D-SUM-\d{3,}$/)
    }
  })

  it('benennt die Differenzen ohne Zellnachweis, getrennt von der Lesbarkeits-Degradation', () => {
    // `degradationsDe` bleibt die Aussage über die Eingaben (U-04). Die
    // Herkunfts-Lücke des persistierten Pfads (Bestandszeilen ohne die
    // Provenance-Spalten) ist eine eigene, benannte Aussage — bekannt und
    // gezählt statt still.
    const run = lauf()

    expect(run.degradationsDe).toEqual([])
    expect(run.traceabilityDe).toHaveLength(1)
    expect(run.traceabilityDe[0]).toContain('ohne Zellnachweis')
  })

  it('trägt mit Provenienz-Spalten echte Zellnachweise — und die Lücken-Meldung verschwindet', () => {
    // Kriterium 2 Ende zu Ende auf dem DB-Pfad: Zeilen aus der Zeit nach der
    // Provenance-Migration führen Blatt, Formel und Wertzustand — der Katalog
    // baut daraus DifferenceCells, die Kachel zeigt sie im sourceRef, und
    // validateTraceability hat nichts mehr zu melden.
    const mitProvenienz = (werte: Record<string, number | null>) =>
      Object.entries(werte).map(([metric_key, value]) => ({
        metric_key,
        value,
        currency: 'EUR',
        source_cell: 'D42',
        sheet: 'Zusammenfassung',
        formula: metric_key === 'quotationPrice' ? 'SUMME(D10:D40)' : null,
        value_state: metric_key === 'quotationPrice' ? 'formula_and_cached' : 'constant',
      }))

    const run = buildOverviewRun({
      award: summaryMetricsFromRows(mitProvenienz({ materialCosts: 2.0, quotationPrice: 3.3 }), null),
      current: summaryMetricsFromRows(mitProvenienz({ materialCosts: 2.5, quotationPrice: 4.4 }), null),
      awardFileNameDe: 'vergabe.xlsx',
      currentFileNameDe: 'aktuell.xlsx',
    })

    const preis = run.tiles.find((t) => t.key === 'quotationPrice')!
    expect(preis.sourceRef.cells).toHaveLength(2)
    expect(preis.sourceRef.cells[0]).toMatchObject({
      fileRole: 'award',
      sheet: 'Zusammenfassung',
      cell: 'D42',
      formula: 'SUMME(D10:D40)',
      valueState: 'formula_and_cached',
    })
    expect(run.traceabilityDe).toEqual([])
  })

  it('verwirft DB-Fremdwerte im Wertzustand an der Grenze', () => {
    // Validation an der System-Boundary: ein value_state außerhalb der fünf
    // Spezifikations-Zustände wird verworfen — der Satz bleibt dann ehrlich
    // ohne Zelle statt mit erfundenem Zustand.
    const zeilenMitMuell = [
      { metric_key: 'quotationPrice', value: 3.3, currency: 'EUR', source_cell: 'D42', sheet: 'Z', formula: null, value_state: 'kaputt' },
    ]
    const parse = summaryMetricsFromRows(zeilenMitMuell, null)
    expect(parse.valueStates).toBeUndefined()
  })
})
