// KAR-922 / P6.2 — consolidated Zelle→Ergebnis Explain-Panel provenance.
// Per-module adapter tests (synthetic fixtures, no real BMW data) + // allow-customer-string
// integration test asserting a full result value's §17 attributes come out
// correctly merged. Tri-State discipline: every not-present attribute must
// carry an explicit 'not_applicable' or 'not_captured' status, never a bare
// null with no explanation.

import { describe, expect, it } from 'vitest'
import {
  buildDetailSideAttributes,
  buildManufacturingExplainAttributes,
  buildMaterialExplainAttributes,
  buildSbmExplainAttributes,
  buildRmrExplainAttributes,
  buildLogisticsExplainAttributes,
  buildSummaryExplainAttributes,
  buildQafGuideRequirement,
  type ExplainAttributes,
} from '../explain-provenance'
import { QAF_FIELD_KEY_TO_CANONICAL } from '../canonical-fields'

// A canonical id known to resolve in the registry (MANUFACTURING module,
// mandatory field) — used to exercise the 'present' path of
// buildQafGuideRequirement without hand-rolling a CanonicalField.
const KNOWN_CANONICAL_ID = QAF_FIELD_KEY_TO_CANONICAL.positionsnummer

function assertNotFabricated(attrs: ExplainAttributes) {
  // Every attribute must be one of the three known statuses, and a
  // non-present attribute must always carry SOME explanation (note or, for
  // engineVersion/timestamp, an implicit one) — this is a structural
  // Tri-State-Disziplin smoke check reused by several tests below.
  const flat: Array<{ status: string; value: unknown; note?: string }> = [
    attrs.canonicalFieldId,
    attrs.originalLabelDe,
    attrs.originalLabelEn,
    attrs.qafGuideRequirement,
    attrs.calculatedDelta,
    attrs.comparisonRule,
    attrs.validationResult,
    attrs.engineVersion,
    attrs.timestamp,
    ...Object.values(attrs.alt),
    ...Object.values(attrs.neu),
  ]
  for (const a of flat) {
    expect(['present', 'not_applicable', 'not_captured']).toContain(a.status)
    if (a.status === 'present') {
      expect(a.value).not.toBeNull()
    } else {
      expect(a.value).toBeNull()
    }
  }
}

describe('buildQafGuideRequirement', () => {
  it('resolves a known canonical id to its requirement enum key + evidence source (KAR-924/Teil 1: enum key, not a pre-rendered German sentence)', () => {
    const r = buildQafGuideRequirement(KNOWN_CANONICAL_ID)
    expect(r.status).toBe('present')
    expect(['mandatory', 'conditional', 'optional']).toContain(r.value?.requirement)
    expect(typeof r.value?.evidence === 'string' || r.value?.evidence === undefined).toBe(true)
  })

  it('returns not_applicable for an undefined canonical id (field not in the registry)', () => {
    const r = buildQafGuideRequirement(undefined)
    expect(r.status).toBe('not_applicable')
  })

  it('returns not_captured for a canonical id that does not resolve in the registry', () => {
    const r = buildQafGuideRequirement('mfg_does_not_exist')
    expect(r.status).toBe('not_captured')
  })
})

