// Diagrammvorlagen der Anzeigeebene (Kap. 22).

import { describe, it, expect } from 'vitest'
import { buildMovers, buildWaterfall, emptyState, type WaterfallInput } from '../chart-specs'

const bridge = (over: Partial<WaterfallInput> = {}): WaterfallInput => ({
  startLabelDe: 'Vergabestand',
  endLabelDe: 'Aktueller Stand',
  start: 100,
  end: 130,
  steps: [
    { key: 'material', labelDe: 'Material', delta: 40, differenceIds: ['D-MAT-001'] },
    { key: 'fertigung', labelDe: 'Fertigung', delta: -10 },
  ],
  residual: 0,
  ...over,
})

describe('buildWaterfall', () => {
  it('setzt die Balken auf ihre aufgelaufenen Kanten', () => {
    const w = buildWaterfall(bridge())
    expect(w.state).toBe('ok')
    expect(w.bars.map((b) => [b.key, b.kind, b.value, b.base])).toEqual([
      ['start', 'start', 100, 0],
      ['material', 'increase', 40, 100],
      ['fertigung', 'decrease', -10, 130],
      ['end', 'end', 130, 0],
    ])
  })

  it('zeichnet keine Brücke, die nicht schliesst', () => {
    // Ein Bild, das schliesst, obwohl die Zahlen es nicht tun, ist schlimmer
    // als kein Bild.
    const w = buildWaterfall(bridge({ residual: 0.3 }))
    expect(w.state).toBe('not_reconciled')
    expect(w.bars).toEqual([])
    expect(w.diagnosticsDe.join(' ')).toContain('Rest')
  })

  it('erzeugt niemals einen Balken „Übrige"', () => {
    // Der Balken, der genau den Betrag versteckt, dessen Herkunft niemand kennt.
    const w = buildWaterfall(bridge({ residual: 5 }))
    expect(w.bars.map((b) => b.labelDe)).not.toContain('Übrige')
    expect(w.diagnosticsDe.join(' ')).toContain('Übrige')
  })

  it('rechnet den Rest selbst nach, wenn keiner mitgegeben wird', () => {
    const w = buildWaterfall(bridge({ residual: null, steps: [{ key: 'a', labelDe: 'A', delta: 10 }] }))
    expect(w.state).toBe('not_reconciled')
  })

  it('schreibt die Diagnose in lesbaren Zahlen', () => {
    // Vorher stand hier das rohe Ergebnis der Subtraktion: „es bleibt ein Rest
    // von 1.370000000000004". Für den Leser ist das kein Befund, sondern ein
    // zweites Rätsel — und die bisherigen Zusicherungen prüften nur, DASS das
    // Wort „Rest" vorkommt, nie in welcher Form.
    const w = buildWaterfall({
      startLabelDe: 'Vergabestand',
      endLabelDe: 'Aktueller Stand',
      start: 87.42,
      end: 92.15,
      steps: [
        { key: 'material', labelDe: 'Material', delta: 2.84 },
        { key: 'werkzeug', labelDe: 'Werkzeugumlage', delta: 0.68 },
        { key: 'logistik', labelDe: 'Logistik', delta: -0.16 },
      ],
      residual: null,
    })

    expect(w.state).toBe('not_reconciled')
    const text = w.diagnosticsDe.join(' ')
    expect(text).toContain('3,36')
    expect(text).toContain('4,73')
    expect(text).toContain('1,37')
    // Kein Punkt als Dezimaltrennzeichen und keine Gleitkomma-Nachkommastellen.
    expect(text).not.toMatch(/\d\.\d/)
    expect(text).not.toMatch(/\d{6}/)
  })

  it('glaubt einem mitgegebenen Restposten nicht mehr als den Balken', () => {
    // Sonst entschiede eine Zahl von aussen darüber, ob das Bild gezeichnet
    // wird — und ein Aufrufer, der veraltete Schritte mit einem alten
    // Restposten kombiniert, bekäme einen Wasserfall, dessen Balken nicht zum
    // Ende führen.
    const w = buildWaterfall(bridge({ residual: 0, steps: [{ key: 'a', labelDe: 'A', delta: 10 }] }))
    expect(w.state).toBe('not_reconciled')
    expect(w.bars).toEqual([])
  })

  it('meldet „nicht berechnet" statt einer geschätzten Kante', () => {
    expect(buildWaterfall(bridge({ start: null })).state).toBe('not_computed')
    expect(buildWaterfall(bridge({ end: null })).bars).toEqual([])
  })

  it('lässt Rundungsreste innerhalb der Toleranz durchgehen', () => {
    expect(buildWaterfall(bridge({ residual: 0.004 })).state).toBe('ok')
    expect(buildWaterfall(bridge({ residual: 0.004, tolerance: 0.001 })).state).toBe('not_reconciled')
  })

  it('kennzeichnet eine beschnittene Skala', () => {
    // Ohne Kennzeichnung wirkt ein Prozent wie eine Verdopplung.
    const klein = buildWaterfall({
      ...bridge(),
      start: 1000,
      end: 1010,
      steps: [{ key: 'a', labelDe: 'A', delta: 10 }],
      residual: 0,
    })
    expect(klein.truncatedScale).toBe(true)
    expect(buildWaterfall(bridge()).truncatedScale).toBe(false)
  })

  it('führt die Belege je Schritt mit', () => {
    const w = buildWaterfall(bridge())
    expect(w.bars.find((b) => b.key === 'material')!.differenceIds).toEqual(['D-MAT-001'])
  })
})

