// KAR-840 PR1: rehydrate persisted qaf_* rows back into a QafExportInput.
import { describe, it, expect } from 'vitest'
import type { QAFRow } from '@/lib/qaf-parser'
import {
  summaryMetricsFromRows,
  partToSummary,
  rehydrateFile,
  rehydrateExportInput,
  resolvePersistedRuleEnforcement,
  resolvePersistedEngineConfig,
  resolveReplaceMultiQafDetectionConfig,
  type PartRow,
  type RehydratedFile,
} from '../rehydrate'
import { DEFAULT_ENGINE_CONFIG } from '../engine-config'

function mkRow(o: Partial<QAFRow> = {}): QAFRow {
  return {
    positionsnummer: '1', teilebenennung: 'Teil', prozessbezeichnung: 'Montage',
    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, ...o,
  }
}

const part: PartRow = {
  part_number: '7490365', part_name: 'Blende', variant: 'A', project_code: 'G60',
  supplier: 'ACME', quotation_date: '2024-01-01', version: 'v2',
}

describe('partToSummary', () => {
  it('mappt qaf_part-Felder auf die 9 Summary-Felder, cell immer null', () => {
    const s = partToSummary(part)
    expect(s.partNumber).toEqual({ value: '7490365', cell: null })
    expect(s.supplier.value).toBe('ACME')
    expect(s.quotationDate.value).toBe('2024-01-01')
    expect(s.requestVersion.value).toBe('v2')
    // nicht persistierte Felder bleiben null (keine Identity-Plausibilität-Fehlalarme)
    expect(s.changeIndex).toEqual({ value: null, cell: null })
    expect(s.supplierNo).toEqual({ value: null, cell: null })
  })

  it('toleriert null-Part (alle Felder null)', () => {
    const s = partToSummary(null)
    expect(s.partNumber.value).toBeNull()
    // KAR-910: QafSummary grew from 9 to 13 fields (peakVolumeYear/
    // productionStartSop/deliverySite/shiftsPerWeek added) — all 4 are "not
    // persisted distinctly" in partToSummary, same as changeIndex/supplierNo.
    // QVS-P4: grew again to 15 (plannedCapacity/lotSize) — same "not
    // persisted distinctly on qaf_part" reasoning, see rehydrate.ts.
    expect(Object.keys(s)).toHaveLength(15)
  })
})

describe('rehydrateFile', () => {
  it('baut ref (id/fileName/quotationDate) + summary + steps', () => {
    const rf: RehydratedFile = {
      file: { id: 'f1', original_file_name: 'alt.xlsx', template_type: 'g60' },
      part,
      steps: [mkRow()],
    }
    const parsed = rehydrateFile(rf)
    expect(parsed.ref).toEqual({ id: 'f1', fileName: 'alt.xlsx', quotationDate: '2024-01-01' })
    expect(parsed.summary.partNumber.value).toBe('7490365')
    expect(parsed.steps).toHaveLength(1)
  })

  it('quotationDate faellt auf null wenn kein Part', () => {
    const parsed = rehydrateFile({
      file: { id: 'f1', original_file_name: 'x.xlsx', template_type: null },
      part: null,
      steps: [],
    })
    expect(parsed.ref.quotationDate).toBeNull()
  })

  it('KAR-886: sourceCells/normalized ueberleben den Rehydrate-Roundtrip verlustfrei (via raw_values)', () => {
    const rowWithProvenance = mkRow({
      sourceCells: { fk: 'Fertigungskosten!Q2' },
      normalized: { fk: 100 },
    })
    const rf: RehydratedFile = {
      file: { id: 'f1', original_file_name: 'alt.xlsx', template_type: null },
      part,
      steps: [rowWithProvenance],
    }
    const parsed = rehydrateFile(rf)
    expect(parsed.steps[0].sourceCells).toEqual({ fk: 'Fertigungskosten!Q2' })
    expect(parsed.steps[0].normalized).toEqual({ fk: 100 })
  })

  it('KAR-886: Zeilen ohne Provenance (pre-KAR-886 raw_values) werden toleriert', () => {
    const legacyRow = mkRow() // keine sourceCells/normalized-Keys
    const parsed = rehydrateFile({
      file: { id: 'f1', original_file_name: 'alt.xlsx', template_type: null },
      part,
      steps: [legacyRow],
    })
    expect(parsed.steps[0].sourceCells).toBeUndefined()
    expect(parsed.steps[0].normalized).toBeUndefined()
    expect(parsed.steps[0].fk).toBe(100)
  })
})

