// KAR-922/P6.2 — pure helpers extracted from qaf-explain-panel.tsx (the JSX
// component itself is tdd-guard:skip, same as qaf-provenance.tsx — no RTL in
// this repo, see qaf-provenance.test.ts for the established pattern).

import { describe, expect, it } from 'vitest'
import {
  buildExplainRows,
  fmtExplainTimestamp,
  formatExplainBilingual,
  formatExplainEngineVersion,
  formatExplainFormula,
  formatExplainMappingMethod,
  formatExplainNumber,
  formatExplainQafGuideRequirement,
  formatExplainString,
  formatExplainValue,
} from '../qaf-explain-panel'
import type { ExplainAttributes } from '@/lib/qaf-differences'

describe('formatExplainValue / formatExplainString', () => {
  it('renders the value for a present attribute, never the status phrase', () => {
    expect(formatExplainString({ status: 'present', value: 'Fertigungskosten!D15' }, 'de')).toBe('Fertigungskosten!D15')
  })

  it('renders the fixed not_applicable phrase (DE) when no note is given', () => {
    expect(formatExplainString({ status: 'not_applicable', value: null }, 'de')).toBe('nicht anwendbar')
  })

  it('renders the fixed not_captured phrase verbatim as specified by the task ("nicht erfasst (älterer Lauf)")', () => {
    expect(formatExplainString({ status: 'not_captured', value: null }, 'de')).toBe('nicht erfasst (älterer Lauf)')
  })

  it('appends the adapter note after the status phrase when present, never replacing it', () => {
    expect(
      formatExplainString(
        { status: 'not_captured', value: null, note: { de: 'Feld nicht im Header gefunden.', en: 'Field not found in the header.' } },
        'de',
      ),
    ).toBe('nicht erfasst (älterer Lauf) — Feld nicht im Header gefunden.')
  })

  it('appends the EN side of a bilingual note for locale en, never the German text', () => {
    expect(
      formatExplainString(
        { status: 'not_captured', value: null, note: { de: 'Feld nicht im Header gefunden.', en: 'Field not found in the header.' } },
        'en',
      ),
    ).toBe('not captured (older run) — Field not found in the header.')
  })

  it('uses the EN status phrase for locale en', () => {
    expect(formatExplainString({ status: 'not_captured', value: null }, 'en')).toBe('not captured (older run)')
    expect(formatExplainString({ status: 'not_applicable', value: null }, 'en')).toBe('not applicable')
  })

  it('never fabricates a value for a non-present status — status phrase only, regardless of a stray value', () => {
    // Defensive: even if a caller somehow attached a non-null value to a
    // non-present status, the renderer must not surface it as if it were real.
    expect(formatExplainValue({ status: 'not_applicable', value: 42 }, 'de', (n) => String(n))).toBe('nicht anwendbar')
  })
})

describe('formatExplainNumber', () => {
  it('stringifies a present number', () => {
    expect(formatExplainNumber({ status: 'present', value: 0.95 }, 'de')).toBe('0.95')
  })
})

describe('formatExplainFormula', () => {
  it('renders the raw formula text for a present formula', () => {
    expect(formatExplainFormula({ status: 'present', value: { raw: '=S14+S13', normalized: 'S14+S13' } }, 'de')).toBe('=S14+S13')
  })
  it('renders the not_applicable phrase when no formula exists', () => {
    expect(
      formatExplainFormula({ status: 'not_applicable', value: null, note: { de: 'Kein Wert.', en: 'No value.' } }, 'de'),
    ).toBe('nicht anwendbar — Kein Wert.')
  })
})

describe('formatExplainBilingual', () => {
  it('renders the DE side of a present bilingual value for locale de, EN side for locale en', () => {
    const v = { status: 'present' as const, value: { de: 'R2: Pflichtfeld ist ungültig.', en: 'R2: Mandatory field is invalid.' } }
    expect(formatExplainBilingual(v, 'de')).toBe('R2: Pflichtfeld ist ungültig.')
    expect(formatExplainBilingual(v, 'en')).toBe('R2: Mandatory field is invalid.')
  })
})