describe('buildDetailSideAttributes (generic row-module core)', () => {
  it('returns not_applicable for every field when the row itself is null (one-sided draft)', () => {
    const side = buildDetailSideAttributes({
      fileName: null,
      sheetName: 'Fertigungskosten',
      row: null,
      fieldKey: 'zykluszeit',
      moduleConfidence: null,
      templateProfile: null,
    })
    expect(side.sourceCell.status).toBe('not_applicable')
    expect(side.rawValue.status).toBe('not_applicable')
    expect(side.formula.status).toBe('not_applicable')
  })

  it('reads sourceCell/normalizedValue/confidence/templateProfile straight from the row when present', () => {
    const side = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: { sourceCells: { zykluszeit: 'Fertigungskosten!D15' }, normalized: { zykluszeit: 12.5 }, rawText: {} },
      fieldKey: 'zykluszeit',
      moduleConfidence: 0.95,
      templateProfile: 'QAF_V9_SUMMARY (known)',
    })
    expect(side.sourceFile).toEqual({ status: 'present', value: 'ALT.xlsx' })
    expect(side.sourceCell).toEqual({ status: 'present', value: 'Fertigungskosten!D15' })
    expect(side.normalizedValue).toEqual({ status: 'present', value: '12.5' })
    expect(side.confidence).toEqual({ status: 'present', value: 0.95 })
    expect(side.templateProfileVersion).toEqual({ status: 'present', value: 'QAF_V9_SUMMARY (known)' })
    expect(side.mappingMethod).toEqual({ status: 'present', value: 'automatic_header_mapping' })
  })

  it('marks sourceCell/normalizedValue not_captured (not not_applicable) when the row predates KAR-886 (no sourceCells at all)', () => {
    const side = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: { zykluszeit: 12 } as never,
      fieldKey: 'zykluszeit',
      moduleConfidence: null,
      templateProfile: null,
    })
    expect(side.sourceCell.status).toBe('not_captured')
    expect(side.sourceCell.note?.de).toMatch(/älterer Lauf|nicht im Header/)
    expect(side.sourceCell.note?.en).toMatch(/older run|not found in the header/)
    expect(side.normalizedValue.status).toBe('not_captured')
  })

  it('rawValue is present from rawText for a non-numeric text override ("n.a."), not_captured when only normalized exists', () => {
    const withRawText = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: { sourceCells: { ausschuss: 'Fertigungskosten!R15' }, normalized: {}, rawText: { ausschuss: 'n.a.' } },
      fieldKey: 'ausschuss',
      moduleConfidence: 0.9,
      templateProfile: null,
    })
    expect(withRawText.rawValue).toEqual({ status: 'present', value: 'n.a.' })

    const numericOnly = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: { sourceCells: { ausschuss: 'Fertigungskosten!R15' }, normalized: { ausschuss: 3 }, rawText: {} },
      fieldKey: 'ausschuss',
      moduleConfidence: 0.9,
      templateProfile: null,
    })
    expect(numericOnly.rawValue.status).toBe('not_captured')
    expect(numericOnly.normalizedValue).toEqual({ status: 'present', value: '3' })
  })

  it('formula is present when the field carried a resolvable formula, not_captured when unresolved, not_applicable when none', () => {
    const withFormula = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: {
        sourceCells: { fkAW: 'Fertigungskosten!S15' },
        normalized: { fkAW: 100 },
        rawText: {},
        formulas: { fkAW: { raw: '=S15*T15', normalized: 'SUM(...)', hash: 'abc' } },
      },
      fieldKey: 'fkAW',
      moduleConfidence: 1,
      templateProfile: null,
    })
    expect(withFormula.formula).toEqual({ status: 'present', value: { raw: '=S15*T15', normalized: 'SUM(...)' } })

    const unresolved = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: {
        sourceCells: { fkAW: 'Fertigungskosten!S15' },
        normalized: { fkAW: 100 },
        rawText: {},
        formulas: { fkAW: { raw: '', normalized: '', hash: '', unresolved: true } },
      },
      fieldKey: 'fkAW',
      moduleConfidence: 1,
      templateProfile: null,
    })
    expect(unresolved.formula.status).toBe('not_captured')

    const noFormula = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: { sourceCells: { fkAW: 'Fertigungskosten!S15' }, normalized: { fkAW: 100 }, rawText: {} },
      fieldKey: 'fkAW',
      moduleConfidence: 1,
      templateProfile: null,
    })
    expect(noFormula.formula.status).toBe('not_applicable')
  })

  it('mappingMethod is "manuell zugeordnet" when a field-mapping override is in effect', () => {
    const side = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: {
        sourceCells: {},
        normalized: { lohnkosten: 42 },
        rawText: {},
        manualOverride: { lohnkosten: { sourceDescription: 'Telefonat Kunde', setBy: 'user-1', setAt: '2026-07-01T10:00:00Z' } },
      },
      fieldKey: 'lohnkosten',
      moduleConfidence: 0.8,
      templateProfile: null,
    })
    expect(side.mappingMethod).toEqual({ status: 'present', value: 'manual_override' })
  })

  it('unitConversion/currencyConversion are always not_applicable with an explanatory note (engine performs none)', () => {
    const side = buildDetailSideAttributes({
      fileName: 'ALT.xlsx',
      sheetName: 'Fertigungskosten',
      row: { sourceCells: { zykluszeit: 'X!A1' }, normalized: { zykluszeit: 1 }, rawText: {} },
      fieldKey: 'zykluszeit',
      moduleConfidence: 1,
      templateProfile: null,
    })
    expect(side.unitConversion.status).toBe('not_applicable')
    expect(side.currencyConversion.status).toBe('not_applicable')
    expect(side.unitConversion.note?.de).toMatch(/keine Einheiten-\/Währungsumrechnung/)
    expect(side.unitConversion.note?.en).toMatch(/no unit\/currency conversion/)
  })
})