describe('rehydrateExportInput', () => {
  const baseline: RehydratedFile = {
    file: { id: 'old', original_file_name: 'old.xlsx', template_type: 'g60' },
    part,
    steps: [mkRow({ positionsnummer: '2', prozessbezeichnung: 'Schweissen', fk: 200 })],
  }
  const comparison: RehydratedFile = {
    file: { id: 'new', original_file_name: 'new.xlsx', template_type: 'g60' },
    part: { ...part, quotation_date: '2025-01-01' },
    steps: [mkRow({ positionsnummer: '2', prozessbezeichnung: 'Schweissen', fk: 500 })],
  }

  it('re-runt compareQafPair und liefert eine valide QafExportInput', () => {
    const input = rehydrateExportInput({ generatedAtLabel: '2026-07-03', baseline, comparison })
    expect(input.generatedAtLabel).toBe('2026-07-03')
    expect(input.files).toHaveLength(2)
    expect(input.files[0]).toEqual({ fileName: 'old.xlsx', partNumber: '7490365', status: 'parsed' })
    expect(input.comparisons).toHaveLength(1)
    // der FK-Delta 200->500 muss durch die deterministische Engine auftauchen
    const fkDiff = input.comparisons[0].stepComparisons
      .flatMap((s) => s.fieldDiffs)
      .find((d) => d.field === 'fk')
    expect(fkDiff?.deltaAbsolute).toBe(300)
  })

  it('leere Step-Listen ergeben eine leere, aber valide Vergleichsstruktur', () => {
    const empty: RehydratedFile = { file: { id: 'e', original_file_name: 'e.xlsx', template_type: null }, part: null, steps: [] }
    const input = rehydrateExportInput({ generatedAtLabel: 't', baseline: empty, comparison: empty })
    expect(input.comparisons).toHaveLength(1)
    expect(input.comparisons[0].stepComparisons).toEqual([])
  })

  it('plausibilityOverride ersetzt die re-run-Plausibilität (Fidelity-Fix)', () => {
    const persisted = [
      { type: 'variant_changed', severity: 'hinweis' as const, explanation: 'Variante A -> B' },
      { type: 'quotation_date_order', severity: 'pruefen' as const, explanation: 'NEU aelter als ALT' },
    ]
    const input = rehydrateExportInput({
      generatedAtLabel: 't', baseline, comparison, plausibilityOverride: persisted,
    })
    expect(input.comparisons[0].plausibility).toEqual(persisted)
  })

  it('ohne Override bleibt die re-run-Plausibilität erhalten', () => {
    const input = rehydrateExportInput({ generatedAtLabel: 't', baseline, comparison })
    // gleiches qaf_part fuer beide Seiten -> Identity-Checks feuern nicht (dokumentiert)
    expect(Array.isArray(input.comparisons[0].plausibility)).toBe(true)
  })

  // KAR-887/P0.2: reconciliation.ts is wired into compareQafPair via the same
  // `summaryMetrics ? checkReconciliation(...) : []` guard as summaryDiffs — so
  // it inherits the exact same rehydrate behaviour that summaryDiffs already
  // has (see the "returns empty summaryDiffs when no summary metrics were
  // parsed" case in compare.test.ts): rehydrateFile() does not reconstruct
  // summaryMetrics, so a rehydrate-without-override re-run produces zero
  // reconciliation issues instead of crashing — exactly what
  // rehydrateExportInput already relies on plausibilityOverride to paper over
  // for identity plausibility (see the "Fidelity-Fix" test above).
  it('rehydrate-without-override: no summaryMetrics reconstructed -> zero reconciliation issues, no crash', () => {
    const input = rehydrateExportInput({ generatedAtLabel: 't', baseline, comparison })
    expect(input.comparisons[0].plausibility.some((i) => i.type.startsWith('recon_'))).toBe(false)
  })

  it('plausibilityOverride reproduces the exact analyze-time reconciliation issues (deterministic, persisted-issues path)', () => {
    const persistedWithRecon = [
      {
        type: 'recon_herstellkosten_cascade',
        severity: 'kritisch' as const,
        step: 'ALT',
        explanation:
          'Summe Herstellkosten (ALT): Summary weist 900.00 aus, die Gegenrechnung ergibt 700.00 (Delta 200.00) — Abweichung ausserhalb der Toleranz (0.5 % / min. 1).',
      },
    ]
    const input = rehydrateExportInput({
      generatedAtLabel: 't',
      baseline,
      comparison,
      plausibilityOverride: persistedWithRecon,
    })
    expect(input.comparisons[0].plausibility).toEqual(persistedWithRecon)
  })
})