describe('formatExplainMappingMethod', () => {
  it('resolves the manual_override / automatic_header_mapping enum keys to DE/EN labels', () => {
    expect(formatExplainMappingMethod({ status: 'present', value: 'manual_override' }, 'de')).toBe('Manuell zugeordnet (Override)')
    expect(formatExplainMappingMethod({ status: 'present', value: 'manual_override' }, 'en')).toBe('Manually mapped (override)')
    expect(formatExplainMappingMethod({ status: 'present', value: 'automatic_header_mapping' }, 'de')).toBe('Automatisch — Kopfzeilen-Zuordnung')
    expect(formatExplainMappingMethod({ status: 'present', value: 'automatic_header_mapping' }, 'en')).toBe('Automatic — header mapping')
  })
})

describe('formatExplainQafGuideRequirement', () => {
  it('resolves the requirement enum key + evidence to a localized label, never translating the citation', () => {
    const v = { status: 'present' as const, value: { requirement: 'mandatory' as const, evidence: 'leitfaden-teil1:9' } }
    expect(formatExplainQafGuideRequirement(v, 'de')).toBe('Pflichtfeld (Quelle: leitfaden-teil1:9)')
    expect(formatExplainQafGuideRequirement(v, 'en')).toBe('Mandatory (Source: leitfaden-teil1:9)')
  })
  it('omits the source suffix entirely when there is no evidence', () => {
    const v = { status: 'present' as const, value: { requirement: 'optional' as const } }
    expect(formatExplainQafGuideRequirement(v, 'de')).toBe('optional')
    expect(formatExplainQafGuideRequirement(v, 'en')).toBe('optional')
  })
})

describe('formatExplainEngineVersion', () => {
  it('joins key:value pairs for a present engine-version object', () => {
    expect(formatExplainEngineVersion({ status: 'present', value: { parser: '1.3.0', differ: '1.1.0' } }, 'de')).toBe(
      'parser: 1.3.0 · differ: 1.1.0',
    )
  })
})

describe('fmtExplainTimestamp', () => {
  it('formats a valid ISO timestamp', () => {
    const out = fmtExplainTimestamp('2026-07-10T09:00:00Z')
    expect(out).not.toBe('2026-07-10T09:00:00Z')
    expect(out.length).toBeGreaterThan(0)
  })
  it('falls back to the raw string for an unparsable value (never crashes, never "Invalid Date")', () => {
    expect(fmtExplainTimestamp('not-a-date')).toBe('not-a-date')
  })
})

function fullAttrs(): ExplainAttributes {
  const side = {
    sourceFile: { status: 'present' as const, value: 'ALT.xlsx' },
    sourceSheet: { status: 'present' as const, value: 'Fertigungskosten' },
    sourceCell: { status: 'present' as const, value: 'Fertigungskosten!D15' },
    rawValue: {
      status: 'not_captured' as const,
      value: null,
      note: { de: 'nur normalisiert gespeichert', en: 'only normalized value stored' },
    },
    normalizedValue: { status: 'present' as const, value: '12' },
    formula: { status: 'not_applicable' as const, value: null },
    mappingMethod: { status: 'present' as const, value: 'automatic_header_mapping' as const },
    confidence: { status: 'present' as const, value: 0.9 },
    unitConversion: { status: 'not_applicable' as const, value: null, note: { de: 'keine Umrechnung', en: 'no conversion' } },
    currencyConversion: { status: 'not_applicable' as const, value: null, note: { de: 'keine Umrechnung', en: 'no conversion' } },
    templateProfileVersion: { status: 'present' as const, value: 'QAF_V9_SUMMARY (known)' },
  }
  return {
    canonicalFieldId: { status: 'present', value: 'mfg_cycle_time' },
    originalLabelDe: { status: 'present', value: 'Zykluszeit [s]' },
    originalLabelEn: { status: 'present', value: 'Cycle time [s]' },
    qafGuideRequirement: { status: 'present', value: { requirement: 'mandatory', evidence: 'leitfaden-teil1:9' } },
    alt: side,
    neu: { ...side, sourceFile: { status: 'present', value: 'NEU.xlsx' }, normalizedValue: { status: 'present', value: '14' } },
    calculatedDelta: { status: 'present', value: 'Δ abs 2 · Δ % 16.7%' },
    comparisonRule: {
      status: 'present',
      value: { de: 'Status-Band-Vergleich (auffaellig_10)', en: 'Status-band comparison (auffaellig_10)' },
    },
    validationResult: { status: 'present', value: 'auffaellig_10' },
    engineVersion: { status: 'present', value: { parser: '1.3.0' } },
    timestamp: { status: 'present', value: '2026-07-10T09:00:00Z' },
  }
}