describe('buildManufacturingExplainAttributes (module adapter)', () => {
  const baseInput = {
    fieldKey: 'zykluszeit' as const,
    alt: {
      fileName: 'ALT.xlsx',
      row: { sourceCells: { zykluszeit: 'Fertigungskosten!D15' }, normalized: { zykluszeit: 12 }, rawText: {} },
      moduleConfidence: 1,
      templateProfile: 'QAF_V9_SUMMARY (known)',
    },
    neu: {
      fileName: 'NEU.xlsx',
      row: { sourceCells: { zykluszeit: 'Fertigungskosten!D15' }, normalized: { zykluszeit: 14 }, rawText: {} },
      moduleConfidence: 1,
      templateProfile: 'QAF_V9_SUMMARY (known)',
    },
    diff: { deltaAbsolute: 2, deltaPercent: 0.1667, status: 'auffaellig_10' },
    engineVersion: { parser: '1.3.0', differ: '1.1.0' },
    timestamp: '2026-07-10T09:00:00Z',
  }

  it('merges canonical id, requirement, both sides and the diff-level attributes into one ExplainAttributes', () => {
    const attrs = buildManufacturingExplainAttributes(baseInput)
    expect(attrs.canonicalFieldId).toEqual({ status: 'present', value: QAF_FIELD_KEY_TO_CANONICAL.zykluszeit })
    expect(attrs.originalLabelDe.status).toBe('present')
    expect(attrs.alt.sourceCell).toEqual({ status: 'present', value: 'Fertigungskosten!D15' })
    expect(attrs.neu.sourceCell).toEqual({ status: 'present', value: 'Fertigungskosten!D15' })
    expect(attrs.calculatedDelta).toEqual({ status: 'present', value: 'Δ abs 2 · Δ % 16.7%' })
    expect(attrs.comparisonRule).toEqual({
      status: 'present',
      value: { de: 'Status-Band-Vergleich (auffaellig_10)', en: 'Status-band comparison (auffaellig_10)' },
    })
    expect(attrs.validationResult).toEqual({ status: 'present', value: 'auffaellig_10' })
    expect(attrs.engineVersion).toEqual({ status: 'present', value: { parser: '1.3.0', differ: '1.1.0' } })
    expect(attrs.timestamp).toEqual({ status: 'present', value: '2026-07-10T09:00:00Z' })
    assertNotFabricated(attrs)
  })

  it('prefers a rule-engine finding over the plain status-band label for comparisonRule when one is linked to this field', () => {
    const attrs = buildManufacturingExplainAttributes({
      ...baseInput,
      diff: { deltaAbsolute: null, deltaPercent: null, status: 'blockiert' },
      ruleFinding: { rule: 'R2 Pflichtfeld-Blockade', explanation: 'Pflichtfeld ist ungültig/leer.' },
    })
    // No explanationEn supplied — honest fallback to the DE text on both
    // sides (same tolerance rule decodeBilingual itself applies to legacy
    // DE-only issues), never a crash or a fabricated translation.
    expect(attrs.comparisonRule).toEqual({
      status: 'present',
      value: {
        de: 'R2 Pflichtfeld-Blockade: Pflichtfeld ist ungültig/leer.',
        en: 'R2 Pflichtfeld-Blockade: Pflichtfeld ist ungültig/leer.',
      },
    })
    expect(attrs.calculatedDelta.status).toBe('not_applicable')
  })

  it('KAR-924/Teil 1: carries a supplied explanationEn through into comparisonRule.value.en, never overwriting it with the German text (the ruleFindingFor ".de" fixation bug this task closes)', () => {
    const attrs = buildManufacturingExplainAttributes({
      ...baseInput,
      diff: { deltaAbsolute: null, deltaPercent: null, status: 'blockiert' },
      ruleFinding: {
        rule: 'R2 Pflichtfeld-Blockade',
        explanation: 'Pflichtfeld ist ungültig/leer.',
        explanationEn: 'Mandatory field is invalid/empty.',
      },
    })
    expect(attrs.comparisonRule).toEqual({
      status: 'present',
      value: {
        de: 'R2 Pflichtfeld-Blockade: Pflichtfeld ist ungültig/leer.',
        en: 'R2 Pflichtfeld-Blockade: Mandatory field is invalid/empty.',
      },
    })
  })

  it('leaves comparisonRule/validationResult not_applicable when there is no diff at all (e.g. new/removed step)', () => {
    const attrs = buildManufacturingExplainAttributes({ ...baseInput, diff: null })
    expect(attrs.comparisonRule.status).toBe('not_applicable')
    expect(attrs.validationResult.status).toBe('not_applicable')
  })

  it('marks engineVersion/timestamp not_captured for a pre-versioning comparison instead of fabricating a value', () => {
    const attrs = buildManufacturingExplainAttributes({ ...baseInput, engineVersion: null, timestamp: null })
    expect(attrs.engineVersion.status).toBe('not_captured')
    expect(attrs.timestamp.status).toBe('not_captured')
  })
})