// Reviewer finding (KAR-889 adversarial verification-review, "Rest-Lücke im
// Export/Rehydrate-Pfad"): rehydrateExportInput re-runs compareQafPair
// without a ruleEngineConfig, so an export used the LIVE RULE_ENGINE_CONFIG
// at export time instead of the mode the comparison was actually persisted
// under — a later flip of the global default would silently change an old
// comparison's exported deltas while the on-screen detail page (which reads
// persisted qaf_manufacturing_diff rows, not a re-run) stays correct.
describe('resolvePersistedRuleEnforcement', () => {
  it('reads an explicit block from the persisted engine_version', () => {
    expect(resolvePersistedRuleEnforcement({ ruleEnforcement: 'block' })).toBe('block')
  })

  it('falls back to warn when the key is absent (legacy comparisons pre-KAR-889)', () => {
    expect(resolvePersistedRuleEnforcement({ parser: '1.1.0' })).toBe('warn')
  })

  it('falls back to warn for null/undefined/garbage — never silently upgrades to block', () => {
    expect(resolvePersistedRuleEnforcement(null)).toBe('warn')
    expect(resolvePersistedRuleEnforcement(undefined)).toBe('warn')
    expect(resolvePersistedRuleEnforcement({ ruleEnforcement: 'yolo' })).toBe('warn')
  })
})

describe('rehydrateExportInput — respects the persisted ruleEnforcement mode (KAR-889 reviewer finding)', () => {
  const baselineBlock: RehydratedFile = {
    file: { id: 'old', original_file_name: 'old.xlsx', template_type: 'g60' },
    part,
    steps: [mkRow({ positionsnummer: '1', prozessbezeichnung: 'Montage', fkAW: 100 })],
  }
  const comparisonBlock: RehydratedFile = {
    file: { id: 'new', original_file_name: 'new.xlsx', template_type: 'g60' },
    part,
    steps: [mkRow({ positionsnummer: '1', prozessbezeichnung: 'Montage', fkAW: null })], // mandatory field genuinely blank
  }

  it('(a) persisted block mode exports a blocked delta, independent of the live global default (warn)', () => {
    const input = rehydrateExportInput({
      generatedAtLabel: 't',
      baseline: baselineBlock,
      comparison: comparisonBlock,
      engineVersion: { parser: '1.1.0', ruleEnforcement: 'block' },
    })
    const fkAWDiff = input.comparisons[0].stepComparisons.flatMap((s) => s.fieldDiffs).find((d) => d.field === 'fkAW')
    expect(fkAWDiff?.status).toBe('blockiert')
  })

  it('(b) legacy comparison without the ruleEnforcement key falls back to warn (no blocked status)', () => {
    const input = rehydrateExportInput({
      generatedAtLabel: 't',
      baseline: baselineBlock,
      comparison: comparisonBlock,
      engineVersion: { parser: '1.1.0' }, // pre-KAR-889 shape, no ruleEnforcement key
    })
    const fkAWDiff = input.comparisons[0].stepComparisons.flatMap((s) => s.fieldDiffs).find((d) => d.field === 'fkAW')
    expect(fkAWDiff?.status).not.toBe('blockiert')
  })

  it('missing engineVersion entirely also falls back to warn (backward-compatible default)', () => {
    const input = rehydrateExportInput({ generatedAtLabel: 't', baseline: baselineBlock, comparison: comparisonBlock })
    const fkAWDiff = input.comparisons[0].stepComparisons.flatMap((s) => s.fieldDiffs).find((d) => d.field === 'fkAW')
    expect(fkAWDiff?.status).not.toBe('blockiert')
  })
})

