// Datenverträge der Anzeigeebene (Teil D, Kap. 22 und 26).

import { describe, it, expect } from 'vitest'
import {
  buildKpiTiles,
  displaySum,
  evidenceFor,
  sectionHeaders,
  statusFor,
  validateViewSpec,
  validationBanner,
  type KpiTileInput,
} from '../view-specs'
import { assignDifferenceIds, type DifferenceRecord, type RawDifference } from '../all-differences'
import { assignFindingIds } from '../data-quality'

const diff = (over: Partial<RawDifference> = {}): RawDifference => ({
  area: 'SUM',
  kind: 'changed',
  formulaFindingKind: null,
  descriptionDe: 'Preis verändert',
  valueAward: 100,
  valueCurrent: 110,
  delta: 10,
  deltaNullReason: null,
  unit: 'EUR',
  cells: [
    { fileRole: 'award', sheet: 'Zusammenfassung', cell: 'P29', formula: 'SUM(P12:P28)', valueState: 'formula_and_cached' },
    { fileRole: 'current', sheet: 'Zusammenfassung', cell: 'P29', formula: null, valueState: 'constant' },
  ],
  relatedDq: [],
  anchorRow: 29,
  ...over,
})

const records = (): DifferenceRecord[] => assignDifferenceIds([diff()])

const tile = (over: Partial<KpiTileInput> = {}): KpiTileInput => ({
  key: 'preis',
  labelDe: 'Stückpreis',
  valueAward: 100,
  valueCurrent: 110,
  unit: 'EUR_per_piece',
  path: 'summary_lines[0].value_current',
  differenceIds: records().map((r) => r.differenceId),
  ...over,
})

describe('statusFor', () => {
  it('färbt nach Schwellen und nicht nach Einzelfall', () => {
    expect(statusFor(0.15)).toBe('critical')
    expect(statusFor(0.05)).toBe('warning')
    expect(statusFor(0.001)).toBe('neutral')
    expect(statusFor(-0.05)).toBe('good')
  })

  it('bleibt neutral, wo kein Delta bestimmbar ist', () => {
    // Grau heisst „nicht bestimmbar" — grün wäre eine Aussage, die niemand
    // geprüft hat.
    expect(statusFor(null)).toBe('neutral')
  })

  it('nimmt eigene Schwellen entgegen', () => {
    expect(statusFor(0.03, { warning: 0.05, critical: 0.2 })).toBe('neutral')
  })
})

describe('buildKpiTiles', () => {
  it('rechnet Delta und Anteil, damit es kein Renderer tut', () => {
    // Vier Renderer, die dasselbe rechnen, rechnen es irgendwann verschieden.
    const [t] = buildKpiTiles([tile()], records())
    expect(t.deltaAbsolute).toBe(10)
    expect(t.deltaRatio).toBeCloseTo(0.1, 9)
    expect(t.status).toBe('critical')
  })

  it('hängt Pfad und Zellen als Quelle an', () => {
    const [t] = buildKpiTiles([tile()], records())
    expect(t.sourceRef.path).toBe('summary_lines[0].value_current')
    expect(t.sourceRef.cells).toHaveLength(2)
  })

  it('lässt den Anteil weg, wo der Ausgangswert null ist', () => {
    // Eine Steigerung „um unendlich Prozent" ist keine Aussage.
    const [t] = buildKpiTiles([tile({ valueAward: 0 })], records())
    expect(t.deltaAbsolute).toBe(110)
    expect(t.deltaRatio).toBeNull()
  })

  it('lässt Delta und Anteil weg, wo ein Stand fehlt', () => {
    const [t] = buildKpiTiles([tile({ valueAward: null })], records())
    expect(t.deltaAbsolute).toBeNull()
    expect(t.deltaRatio).toBeNull()
    expect(t.status).toBe('neutral')
  })

  it('führt den Formelhinweis mit, wo einer gehört', () => {
    const [t] = buildKpiTiles(
      [tile({ key: 'lifetime', formulaNoteDe: 'Delta mal Gesamtvolumen — Bewertungsgrösse, keine bestätigte Einsparung.' })],
      records(),
    )
    expect(t.formulaNoteDe).toContain('Bewertungsgrösse')
  })
})

describe('displaySum — U-03', () => {
  it('summiert erst und rundet dann', () => {
    // Andersherum liegt die Spaltensumme um einen Cent daneben und löst jede
    // Rückfrage aus.
    const werte = [0.004, 0.004, 0.004]
    expect(displaySum(werte, 2)).toBe(0.01)
    expect(werte.map((v) => Math.round(v * 100) / 100).reduce((s, v) => s + v, 0)).toBe(0)
  })

  it('behandelt fehlende Werte als null Beitrag', () => {
    expect(displaySum([1.5, null, 2.5], 2)).toBe(4)
  })

  it('rundet auf die verlangte Stellenzahl', () => {
    expect(displaySum([0.123456], 4)).toBe(0.1235)
  })
})