describe('buildMaterialExplainAttributes / buildSbmExplainAttributes / buildRmrExplainAttributes / buildLogisticsExplainAttributes (module adapters)', () => {
  it('MATERIAL: resolves the canonical id via MATERIAL_FIELD_KEY_TO_CANONICAL and reads the MaterialRow shape', () => {
    const attrs = buildMaterialExplainAttributes({
      fieldKey: 'materialCost',
      alt: {
        fileName: 'ALT.xlsx',
        row: { sourceCells: { materialCost: 'MATERIAL!P5' }, normalized: { materialCost: 10 }, rawText: {} },
        moduleConfidence: 0.9,
        templateProfile: null,
      },
      neu: { fileName: null, row: null, moduleConfidence: null, templateProfile: null },
      diff: null,
      engineVersion: null,
      timestamp: null,
    })
    expect(attrs.alt.sourceSheet).toEqual({ status: 'present', value: 'MATERIAL' })
    expect(attrs.neu.sourceFile.status).toBe('not_applicable')
    assertNotFabricated(attrs)
  })

  it('SBM: resolves via SBM_FIELD_KEY_TO_CANONICAL and reads the SbmRow shape', () => {
    const attrs = buildSbmExplainAttributes({
      fieldKey: 'positionNumber',
      alt: { fileName: 'ALT.xlsx', row: { sourceCells: { positionNumber: 'SBM!A5' }, normalized: {}, rawText: {} }, moduleConfidence: 1, templateProfile: null },
      neu: { fileName: 'NEU.xlsx', row: { sourceCells: { positionNumber: 'SBM!A5' }, normalized: {}, rawText: {} }, moduleConfidence: 1, templateProfile: null },
      diff: { deltaAbsolute: null, deltaPercent: null, status: 'konstant' },
      engineVersion: {},
      timestamp: '2026-07-10T09:00:00Z',
    })
    expect(attrs.alt.sourceSheet).toEqual({ status: 'present', value: 'SBM-DEVICES-FWZ' })
    assertNotFabricated(attrs)
  })

  it('RMR: resolves via RMR_FIELD_KEY_TO_CANONICAL and reads the RmrRow shape', () => {
    const attrs = buildRmrExplainAttributes({
      fieldKey: 'positionNumber',
      alt: { fileName: 'ALT.xlsx', row: { sourceCells: {}, normalized: {}, rawText: {} }, moduleConfidence: 0.6, templateProfile: null },
      neu: { fileName: 'ALT.xlsx', row: { sourceCells: {}, normalized: {}, rawText: {} }, moduleConfidence: 0.6, templateProfile: null },
      diff: null,
      engineVersion: null,
      timestamp: null,
    })
    expect(attrs.alt.sourceSheet).toEqual({ status: 'present', value: 'RAW MATERIAL RISKS' })
    assertNotFabricated(attrs)
  })

  it('LOGISTICS: resolves via LOG_FIELD_KEY_TO_CANONICAL and reads the LogisticsRow shape', () => {
    const attrs = buildLogisticsExplainAttributes({
      fieldKey: 'positionNumber',
      alt: { fileName: 'ALT.xlsx', row: { sourceCells: {}, normalized: {}, rawText: {} }, moduleConfidence: 0.5, templateProfile: null },
      neu: { fileName: 'ALT.xlsx', row: { sourceCells: {}, normalized: {}, rawText: {} }, moduleConfidence: 0.5, templateProfile: null },
      diff: null,
      engineVersion: null,
      timestamp: null,
    })
    expect(attrs.alt.sourceSheet).toEqual({ status: 'present', value: 'LOGISTICS & CUSTOM' })
    assertNotFabricated(attrs)
  })
})