// P1.5/KAR-896: resolvePersistedEngineConfig generalizes
// resolvePersistedRuleEnforcement to every config section, and
// rehydrateExportInput now re-runs compareQafPair under the FULL
// reconstructed config (not just ruleEnforcement) — so a persisted
// differBands/reconciliation/matching override also survives an export
// re-run, not only the rule-engine mode.
describe('resolvePersistedEngineConfig (KAR-896)', () => {
  it('falls back to DEFAULT_ENGINE_CONFIG for null/undefined/garbage engine_version', () => {
    expect(resolvePersistedEngineConfig(null)).toEqual(DEFAULT_ENGINE_CONFIG)
    expect(resolvePersistedEngineConfig(undefined)).toEqual(DEFAULT_ENGINE_CONFIG)
    expect(resolvePersistedEngineConfig('garbage')).toEqual(DEFAULT_ENGINE_CONFIG)
  })

  it('reconstructs a persisted section override from engineConfigOverrides, leaves other sections at default', () => {
    const cfg = resolvePersistedEngineConfig({
      configVersion: '1.0.0',
      engineConfigOverrides: { reconciliation: { relativeTolerance: 0.02, absoluteToleranceMinor: 5 } },
    })
    expect(cfg.reconciliation).toEqual({ relativeTolerance: 0.02, absoluteToleranceMinor: 5 })
    expect(cfg.matching).toEqual(DEFAULT_ENGINE_CONFIG.matching)
    expect(cfg.differBands).toEqual(DEFAULT_ENGINE_CONFIG.differBands)
  })

  // KAR-926 adversarial-review F1 fix (12.07.2026): general
  // resolvePersistedEngineConfig round-trip for a persisted multiQafDetection
  // override — used by export/rehydrate reproduction and by
  // resolveReplaceMultiQafDetectionConfig below (see the dedicated describe
  // block below that one) when an explicit override IS present. Since
  // KAR-925 (13.07.2026), replaceComparisonFile (file-replace-actions.ts) no longer calls
  // this function directly for the no-override case — see
  // resolveReplaceMultiQafDetectionConfig's own tests for why.
  it('reconstructs a persisted multiQafDetection override from engineConfigOverrides, leaves other sections at default', () => {
    const cfg = resolvePersistedEngineConfig({
      configVersion: '1.3.0',
      engineConfigOverrides: {
        multiQafDetection: { ...DEFAULT_ENGINE_CONFIG.multiQafDetection, enabled: true, strongMaterialFormulaColumnCount: 20 },
      },
    })
    expect(cfg.multiQafDetection.enabled).toBe(true)
    expect(cfg.multiQafDetection.strongMaterialFormulaColumnCount).toBe(20)
    // Untouched threshold fields fall through from the override object itself
    // (a full MultiQafDetectionConfig was passed, same as every real caller
    // would persist via buildEngineConfigDelta) — reconciliation/matching
    // stay at their own defaults, unaffected.
    expect(cfg.reconciliation).toEqual(DEFAULT_ENGINE_CONFIG.reconciliation)
    expect(cfg.matching).toEqual(DEFAULT_ENGINE_CONFIG.matching)
  })

  it('a legacy bare ruleEnforcement key (pre-KAR-896 KAR-889 shape) resolves via ruleEngine.ruleEnforcement', () => {
    expect(resolvePersistedEngineConfig({ ruleEnforcement: 'block' }).ruleEngine.ruleEnforcement).toBe('block')
    expect(resolvePersistedEngineConfig({ ruleEnforcement: 'yolo' }).ruleEngine.ruleEnforcement).toBe('warn')
  })

  it('resolvePersistedRuleEnforcement stays a thin, behavior-preserving wrapper', () => {
    expect(resolvePersistedRuleEnforcement({ ruleEnforcement: 'block' })).toBe(
      resolvePersistedEngineConfig({ ruleEnforcement: 'block' }).ruleEngine.ruleEnforcement,
    )
  })
})

