import { describe, it, expect } from 'vitest'
import type { QAFRow } from '@/lib/qaf-parser'
import {
  computeFieldDiff,
  computeFieldDiffWithNotApplicable,
  computeNumericDelta,
  diffSteps,
  DEFAULT_DIFFER_BANDS_CONFIG,
  type DifferBandsConfig,
} from '../differ'
import { buildFormulaProvenance } from '../formula-engine'

function mkRow(overrides: Partial<QAFRow> = {}): QAFRow {
  return {
    positionsnummer: '1',
    teilebenennung: 'Teil',
    prozessbezeichnung: 'Prozess',
    bezeichnungAnlage: 'Anlage',
    standort: 'Werk',
    beschaffungswaehrung: 'EUR',
    angebotswaehrung: 'EUR',
    zykluszeit: 10,
    teileProZyklus: 1,
    anzahlMA: 1,
    lohnkosten: 30,
    lohnzuschlagssaetze: 10,
    mss: 50,
    ruestkosten: 0,
    fek: 0,
    rfgk: 0,
    fk: 100,
    wechselkurs: 1,
    anzahlProAngebotsteil: 1,
    fkAW: 100,
    ausschuss: 2,
    ausschusskosten: 1,
    ...overrides,
  }
}

describe('computeFieldDiff', () => {
  it('computes absolute and relative delta', () => {
    const d = computeFieldDiff('fk', 100, 110)
    expect(d.deltaAbsolute).toBe(10)
    expect(d.deltaPercent).toBeCloseTo(0.1, 6)
    // exactly +10 % is not "> 10 %", so it stays a plain anstieg (spec B13)
    expect(d.status).toBe('anstieg')
  })

  it('flags konstant when unchanged', () => {
    expect(computeFieldDiff('fk', 100, 100).status).toBe('konstant')
  })

  it('flags small rise/fall under 10%', () => {
    expect(computeFieldDiff('fk', 100, 105).status).toBe('anstieg')
    expect(computeFieldDiff('fk', 100, 96).status).toBe('senkung')
  })

  it('escalates by magnitude band', () => {
    expect(computeFieldDiff('fk', 100, 120).status).toBe('auffaellig_10') // +20%
    expect(computeFieldDiff('fk', 100, 130).status).toBe('auffaellig_25') // +30%
    expect(computeFieldDiff('fk', 100, 160).status).toBe('kritisch_50') // +60%
    expect(computeFieldDiff('fk', 100, 40).status).toBe('kritisch_50') // -60%
  })

  it('marks neu when ALT is blank/null', () => {
    const d = computeFieldDiff('fk', null, 50)
    expect(d.status).toBe('neu')
    expect(d.deltaPercent).toBeNull()
    expect(d.deltaAbsolute).toBeNull()
  })

  it('marks entfallen when NEU is null', () => {
    expect(computeFieldDiff('fk', 50, null).status).toBe('entfallen')
  })

  it('marks nicht_berechenbar when both null', () => {
    expect(computeFieldDiff('fk', null, null).status).toBe('nicht_berechenbar')
  })

  it('does not force a percent delta when ALT is real 0', () => {
    const d = computeFieldDiff('fk', 0, 5)
    expect(d.deltaPercent).toBeNull()
    expect(d.deltaAbsolute).toBe(5)
    expect(d.status).toBe('anstieg')
  })

  it('adds percentage-point delta for percent fields (SGK)', () => {
    const d = computeFieldDiff('lohnzuschlagssaetze', 10, 15)
    expect(d.isPercentField).toBe(true)
    expect(d.deltaPercentagePoints).toBe(5)
  })

  it('leaves percentage-point delta null for non-percent fields', () => {
    expect(computeFieldDiff('fk', 100, 110).deltaPercentagePoints).toBeNull()
  })
})

describe('diffSteps', () => {
  it('produces one diff per numeric field for two steps', () => {
    const diffs = diffSteps(mkRow(), mkRow({ fk: 120, zykluszeit: 12 }))
    const fk = diffs.find((d) => d.field === 'fk')
    const ct = diffs.find((d) => d.field === 'zykluszeit')
    expect(fk?.deltaAbsolute).toBe(20)
    expect(ct?.deltaAbsolute).toBe(2)
  })

  it('marks all fields entfallen when neu step is missing (removed)', () => {
    const diffs = diffSteps(mkRow(), null)
    expect(diffs.every((d) => d.status === 'entfallen' || d.status === 'nicht_berechenbar')).toBe(true)
  })

  it('marks all fields neu when alt step is missing (new)', () => {
    const diffs = diffSteps(null, mkRow())
    expect(diffs.every((d) => d.status === 'neu' || d.status === 'nicht_berechenbar')).toBe(true)
  })
})