describe('buildSummaryExplainAttributes (module adapter, distinct row shape)', () => {
  it('merges a full SUMMARY metric result — present values on both sides, honest not_captured for formula/confidence/mappingMethod', () => {
    const attrs = buildSummaryExplainAttributes({
      metricKey: 'manufacturingCosts',
      alt: { fileName: 'ALT.xlsx', cell: 'Zusammenfassung!I10', value: 100, currency: 'EUR', templateProfile: 'QAF_V9_SUMMARY (known)' },
      neu: { fileName: 'NEU.xlsx', cell: 'Zusammenfassung!I10', value: 120, currency: 'EUR', templateProfile: 'QAF_V9_SUMMARY (known)' },
      deltaAbsolute: 20,
      deltaPercent: 0.2,
      status: 'auffaellig_25',
      engineVersion: { parser: '1.3.0' },
      timestamp: '2026-07-10T09:00:00Z',
    })
    expect(attrs.canonicalFieldId.status).toBe('present')
    expect(attrs.alt.sourceCell).toEqual({ status: 'present', value: 'Zusammenfassung!I10' })
    expect(attrs.alt.normalizedValue).toEqual({ status: 'present', value: '100 EUR' })
    // Formula/confidence/mappingMethod are genuinely not persisted for SUMMARY
    // (see summary-metrics.ts SummaryMetricsParse doc + persistence-mapper.ts
    // SummaryMetricRow/SummaryDiffRow shape) — must read not_captured, not
    // silently absent or fabricated as not_applicable.
    expect(attrs.alt.formula.status).toBe('not_captured')
    expect(attrs.alt.confidence.status).toBe('not_captured')
    expect(attrs.alt.mappingMethod.status).toBe('not_captured')
    expect(attrs.calculatedDelta).toEqual({ status: 'present', value: 'Δ abs 20 · Δ % 20.0%' })
    assertNotFabricated(attrs)
  })

  it('one-sided (no NEU file yet): NEU side is not_applicable throughout, not fabricated as not_captured', () => {
    const attrs = buildSummaryExplainAttributes({
      metricKey: 'totalCosts',
      alt: { fileName: 'ALT.xlsx', cell: 'Zusammenfassung!I20', value: 500, currency: 'EUR', templateProfile: null },
      neu: { fileName: null, cell: null, value: null, currency: null, templateProfile: null },
      deltaAbsolute: null,
      deltaPercent: null,
      status: null,
      engineVersion: null,
      timestamp: null,
    })
    expect(attrs.neu.sourceFile.status).toBe('not_applicable')
    expect(attrs.calculatedDelta.status).toBe('not_applicable')
    expect(attrs.comparisonRule.status).toBe('not_applicable')
    assertNotFabricated(attrs)
  })
})

