// Tabellenvorlagen der Anzeigeebene (Kap. 22, Invariante U-08).

import { describe, it, expect } from 'vitest'
import { buildTable, rowsFromRecords, validateTable, type TableRow } from '../table-specs'
import { assignDifferenceIds, type RawDifference } from '../all-differences'

const row = (over: Partial<TableRow> = {}): TableRow => ({
  differenceId: 'D-MAT-001',
  anchorRow: 10,
  namesAward: ['Dichtung'],
  namesCurrent: ['Dichtung'],
  matchType: 'exact',
  valueAward: 1,
  valueCurrent: 2,
  delta: 1,
  unit: 'EUR_per_piece',
  bucket: 'price_change',
  findingIds: [],
  cells: [],
  ...over,
})

const alle = (): TableRow[] => [
  row({ differenceId: 'D-MAT-003', anchorRow: 30, delta: -5, bucket: 'consolidation', matchType: 'merged' }),
  row({ differenceId: 'D-MAT-001', anchorRow: 10, delta: 1 }),
  row({ differenceId: 'D-MAT-002', anchorRow: 20, delta: 0, bucket: 'unchanged', findingIds: ['DQ-B-004'] }),
  row({ differenceId: 'D-MAT-004', anchorRow: null, delta: 3, matchType: 'added', valueAward: null }),
]

describe('buildTable — Sortierung', () => {
  it('ordnet nach Blattreihenfolge, nicht nach Betrag', () => {
    // Wer eine Position sucht, sucht sie dort, wo sie in der Datei steht.
    const t = buildTable(alle())
    expect(t.rows.map((r) => r.anchorRow)).toEqual([10, 20, 30, null])
  })

  it('hängt Zeilen ohne Zeilenbezug hinten an', () => {
    expect(buildTable(alle()).rows[3].differenceId).toBe('D-MAT-004')
  })

  it('sortiert bei gleicher Zeile nach Kennung', () => {
    // Sonst wandert die Reihenfolge zwischen zwei Läufen.
    const gleich = [row({ differenceId: 'D-MAT-009', anchorRow: 10 }), row({ differenceId: 'D-MAT-002', anchorRow: 10 })]
    expect(buildTable(gleich).rows.map((r) => r.differenceId)).toEqual(['D-MAT-002', 'D-MAT-009'])
    expect(buildTable([...gleich].reverse()).rows.map((r) => r.differenceId)).toEqual(['D-MAT-002', 'D-MAT-009'])
  })

  it('sortiert auf Wunsch nach Betrag', () => {
    expect(buildTable(alle(), {}, 'delta_desc').rows.map((r) => r.delta)).toEqual([3, 1, 0, -5])
    expect(buildTable(alle(), {}, 'delta_asc').rows.map((r) => r.delta)).toEqual([-5, 0, 1, 3])
  })
})

describe('buildTable — Filter', () => {
  it('nennt, wie viele Zeilen es ausblendet und welche Summe damit fehlt', () => {
    // Eine Tabelle, die stillschweigend die Hälfte weglässt, ist die bequemste
    // Art, eine Verhandlung zu verlieren.
    const t = buildTable(alle(), { changedOnly: true })
    expect(t.rows).toHaveLength(3)
    expect(t.hidden).toEqual({ count: 1, deltaSum: 0 })
    expect(t.noticeDe).toContain('1 von 4')
  })

  it('schweigt, wenn nichts ausgeblendet ist', () => {
    expect(buildTable(alle()).noticeDe).toBeNull()
  })

  it('filtert nach Zuordnungsart und Effektgruppe', () => {
    expect(buildTable(alle(), { matchTypes: ['merged'] }).rows.map((r) => r.differenceId)).toEqual(['D-MAT-003'])
    expect(buildTable(alle(), { buckets: ['consolidation'] }).rows).toHaveLength(1)
  })

  it('filtert auf Zeilen mit Befund', () => {
    expect(buildTable(alle(), { withFindingsOnly: true }).rows.map((r) => r.differenceId)).toEqual(['D-MAT-002'])
  })

  it('filtert auf Nullkosten-Zeilen', () => {
    // Eine Seite ohne Betrag genügt — das ist der Fall, um den es geht.
    expect(buildTable(alle(), { zeroCostOnly: true }).rows.map((r) => r.differenceId)).toEqual(['D-MAT-004'])
  })

  it('sucht über Namen und Kennung', () => {
    expect(buildTable(alle(), { search: 'D-MAT-003' }).rows).toHaveLength(1)
    expect(buildTable(alle(), { search: 'dicht' }).rows.length).toBeGreaterThan(0)
    expect(buildTable(alle(), { search: '   ' }).rows).toHaveLength(4)
  })

  it('meldet eine leere Trefferliste als solche, nicht als vollständige Tabelle', () => {
    const t = buildTable(alle(), { search: 'gibtesnicht' })
    expect(t.rows).toEqual([])
    expect(t.noticeDe).toContain('4 von 4')
  })
})