// P0.6 / KAR-891: "n.a." field state must not collapse into 'neu'/'entfallen'/
// 'nicht_berechenbar', and must not carry a numeric delta.
describe('computeFieldDiffWithNotApplicable', () => {
  it('both sides "n.a." → nicht_anwendbar, no numeric delta (unauffaellig)', () => {
    const d = computeFieldDiffWithNotApplicable('fk', 'n.a.', 'n.a.', null, null)
    expect(d.status).toBe('nicht_anwendbar')
    expect(d.deltaAbsolute).toBeNull()
    expect(d.deltaPercent).toBeNull()
    expect(d.altValue).toBeNull()
    expect(d.neuValue).toBeNull()
  })

  it('ALT "n.a." / NEU has a real value → nicht_anwendbar, not "neu"', () => {
    const d = computeFieldDiffWithNotApplicable('fk', 'entfällt', undefined, null, 120)
    expect(d.status).toBe('nicht_anwendbar')
    expect(d.deltaAbsolute).toBeNull()
    expect(d.neuValue).toBe(120)
  })

  it('ALT has a real value / NEU "n.a." → nicht_anwendbar, not "entfallen"', () => {
    const d = computeFieldDiffWithNotApplicable('fk', undefined, 'n/a', 100, null)
    expect(d.status).toBe('nicht_anwendbar')
    expect(d.deltaAbsolute).toBeNull()
    expect(d.altValue).toBe(100)
  })

  it('neither side "n.a." → falls through to normal computeFieldDiff behaviour', () => {
    const d = computeFieldDiffWithNotApplicable('fk', undefined, undefined, 100, 110)
    expect(d.status).toBe('anstieg')
    expect(d.deltaAbsolute).toBe(10)
  })

  it('carries isPercentField through for percent fields', () => {
    const d = computeFieldDiffWithNotApplicable('lohnzuschlagssaetze', 'n.a.', undefined, null, 10)
    expect(d.isPercentField).toBe(true)
    expect(d.deltaPercentagePoints).toBeNull()
  })
})

// P1.5/KAR-896: bands are now an explicit, injectable parameter (default
// DEFAULT_DIFFER_BANDS_CONFIG matches the previously-hardcoded 10/25/50%
// literals byte-for-byte — see the "escalates by magnitude band" case above).
describe('DifferBandsConfig injection (KAR-896)', () => {
  it('DEFAULT_DIFFER_BANDS_CONFIG matches the historical hardcoded thresholds', () => {
    expect(DEFAULT_DIFFER_BANDS_CONFIG).toEqual({ auffaellig10: 0.1, auffaellig25: 0.25, kritisch50: 0.5 })
  })

  it('computeNumericDelta uses the default bands when no config is passed (regression)', () => {
    expect(computeNumericDelta(100, 120).status).toBe('auffaellig_10')
  })

  it('a wider custom band config raises the escalation threshold', () => {
    const wide: DifferBandsConfig = { auffaellig10: 0.2, auffaellig25: 0.4, kritisch50: 0.6 }
    // +20% would normally be auffaellig_10 (> 0.1), but with a 0.2 threshold it
    // is not strictly greater than the band, so it falls back to plain anstieg.
    expect(computeNumericDelta(100, 120, wide).status).toBe('anstieg')
    // +25% is > 0.2 (auffaellig10) and not > 0.4 (auffaellig25) -> auffaellig_10
    expect(computeNumericDelta(100, 125, wide).status).toBe('auffaellig_10')
  })

  it('computeFieldDiff threads a custom band config through', () => {
    const narrow: DifferBandsConfig = { auffaellig10: 0.05, auffaellig25: 0.15, kritisch50: 0.3 }
    expect(computeFieldDiff('fk', 100, 120, narrow).status).toBe('auffaellig_25') // +20% > 15%
  })

  it('computeFieldDiffWithNotApplicable threads a custom band config through the non-n.a. fallthrough', () => {
    const narrow: DifferBandsConfig = { auffaellig10: 0.05, auffaellig25: 0.15, kritisch50: 0.3 }
    const d = computeFieldDiffWithNotApplicable('fk', undefined, undefined, 100, 120, narrow)
    expect(d.status).toBe('auffaellig_25')
  })

  it('diffSteps threads a custom band config through every field', () => {
    const narrow: DifferBandsConfig = { auffaellig10: 0.05, auffaellig25: 0.15, kritisch50: 0.3 }
    const diffs = diffSteps(mkRow(), mkRow({ fk: 120 }), narrow)
    expect(diffs.find((d) => d.field === 'fk')?.status).toBe('auffaellig_25')
  })
})

