// Zellgenaue Datenqualitäts-Detektoren (Kap. 8.7, R-12).

import { describe, it, expect } from 'vitest'
import {
  detectTextDates,
  detectErrorCells,
  detectExternalLinks,
  detectNewlyHiddenColumns,
  detectBrokenTemplateSums,
  detectDeletedCalculationBase,
  detectHardcodedRates,
} from '../data-quality-cells'
import { assignFindingIds } from '../data-quality'
import type { CellSnapshot } from '../cell-state'

const cell = (ref: string, over: Partial<CellSnapshot> = {}): CellSnapshot => ({
  sheet: 'Zusammenfassung',
  cell: ref,
  formula: null,
  value: 1,
  ...over,
})

describe('D01 — Datum als Text', () => {
  it('meldet die Textdatums-Zelle mit Fundstelle', () => {
    const f = detectTextDates([cell('C7', { value: '07.04.2025' }), cell('C8', { value: 45754 })], 'current')
    expect(f).toHaveLength(1)
    expect(f[0].affectedCells).toEqual([{ fileRole: 'current', sheet: 'Zusammenfassung', cell: 'C7' }])
  })

  it('meldet nichts ohne Textdatum', () => {
    expect(detectTextDates([cell('C8', { value: 45754 })], 'current')).toEqual([])
  })
})

describe('D03 — Fehlerzellen', () => {
  const zellen = [cell('AT12', { value: '#N/A' }), cell('AT13', { value: '#N/A' }), cell('AT14', { value: 5 })]

  it('zählt die Fehlerzellen und nennt sie', () => {
    const f = detectErrorCells(zellen, 'both')
    expect(f[0].descriptionDe).toContain('2 Zellen')
    expect(f[0].affectedCells).toHaveLength(2)
  })

  it('empfiehlt im Rechenpfad etwas anderes als in der Vorlage', () => {
    // Im Rechenpfad fehlt ein Betrag; in einer Prüfspalte verdeckt der Fehler
    // nur die Prüfung. Dieselbe Beobachtung, andere Folge.
    expect(detectErrorCells(zellen, 'both', 'cost_path')[0].recommendationDe).toContain('Betrag fehlt')
    expect(detectErrorCells(zellen, 'both', 'template')[0].recommendationDe).toContain('Vorlagenprüfung')
  })

  it('führt den Rechenpfad schwerer als die Vorlage', () => {
    const imPfad = assignFindingIds(detectErrorCells(zellen, 'both', 'cost_path'))[0]
    const inVorlage = assignFindingIds(detectErrorCells(zellen, 'both', 'template'))[0]
    expect(imPfad.severity).toBe('high')
    expect(inVorlage.severity).toBe('medium')
  })
})

describe('D04 — Verweise auf fremde Arbeitsmappen', () => {
  it('meldet den externen Bezug', () => {
    const f = detectExternalLinks(
      [cell('C10', { formula: "'[Sub-QAF.xlsx]Blatt'!A1" }), cell('C11', { formula: 'C10*2' })],
      'both',
    )
    expect(f).toHaveLength(1)
    expect(f[0].descriptionDe).toContain('1 Zellen')
    expect(f[0].recommendationDe).toContain('nicht nachvollziehbar')
  })

  it('meldet nichts bei Bezügen innerhalb der Mappe', () => {
    expect(detectExternalLinks([cell('C11', { formula: 'MATERIAL!K10' })], 'both')).toEqual([])
  })
})

describe('D05 — neu ausgeblendete Spalten', () => {
  it('meldet nur die Spalten, die vorher sichtbar waren', () => {
    // Eine seit jeher ausgeblendete Spalte ist kein Transparenzverlust.
    const f = detectNewlyHiddenColumns(['AZ'], ['AZ', 'H', 'I'], 'SBM-DEVICES-FWZ')
    expect(f).toHaveLength(1)
    expect(f[0].descriptionDe).toContain('H, I')
    expect(f[0].descriptionDe).not.toContain('AZ')
    expect(f[0].scope).toBe('current')
  })

  it('meldet nichts, wenn nichts dazugekommen ist', () => {
    expect(detectNewlyHiddenColumns(['H'], ['H'], 'SBM-DEVICES-FWZ')).toEqual([])
  })

  it('wertet eine wieder eingeblendete Spalte nicht als Befund', () => {
    expect(detectNewlyHiddenColumns(['H', 'I'], ['H'], 'SBM-DEVICES-FWZ')).toEqual([])
  })
})