// KAR-925 adversarial-review F1 fix (13.07.2026): replaceComparisonFile
// (actions.ts) used to read resolvePersistedEngineConfig(cmp.engine_version)
// .multiQafDetection directly — safe while DEFAULT_ENGINE_CONFIG.
// multiQafDetection.enabled was false (KAR-926), but once KAR-925 flipped
// that default to true, the SAME fallback silently activated the detector
// for every pre-flip comparison the instant its file got replaced (no
// comparison persists a multiQafDetection override, so every one of them hit
// the "fall back to CURRENT default" branch) — an old comparison could
// suddenly have a legitimate resubmission rejected+deleted by the role-kind
// guard, something that could never happen before this PR. This violates
// rehydrate.ts's own documented invariant: "a missing key must never
// silently upgrade an old comparison to a stricter mode."
// resolveReplaceMultiQafDetectionConfig fixes this by reading the
// comparison's own `configVersion` stamp instead of jumping straight to the
// current default.
describe('resolveReplaceMultiQafDetectionConfig (KAR-925 adversarial-review F1 fix)', () => {
  it('a pre-flip comparison (configVersion 1.3.0, no override) replays enabled:false — the value in effect when it was created', () => {
    const cfg = resolveReplaceMultiQafDetectionConfig({ configVersion: '1.3.0' })
    expect(cfg.enabled).toBe(false)
    // Thresholds still come from the current default — only `enabled` is
    // pinned to the historical value.
    expect(cfg.strongVariantColumnPopulation).toBe(DEFAULT_ENGINE_CONFIG.multiQafDetection.strongVariantColumnPopulation)
  })

  it('a comparison with no recognizable configVersion stamp at all (null/undefined/garbage/missing key) also replays enabled:false', () => {
    expect(resolveReplaceMultiQafDetectionConfig(null).enabled).toBe(false)
    expect(resolveReplaceMultiQafDetectionConfig(undefined).enabled).toBe(false)
    expect(resolveReplaceMultiQafDetectionConfig('garbage').enabled).toBe(false)
    expect(resolveReplaceMultiQafDetectionConfig({}).enabled).toBe(false)
    // The G60-insert literal shape (types.ts ENGINE_VERSION.configVersion is
    // a permanently-unbumped '1.0.0') falls into the same safe branch.
    expect(resolveReplaceMultiQafDetectionConfig({ configVersion: '1.0.0', baselineStatus: 'baseline_review' }).enabled).toBe(
      false,
    )
  })

  it('a comparison stamped at exactly 1.4.0 or later inherits the current (enabled:true) default — a genuine no-op, not an upgrade', () => {
    expect(resolveReplaceMultiQafDetectionConfig({ configVersion: '1.4.0' }).enabled).toBe(true)
    expect(resolveReplaceMultiQafDetectionConfig({ configVersion: '1.5.0' }).enabled).toBe(true)
    expect(resolveReplaceMultiQafDetectionConfig({ configVersion: '2.0.0' }).enabled).toBe(true)
  })

  it('an explicit engineConfigOverrides.multiQafDetection always wins, regardless of configVersion', () => {
    const overrideEnabled = resolveReplaceMultiQafDetectionConfig({
      configVersion: '1.3.0',
      engineConfigOverrides: { multiQafDetection: { ...DEFAULT_ENGINE_CONFIG.multiQafDetection, enabled: true } },
    })
    expect(overrideEnabled.enabled).toBe(true)

    const overrideDisabled = resolveReplaceMultiQafDetectionConfig({
      configVersion: '1.4.0',
      engineConfigOverrides: { multiQafDetection: { ...DEFAULT_ENGINE_CONFIG.multiQafDetection, enabled: false } },
    })
    expect(overrideDisabled.enabled).toBe(false)
  })

  it('never mutates DEFAULT_ENGINE_CONFIG.multiQafDetection itself', () => {
    resolveReplaceMultiQafDetectionConfig({ configVersion: '1.3.0' })
    expect(DEFAULT_ENGINE_CONFIG.multiQafDetection.enabled).toBe(true)
  })
})