// KAR-900/P2.1: formula-aware diffing — diffSteps consults each row's
// optional `formulas` provenance (formula-engine.ts) and overrides the
// otherwise-computed status for the two "reportable, status-overriding"
// comparison kinds, tags the "value also changed" kind, and tags the
// hash-equal-but-value-different kind — see formula-engine.ts
// FormulaComparisonKind doc for the full state machine.
describe('diffSteps — formula-aware (KAR-900/P2.1)', () => {
  const P = (raw: string) => buildFormulaProvenance(raw)

  it('rows with no formula provenance at all diff exactly as before (no regression)', () => {
    const diffs = diffSteps(mkRow(), mkRow({ fk: 120 }))
    const fk = diffs.find((d) => d.field === 'fk')
    expect(fk?.status).toBe('auffaellig_10') // +20%, unaffected by formula logic
    expect(fk?.formulaFinding).toBeUndefined()
    expect(fk?.formulaInputsChanged).toBeUndefined()
  })

  it('same formula (hash equal), same value -> status unaffected, no formulaInputsChanged flag', () => {
    const altRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 100, formulas: { fk: P('$B$2+D2') } }) // same hash, anchors differ
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('konstant')
    expect(fk?.formulaInputsChanged).toBeUndefined()
  })

  it('same formula (hash equal), different value -> normal band status + formulaInputsChanged', () => {
    const altRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 150, formulas: { fk: P('B2+D2') } })
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('auffaellig_25') // +50%
    expect(fk?.formulaInputsChanged).toBe(true)
    expect(fk?.formulaFinding).toBeUndefined()
  })

  it('formula changed, value unchanged -> status overridden to formel_geaendert (the hidden-risk case)', () => {
    const altRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 100, formulas: { fk: P('B2+E2') } })
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('formel_geaendert')
    expect(fk?.deltaAbsolute).toBe(0)
    expect(fk?.formulaFinding?.kind).toBe('formel_geaendert_wert_gleich')
  })

  it('formula changed, value also changed -> status stays the normal band, formulaFinding carries the detail', () => {
    const altRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 120, formulas: { fk: P('B2+E2') } })
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('auffaellig_10')
    expect(fk?.formulaFinding?.kind).toBe('formel_geaendert_wert_geaendert')
  })

  it('formula removed and replaced by a hardcoded value -> formel_zu_konstante (kritisch)', () => {
    const altRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 999 }) // no formulas at all — plain constant
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('formel_zu_konstante')
    expect(fk?.formulaFinding?.kind).toBe('formel_zu_konstante')
  })

  it('a formula newly added where there was none before is benign — no override', () => {
    const altRow = mkRow({ fk: 100 })
    const neuRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('konstant')
    expect(fk?.formulaFinding).toBeUndefined()
  })

  it('nicht_anwendbar (n.a. marker) takes precedence over a formula-changed override', () => {
    const altRow = mkRow({ fk: null, rawText: { fk: 'n.a.' }, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 100, formulas: { fk: P('B2+E2') } })
    const fk = diffSteps(altRow, neuRow).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('nicht_anwendbar')
  })

  it('formulaEngineEnabled=false disables every formula-derived override', () => {
    const altRow = mkRow({ fk: 100, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 100, formulas: { fk: P('B2+E2') } })
    const fk = diffSteps(altRow, neuRow, DEFAULT_DIFFER_BANDS_CONFIG, false).find((d) => d.field === 'fk')
    expect(fk?.status).toBe('konstant')
    expect(fk?.formulaFinding).toBeUndefined()
  })

  it('only fields with formula provenance are affected — other fields diff normally', () => {
    const altRow = mkRow({ fk: 100, zykluszeit: 10, formulas: { fk: P('B2+D2') } })
    const neuRow = mkRow({ fk: 100, zykluszeit: 12, formulas: { fk: P('B2+E2') } })
    const diffs = diffSteps(altRow, neuRow)
    expect(diffs.find((d) => d.field === 'fk')?.status).toBe('formel_geaendert')
    expect(diffs.find((d) => d.field === 'zykluszeit')?.status).toBe('auffaellig_10') // +20%
  })
})

describe('diffSteps with rawText n.a. markers', () => {
  it('routes a field whose raw cell text is "n.a." to nicht_anwendbar even though the parsed value is null', () => {
    const altRow = mkRow({ fk: null, rawText: { fk: 'n.a.' } })
    const diffs = diffSteps(altRow, mkRow({ fk: 150 }))
    const fk = diffs.find((d) => d.field === 'fk')
    expect(fk?.status).toBe('nicht_anwendbar')
    expect(fk?.deltaAbsolute).toBeNull()
  })

  it('does not affect fields without a rawText n.a. marker', () => {
    const diffs = diffSteps(mkRow(), mkRow({ fk: 120 }))
    const fk = diffs.find((d) => d.field === 'fk')
    expect(fk?.status).not.toBe('nicht_anwendbar')
  })
})