describe('integration: one full result value round-trips every §17 attribute correctly merged', () => {
  it('a complete MANUFACTURING result carries source file/sheet/cell/label/canonical id/raw/normalized/formula/rule/method/confidence/delta/validation/requirement/engine-version/profile/timestamp', () => {
    const attrs = buildManufacturingExplainAttributes({
      fieldKey: 'fkAW',
      alt: {
        fileName: 'ALT_QAF.xlsx',
        row: {
          sourceCells: { fkAW: 'Fertigungskosten!S15' },
          normalized: { fkAW: 100 },
          rawText: {},
          formulas: { fkAW: { raw: '=S14+S13', normalized: 'S14+S13', hash: 'h1' } },
        },
        moduleConfidence: 0.95,
        templateProfile: 'QAF_V9_SUMMARY (known)',
      },
      neu: {
        fileName: 'NEU_QAF.xlsx',
        row: {
          sourceCells: { fkAW: 'Fertigungskosten!S15' },
          normalized: { fkAW: 130 },
          rawText: {},
          formulas: { fkAW: { raw: '=S14+S13', normalized: 'S14+S13', hash: 'h1' } },
        },
        moduleConfidence: 0.95,
        templateProfile: 'QAF_V9_SUMMARY (known)',
      },
      diff: { deltaAbsolute: 30, deltaPercent: 0.3, status: 'kritisch_50' },
      ruleFinding: null,
      engineVersion: { parser: '1.3.0', differ: '1.1.0', ruleEngine: '1.1.0' },
      timestamp: '2026-07-10T12:00:00Z',
    })

    // Shared attributes
    expect(attrs.canonicalFieldId.status).toBe('present')
    expect(attrs.originalLabelDe.status).toBe('present')
    expect(attrs.originalLabelEn.status).toBe('present')
    expect(attrs.qafGuideRequirement.status).toBe('present')
    expect(attrs.calculatedDelta).toEqual({ status: 'present', value: 'Δ abs 30 · Δ % 30.0%' })
    expect(attrs.comparisonRule.status).toBe('present')
    expect(attrs.validationResult).toEqual({ status: 'present', value: 'kritisch_50' })
    expect(attrs.engineVersion.status).toBe('present')
    expect(attrs.timestamp.status).toBe('present')

    // Per-side attributes, both ALT and NEU
    for (const side of [attrs.alt, attrs.neu] as const) {
      expect(side.sourceFile.status).toBe('present')
      expect(side.sourceSheet).toEqual({ status: 'present', value: 'Fertigungskosten' })
      expect(side.sourceCell).toEqual({ status: 'present', value: 'Fertigungskosten!S15' })
      expect(side.normalizedValue.status).toBe('present')
      expect(side.formula).toEqual({ status: 'present', value: { raw: '=S14+S13', normalized: 'S14+S13' } })
      expect(side.mappingMethod.status).toBe('present')
      expect(side.confidence).toEqual({ status: 'present', value: 0.95 })
      expect(side.templateProfileVersion.status).toBe('present')
      // Honest structural gaps, never fabricated:
      expect(side.unitConversion.status).toBe('not_applicable')
      expect(side.currencyConversion.status).toBe('not_applicable')
      expect(side.rawValue.status).toBe('not_captured')
    }

    assertNotFabricated(attrs)
  })
})