describe('rehydrateExportInput — respects a persisted differBands override (KAR-896)', () => {
  it('a persisted differBands override changes the exported delta status, independent of the live default', () => {
    const wideBaseline: RehydratedFile = {
      file: { id: 'old', original_file_name: 'old.xlsx', template_type: 'g60' },
      part,
      steps: [mkRow({ positionsnummer: '1', prozessbezeichnung: 'Montage', fk: 100 })],
    }
    const wideComparison: RehydratedFile = {
      file: { id: 'new', original_file_name: 'new.xlsx', template_type: 'g60' },
      part,
      steps: [mkRow({ positionsnummer: '1', prozessbezeichnung: 'Montage', fk: 122 })], // +22%
    }
    const withDefault = rehydrateExportInput({
      generatedAtLabel: 't',
      baseline: wideBaseline,
      comparison: wideComparison,
    })
    expect(
      withDefault.comparisons[0].stepComparisons.flatMap((s) => s.fieldDiffs).find((d) => d.field === 'fk')?.status,
    ).toBe('auffaellig_10')

    const withOverride = rehydrateExportInput({
      generatedAtLabel: 't',
      baseline: wideBaseline,
      comparison: wideComparison,
      engineVersion: {
        configVersion: '1.0.0',
        engineConfigOverrides: { differBands: { auffaellig10: 0.3, auffaellig25: 0.4, kritisch50: 0.6 } },
      },
    })
    expect(
      withOverride.comparisons[0].stepComparisons.flatMap((s) => s.fieldDiffs).find((d) => d.field === 'fk')?.status,
    ).toBe('anstieg')
  })
})

describe('summaryMetricsFromRows (KAR-845 re-compare)', () => {
  it('rebuilds metrics with values, currency and conservative locate metadata', () => {
    const parse = summaryMetricsFromRows(
      [
        { metric_key: 'quotationPrice', value: 3.78, currency: 'EUR', source_cell: 'C26' },
        { metric_key: 'materialCosts', value: 0.76, currency: null, source_cell: 'C10' },
      ],
      'BMW_SUMMARY_LEGACY',
    )
    expect(parse.metrics.quotationPrice).toMatchObject({ value: 3.78, cell: 'C26', confidence: 0.6 })
    expect(parse.metrics.materialCosts.value).toBe(0.76)
    expect(parse.metrics.manufacturingCosts.value).toBeNull()
    expect(parse.currency).toBe('EUR')
    expect(parse.template).toBe('BMW_SUMMARY_LEGACY')
  })
})