describe('buildMovers', () => {
  const alle = [
    { key: 'a', labelDe: 'A', value: 5 },
    { key: 'b', labelDe: 'B', value: -8 },
    { key: 'c', labelDe: 'C', value: 2 },
    { key: 'd', labelDe: 'D', value: -0.42 },
  ]

  it('sortiert nach Betrag, nicht nach Vorzeichen', () => {
    const m = buildMovers(alle, 2, 'EUR')
    expect(m.rows.map((r) => r.key)).toEqual(['b', 'a'])
  })

  it('nennt im Bild, wovon die Auswahl eine Auswahl ist', () => {
    // Ohne diesen Hinweis liest sich eine Top-Liste wie eine vollständige.
    const m = buildMovers(alle, 3, 'EUR')
    expect(m.selectionNoteDe).toBe('Top 3 von 4 · Rest -0.42')
    expect(m.selection).toMatchObject({ shown: 3, total: 4, residualSum: -0.42 })
  })

  it('sagt es auch, wenn nichts ausgeblendet ist', () => {
    expect(buildMovers(alle, 10, 'EUR').selectionNoteDe).toBe('Alle 4 Positionen gezeigt.')
  })

  it('summiert Gezeigtes und Rest getrennt', () => {
    const m = buildMovers(alle, 2, 'EUR')
    expect(m.selection.shownSum).toBe(-3)
    expect(m.selection.residualSum).toBe(1.58)
  })

  it('bleibt bei gleichen Beträgen stabil sortiert', () => {
    const gleich = [
      { key: 'z', labelDe: 'Z', value: 5 },
      { key: 'a', labelDe: 'A', value: 5 },
    ]
    expect(buildMovers(gleich, 2, 'EUR').rows.map((r) => r.key)).toEqual(['a', 'z'])
    expect(buildMovers([...gleich].reverse(), 2, 'EUR').rows.map((r) => r.key)).toEqual(['a', 'z'])
  })

  it('kommt mit einer leeren Liste zurecht', () => {
    const m = buildMovers([], 5, 'EUR')
    expect(m.rows).toEqual([])
    expect(m.selection).toMatchObject({ shown: 0, total: 0, shownSum: 0, residualSum: 0 })
  })
})

describe('emptyState — U-04', () => {
  it('meldet geprüft und leer als positive Nachricht mit Zahl', () => {
    expect(emptyState('empty_no_findings', { checksPassed: 12 })).toEqual({
      tone: 'positive',
      messageDe: 'Keine Auffälligkeiten — 12 Prüfungen bestanden.',
    })
  })

  it('meldet „nicht anwendbar" neutral und mit Grund', () => {
    const e = emptyState('not_applicable', { reasonDe: 'kein Werkzeugblatt vorhanden' })
    expect(e.tone).toBe('neutral')
    expect(e.messageDe).toContain('kein Werkzeugblatt')
  })

  it('meldet „nicht berechnet" als Warnung, nicht als Entwarnung', () => {
    // Die gefährlichste Falschaussage des ganzen Berichts, weil sie beruhigt.
    const e = emptyState('not_computed')
    expect(e.tone).toBe('warning')
    expect(e.messageDe).toContain('keine beruhigende')
    expect(e.messageDe).not.toContain('Keine Auffälligkeiten')
  })

  it('unterscheidet die drei Fälle auch ohne Zusatzangaben', () => {
    const toene = (['empty_no_findings', 'not_applicable', 'not_computed'] as const).map((s) => emptyState(s).tone)
    expect(new Set(toene).size).toBe(3)
  })
})