describe('D06 — Summenzeile gegen ihre Detailzellen', () => {
  const detail = (ref: string, value: number, formula: string | null) =>
    cell(ref, { sheet: 'Fertigung', value, formula })

  it('meldet die Abweichung und nennt die Zellen ohne Formel', () => {
    // Die verbreitetste Ursache: beim Einfügen neuer Zeilen wurde die Formel
    // nicht heruntergezogen. Die Summe sieht vollständig aus und ist es nicht.
    const f = detectBrokenTemplateSums(
      [
        {
          sumCell: cell('Y31', { sheet: 'Fertigung', value: 30, formula: 'SUM(Y12:Y30)' }),
          detailCells: [detail('Y12', 10, 'X12*2'), detail('Y13', 10, 'X13*2'), detail('Y14', 25, null)],
        },
      ],
      'both',
    )
    expect(f).toHaveLength(1)
    expect(f[0].descriptionDe).toContain('45')
    expect(f[0].descriptionDe).toContain('1 der Detailzellen tragen keine Formel')
  })

  it('meldet nichts, wenn die Summe aufgeht', () => {
    expect(
      detectBrokenTemplateSums(
        [
          {
            sumCell: cell('Y31', { sheet: 'Fertigung', value: 20, formula: 'SUM(Y12:Y30)' }),
            detailCells: [detail('Y12', 10, 'X12*2'), detail('Y13', 10, 'X13*2')],
          },
        ],
        'both',
      ),
    ).toEqual([])
  })

  it('rechnet nicht gegen eine Summe ohne Detailwerte', () => {
    // Ohne Detailwerte gibt es keine Gegenrechnung, nur eine Behauptung.
    expect(
      detectBrokenTemplateSums(
        [{ sumCell: cell('Y31', { sheet: 'Fertigung', value: 20 }), detailCells: [] }],
        'both',
      ),
    ).toEqual([])
  })

  it('lässt Rundungsreste durchgehen', () => {
    expect(
      detectBrokenTemplateSums(
        [
          {
            sumCell: cell('Y31', { sheet: 'Fertigung', value: 20 }),
            detailCells: [detail('Y12', 10.001, 'X12'), detail('Y13', 10, 'X13')],
          },
        ],
        'both',
      ),
    ).toEqual([])
  })
})