describe('evidenceFor — das Zellnachweis-Panel', () => {
  it('nennt Datei, Blatt, Zelle, Formel und Zustand', () => {
    const [r] = records()
    const zeilen = evidenceFor(r)
    expect(zeilen).toHaveLength(2)
    expect(zeilen[0]).toMatchObject({
      fileRole: 'award',
      sheet: 'Zusammenfassung',
      cell: 'P29',
      formula: 'SUM(P12:P28)',
      valueState: 'formula_and_cached',
    })
  })

  it('hängt die Befunde derselben Zelle an', () => {
    const findings = assignFindingIds([
      {
        detector: 'D08',
        scope: 'current',
        context: 'cost_path',
        descriptionDe: 'Basis gelöscht',
        recommendationDe: 'Herleitung nachfordern',
        affectedCells: [{ fileRole: 'current', sheet: 'Zusammenfassung', cell: 'P29' }],
        relatedDifferenceIds: [],
      },
    ])
    const zeilen = evidenceFor(records()[0], findings)
    expect(zeilen[1].findingIds).toEqual(['DQ-B-001'])
    expect(zeilen[0].findingIds).toEqual([])
  })

  it('nimmt die am Satz vermerkten Befunde mit auf', () => {
    const [r] = assignDifferenceIds([diff({ relatedDq: ['DQ-B-003'] })])
    expect(evidenceFor(r)[0].findingIds).toEqual(['DQ-B-003'])
  })
})

describe('sectionHeaders — U-04', () => {
  it('macht „nicht berechnet" sichtbar', () => {
    // Eine leer wirkende Sektion, die nie gerechnet wurde, liest sich sonst als
    // Entwarnung.
    const h = sectionHeaders({
      werkzeuge: { labelDe: 'Werkzeuge', state: 'not_computed' },
      material: { labelDe: 'Material', state: 'empty_verified' },
      uebersicht: { labelDe: 'Übersicht', state: 'populated' },
    })
    const nachKey = Object.fromEntries(h.map((x) => [x.key, x]))
    expect(nachKey.werkzeuge.noticeDe).toContain('Nicht berechnet')
    expect(nachKey.material.noticeDe).toContain('keine Abweichungen')
    expect(nachKey.uebersicht.noticeDe).toBeNull()
  })

  it('liefert die Sektionen in stabiler Reihenfolge', () => {
    const h = sectionHeaders({
      werkzeuge: { labelDe: 'W', state: 'populated' },
      material: { labelDe: 'M', state: 'populated' },
    })
    expect(h.map((x) => x.key)).toEqual(['material', 'werkzeuge'])
  })
})

describe('validationBanner', () => {
  it('bleibt unsichtbar, solange alles besteht', () => {
    const b = validationBanner([])
    expect(b).toMatchObject({ visible: false, summaryBlocked: false, messageDe: null })
  })

  it('sperrt die Zusammenfassung sichtbar und nennt den Grund', () => {
    // Eine fehlende Zusammenfassung erklärt sich niemandem von selbst.
    const b = validationBanner(['sbm_bridge_exact', 'bridge_closes'])
    expect(b.visible).toBe(true)
    expect(b.summaryBlocked).toBe(true)
    expect(b.failedChecks).toEqual(['bridge_closes', 'sbm_bridge_exact'])
    expect(b.messageDe).toContain('bridge_closes')
    expect(b.messageDe).toContain('gesperrt')
  })

  it('formuliert die Einzahl richtig', () => {
    expect(validationBanner(['bridge_closes']).messageDe).toContain('1 Prüfung des Kerns')
  })
})

describe('validateViewSpec — U-02 und U-03', () => {
  it('meldet nichts für eine belegte Vorlage', () => {
    expect(validateViewSpec(buildKpiTiles([tile()], records()))).toEqual([])
  })

  it('meldet die Kachel ohne Verweis in den Kern', () => {
    const [t] = buildKpiTiles([tile({ differenceIds: [] })], records())
    const p = validateViewSpec([t])
    expect(p.map((x) => x.kind)).toEqual(['number_without_source'])
  })

  it('lässt eine Kachel ohne Wert durchgehen', () => {
    // Wo nichts steht, braucht es keinen Beleg.
    const [t] = buildKpiTiles([tile({ valueAward: null, valueCurrent: null, differenceIds: [] })], records())
    expect(validateViewSpec([t])).toEqual([])
  })

  it('meldet die Anzeigesumme, die nicht zur Kernsumme passt', () => {
    const p = validateViewSpec([], [{ where: 'material', displayed: 12.35, core: 12.34 }])
    expect(p.map((x) => x.kind)).toEqual(['sum_mismatch'])
    expect(p[0].detail).toContain('12.34')
  })

  it('lässt Rundung auf den Cent durchgehen', () => {
    expect(validateViewSpec([], [{ where: 'material', displayed: 12.34, core: 12.3401 }])).toEqual([])
  })
})