describe('buildTable — Summen (U-03)', () => {
  it('summiert die angezeigten Deltas erst und rundet dann', () => {
    const feine = [
      row({ differenceId: 'D-MAT-001', anchorRow: 1, delta: 0.004 }),
      row({ differenceId: 'D-MAT-002', anchorRow: 2, delta: 0.004 }),
      row({ differenceId: 'D-MAT-003', anchorRow: 3, delta: 0.004 }),
    ]
    expect(buildTable(feine).displayedDeltaSum).toBe(0.01)
  })
})

describe('validateTable', () => {
  it('meldet nichts, solange angezeigt plus ausgeblendet die Gesamtsumme ergibt', () => {
    const rows = alle()
    expect(validateTable(buildTable(rows, { changedOnly: true }), rows)).toEqual([])
  })

  it('meldet den beim Filtern verschwundenen Betrag', () => {
    // Der Fehler, den ein Filter am leichtesten macht.
    const rows = alle()
    const kaputt = { ...buildTable(rows, { changedOnly: true }), hidden: { count: 1, deltaSum: 99 } }
    const p = validateTable(kaputt, rows)
    expect(p.map((x) => x.kind)).toContain('sum_mismatch')
  })

  it('meldet ausgeblendete Zeilen ohne Hinweis', () => {
    const rows = alle()
    const kaputt = { ...buildTable(rows, { changedOnly: true }), noticeDe: null }
    expect(validateTable(kaputt, rows).map((x) => x.kind)).toContain('hidden_without_notice')
  })
})

describe('rowsFromRecords', () => {
  const records = assignDifferenceIds([
    {
      area: 'MAT',
      kind: 'changed',
      formulaFindingKind: null,
      descriptionDe: 'Preis verändert',
      valueAward: 1,
      valueCurrent: 2,
      delta: 1,
      deltaNullReason: null,
      unit: 'EUR_per_piece',
      cells: [],
      relatedDq: ['DQ-B-004'],
      anchorRow: 10,
    } satisfies RawDifference,
  ])

  it('übernimmt Werte, Einheit und Befunde aus dem Katalog', () => {
    const [r] = rowsFromRecords(records, new Map())
    expect(r).toMatchObject({ anchorRow: 10, valueAward: 1, valueCurrent: 2, delta: 1, findingIds: ['DQ-B-004'] })
  })

  it('nimmt Namen und Zuordnungsart von aussen entgegen', () => {
    // Die Beschreibung bleibt ein Satz, die Spalte wird ein Name.
    const namen = new Map([
      [records[0].differenceId, { award: ['Alt'], current: ['Neu'], matchType: 'renamed', bucket: 'price_change' }],
    ])
    const [r] = rowsFromRecords(records, namen)
    expect(r).toMatchObject({ namesAward: ['Alt'], namesCurrent: ['Neu'], matchType: 'renamed', bucket: 'price_change' })
  })

  it('fällt ohne Namensangabe auf die Art des Katalogs zurück', () => {
    expect(rowsFromRecords(records, new Map())[0].matchType).toBe('changed')
  })
})