describe('D08 — Basis gelöscht, Wert hart eingetragen', () => {
  const sbm = { sheet: 'SBM-DEVICES-FWZ' }
  const basis = (values: Array<number | null>) =>
    values.map((v, i) => cell(`AK${88 + i * 0}`.replace('AK', ['AK', 'AM', 'AN'][i] ?? 'AK'), { ...sbm, value: v }))

  it('meldet die Zelle, die vorher rechnete und jetzt fest ist', () => {
    const f = detectDeletedCalculationBase([
      {
        valueAward: cell('AQ88', { ...sbm, formula: 'AK88*AN88', value: 2.135 }),
        valueCurrent: cell('AQ88', { ...sbm, formula: null, value: 1.388 }),
        basisAward: basis([250_000, 1, 1]),
        basisCurrent: basis([250_000, 1, 1]),
      },
    ])
    expect(f).toHaveLength(1)
    expect(f[0].recommendationDe).toContain('Herleitung nachfordern')
  })

  it('meldet den Betrag, dessen Herleitungszellen geleert wurden', () => {
    // Der unauffälligere und im Anlassfall tatsächliche Weg: Einzelpreis und
    // Anzahl sind leer, daneben steht ein Stückwert. Die Wertzelle selbst hat
    // sich dabei nicht verändert.
    const f = detectDeletedCalculationBase([
      {
        valueAward: cell('AQ89', { ...sbm, formula: null, value: 2.135 }),
        valueCurrent: cell('AQ89', { ...sbm, formula: null, value: 0.7505 }),
        basisAward: basis([100, 1, 31_610]),
        basisCurrent: basis([null, null, null]),
      },
    ])
    expect(f).toHaveLength(1)
    expect(f[0].affectedCells[0].cell).toBe('AQ89')
  })

  it('meldet auch dann, wenn der Betrag gleich geblieben ist', () => {
    // Gerade dann fällt niemandem auf, dass die Grundlage verschwunden ist.
    const f = detectDeletedCalculationBase([
      {
        valueAward: cell('AQ88', { ...sbm, formula: null, value: 2.135 }),
        valueCurrent: cell('AQ88', { ...sbm, formula: null, value: 2.135 }),
        basisAward: basis([250_000, 1, 1]),
        basisCurrent: basis([null, null, null]),
      },
    ])
    expect(f).toHaveLength(1)
  })

  it('meldet nichts, wenn weiterhin gerechnet wird', () => {
    expect(
      detectDeletedCalculationBase([
        {
          valueAward: cell('AQ88', { ...sbm, formula: 'AK88*AN88' }),
          valueCurrent: cell('AQ88', { ...sbm, formula: 'AK88*AN88' }),
          basisAward: basis([250_000, 1, 1]),
          basisCurrent: basis([null, null, null]),
        },
      ]),
    ).toEqual([])
  })

  it('meldet nichts, solange die Basis vollständig ist', () => {
    // Dann ist nichts gelöscht worden — der Betrag bleibt herleitbar.
    expect(
      detectDeletedCalculationBase([
        {
          valueAward: cell('AQ88', { ...sbm, formula: null, value: 2 }),
          valueCurrent: cell('AQ88', { ...sbm, formula: null, value: 3 }),
          basisAward: basis([100, 1, 2]),
          basisCurrent: basis([150, 1, 2]),
        },
      ]),
    ).toEqual([])
  })

  it('meldet nichts, wenn die Basis schon im Vergabestand leer war', () => {
    // Es war nie eine Herleitung da, also ist auch keine verschwunden.
    expect(
      detectDeletedCalculationBase([
        {
          valueAward: cell('AQ88', { ...sbm, formula: null, value: 2 }),
          valueCurrent: cell('AQ88', { ...sbm, formula: null, value: 3 }),
          basisAward: basis([null, null, null]),
          basisCurrent: basis([null, null, null]),
        },
      ]),
    ).toEqual([])
  })

  it('meldet nichts, wenn die Zelle jetzt leer ist', () => {
    // Eine leere Zelle ist kein hart eingetragener Wert; das meldet D02.
    expect(
      detectDeletedCalculationBase([
        {
          valueAward: cell('AQ88', { ...sbm, formula: 'AK88*AN88' }),
          valueCurrent: cell('AQ88', { ...sbm, formula: null, value: null }),
          basisAward: basis([250_000, 1, 1]),
          basisCurrent: basis([null, null, null]),
        },
      ]),
    ).toEqual([])
  })
})

describe('D13 — hart codierter Satz', () => {
  it('meldet die Formel ohne Zellbezug', () => {
    const f = detectHardcodedRates(
      [cell('P23', { formula: '0.31*1.11' }), cell('P24', { formula: 'C23*0.31' })],
      'both',
    )
    expect(f).toHaveLength(1)
    expect(f[0].affectedCells.map((c) => c.cell)).toEqual(['P23'])
  })

  it('wird als Hinweis geführt, nicht als Fehler', () => {
    // Ein hart codierter Satz ist erklärungsbedürftig, aber nicht falsch.
    const f = assignFindingIds(detectHardcodedRates([cell('P23', { formula: '0.31*1.11' })], 'both'))
    expect(f[0].severity).toBe('info')
  })
})