describe('buildExplainRows', () => {
  it('produces one row per §17 attribute, in a stable order, DE labels by default', () => {
    const rows = buildExplainRows(fullAttrs(), 'de')
    expect(rows.map((r) => r.key)).toEqual([
      'canonicalFieldId',
      'originalLabel',
      'qafGuideRequirement',
      'sourceFile',
      'sourceSheet',
      'sourceCell',
      'rawValue',
      'normalizedValue',
      'formula',
      'mappingMethod',
      'confidence',
      'unitConversion',
      'currencyConversion',
      'templateProfileVersion',
      'calculatedDelta',
      'comparisonRule',
      'validationResult',
      'engineVersion',
      'timestamp',
    ])
    expect(rows.find((r) => r.key === 'sourceFile')).toMatchObject({ altText: 'ALT.xlsx', neuText: 'NEU.xlsx' })
    expect(rows.find((r) => r.key === 'canonicalFieldId')).toMatchObject({ shared: true, altText: 'mfg_cycle_time' })
  })

  it('uses the EN original label when locale is en, DE otherwise', () => {
    const rowsDe = buildExplainRows(fullAttrs(), 'de')
    const rowsEn = buildExplainRows(fullAttrs(), 'en')
    expect(rowsDe.find((r) => r.key === 'originalLabel')?.altText).toBe('Zykluszeit [s]')
    expect(rowsEn.find((r) => r.key === 'originalLabel')?.altText).toBe('Cycle time [s]')
  })

  it('formats not_captured/not_applicable attributes with their note, never a blank cell', () => {
    const rows = buildExplainRows(fullAttrs(), 'de')
    const rawRow = rows.find((r) => r.key === 'rawValue')
    expect(rawRow?.altText).toBe('nicht erfasst (älterer Lauf) — nur normalisiert gespeichert')
    const formulaRow = rows.find((r) => r.key === 'formula')
    expect(formulaRow?.altText).toBe('nicht anwendbar')
  })

  it('renders the timestamp as a formatted date, not the raw ISO string', () => {
    const rows = buildExplainRows(fullAttrs(), 'de')
    const ts = rows.find((r) => r.key === 'timestamp')
    expect(ts?.altText).not.toBe('2026-07-10T09:00:00Z')
  })

  // KAR-924/Teil 1 (mirrors the KAR-906/#284 labelEn absence-assertion
  // pattern in rule-engine.test.ts): the EN view must never leak the German
  // text for the four text classes this task fixed — a translated status
  // phrase followed by an untranslated German `note`/enum-text was exactly
  // the bug. Each assertion targets ONE specific German string, not a broad
  // regex sweep (same precedent).
  it('EN view: mappingMethod never leaks the German "Automatisch — Kopfzeilen-Zuordnung" text', () => {
    const rows = buildExplainRows(fullAttrs(), 'en')
    const mappingRow = rows.find((r) => r.key === 'mappingMethod')
    expect(mappingRow?.altText).toBe('Automatic — header mapping')
    expect(mappingRow?.altText).not.toContain('Kopfzeilen')
    expect(mappingRow?.altText).not.toContain('Automatisch')
  })

  it('EN view: qafGuideRequirement never leaks the German "Pflichtfeld"/"Quelle" text', () => {
    const rows = buildExplainRows(fullAttrs(), 'en')
    const reqRow = rows.find((r) => r.key === 'qafGuideRequirement')
    expect(reqRow?.altText).toBe('Mandatory (Source: leitfaden-teil1:9)')
    expect(reqRow?.altText).not.toContain('Pflichtfeld')
    expect(reqRow?.altText).not.toContain('Quelle')
  })

  it('EN view: comparisonRule never leaks the German "Status-Band-Vergleich" text', () => {
    const rows = buildExplainRows(fullAttrs(), 'en')
    const ruleRow = rows.find((r) => r.key === 'comparisonRule')
    expect(ruleRow?.altText).toBe('Status-band comparison (auffaellig_10)')
    expect(ruleRow?.altText).not.toContain('Status-Band-Vergleich')
  })

  it('EN view: a not_captured note never leaks the German "nur normalisiert gespeichert" text', () => {
    const rows = buildExplainRows(fullAttrs(), 'en')
    const rawRow = rows.find((r) => r.key === 'rawValue')
    expect(rawRow?.altText).toBe('not captured (older run) — only normalized value stored')
    expect(rawRow?.altText).not.toContain('nur normalisiert gespeichert')
    expect(rawRow?.altText).not.toContain('erfasst (älterer Lauf)')
  })
})
