// Unit tests for summary-totals-differ.ts (KAR-951, urgent Multi-QAF livetest
// fix).
//
// FIXTURE-DATEN-REGEL (same discipline as material-differ.test.ts/container-
// differ.test.ts's own header comment): every id, number, and currency below
// is FREE INVENTION, not lifted from any real analyzed workbook.
//
// Real-file regression (KAR-951 user-pair livetest file + the 4-file
// self-compare regression corpus) lives in
// summary-totals-differ.real-files.test.ts (env-gated).

import { describe, it, expect } from 'vitest'
import type { MultiQafContainer, VirtualVariantSummaryMoneyRows, VirtualVariantSummaryTotals } from '../types'
import type { VariantMatchResult } from '../variant-matcher'
import { diffSummaryTotals } from '../summary-totals-differ'

// ── Minimal fixture builders ────────────────────────────────────────────────

function emptyMoneyRows(): VirtualVariantSummaryMoneyRows {
  return { scrap: null, otherSurcharges: null, offerBasePrice: null, offerBasePriceInclAllocation: null, offerPrice: null }
}

function emptyContainer(overrides: Partial<MultiQafContainer> = {}): MultiQafContainer {
  return {
    sourceWorkbook: { fileName: null, fileHash: null, sheetNames: [], hiddenSheetNames: [] },
    detectedTemplateFamily: 'unknown',
    multiQafVersion: null,
    underlyingQafVersion: null,
    language: 'unknown',
    currencies: [],
    sharedMetadata: {},
    variantDimensions: [],
    activeVariants: [],
    inactiveVariants: [],
    auxiliaryScenarios: [],
    helperColumns: [],
    sharedMaterialMaster: [],
    sharedManufacturingProfiles: [],
    sharedToolingData: [],
    setupCostProfiles: [],
    variantProfileBindings: [],
    summaryAggregation: { perVariant: [], formulaLineageNotes: [] },
    templateFingerprint: { family: 'unknown', structuralHash: null, variantColumnCount: 0, headerRowCount: 0, knownProfile: null, classification: null },
    warnings: [],
    confidence: 0.5,
    summaryMoneyRowsByVariant: {},
    ...overrides,
  }
}

function totals(variantId: string, overrides: Partial<VirtualVariantSummaryTotals> = {}): VirtualVariantSummaryTotals {
  return {
    variantId,
    materialCosts: overrides.materialCosts ?? null,
    materialCostsByCurrency: overrides.materialCostsByCurrency ?? [],
    manufacturingCosts: overrides.manufacturingCosts ?? null,
    totalProductionCosts: overrides.totalProductionCosts ?? null,
    toolingAndFixtureCost: overrides.toolingAndFixtureCost ?? null,
    setupCostAllocation: overrides.setupCostAllocation ?? null,
    scrap: overrides.scrap ?? null,
    otherSurcharges: overrides.otherSurcharges ?? null,
    offerBasePrice: overrides.offerBasePrice ?? null,
    offerBasePriceInclAllocation: overrides.offerBasePriceInclAllocation ?? null,
    offerPrice: overrides.offerPrice ?? null,
  }
}

function matched(leftId: string, rightId: string): VariantMatchResult {
  return {
    kind: 'matched',
    leftIndex: 0,
    leftId,
    rightIndex: 0,
    rightId,
    stage: 'raw_exact',
    confidence: 1,
    evidence: [],
    explanation: 'test fixture',
    matchedDimensionCount: 1,
  }
}

function ambiguous(leftIds: string[], rightIds: string[]): VariantMatchResult {
  return {
    kind: 'ambiguous',
    leftIndices: leftIds.map((_, i) => i),
    leftIds,
    rightIndices: rightIds.map((_, i) => i),
    rightIds,
    candidates: [],
    reviewRelevant: true,
    explanation: 'test fixture ambiguous',
  }
}

// ── Scenarios ────────────────────────────────────────────────────────────────

describe('diffSummaryTotals (KAR-951)', () => {
  it('reports a changed metric (same currency, value moved)', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q71-alt', { offerBasePrice: { value: 100, currency: 'EUR' } })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q71-neu', { offerBasePrice: { value: 115, currency: 'EUR' } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q71-alt', 'q71-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'offerBasePrice')!

    expect(finding.state).toBe('changed')
    expect(finding.deltaAbsolute).toBe(15)
    expect(finding.currencyChanged).toBe(false)
    expect(finding.currencyGate).toBe('same_currency')
    expect(finding.missingSide).toBeUndefined()
  })

  it('reports unchanged when both sides are identical (value + currency)', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q72-alt', { manufacturingCosts: { value: 42, currency: 'EUR' } })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q72-neu', { manufacturingCosts: { value: 42, currency: 'EUR' } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q72-alt', 'q72-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'manufacturingCosts')!

    expect(finding.state).toBe('unchanged')
    expect(finding.deltaAbsolute).toBe(0)
  })

  it('reports nicht_ermittelbar with missingSide "alt" when only ALT is null', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q73-alt', { scrap: null })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q73-neu', { scrap: { value: 5, currency: 'EUR' } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q73-alt', 'q73-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'scrap')!

    expect(finding.state).toBe('nicht_ermittelbar')
    expect(finding.missingSide).toBe('alt')
    expect(finding.deltaAbsolute).toBeNull()
  })

  it('reports nicht_ermittelbar with missingSide "both" when neither side has a value', () => {
    const alt = emptyContainer({ summaryAggregation: { perVariant: [totals('q74-alt')], formulaLineageNotes: [] } })
    const neu = emptyContainer({ summaryAggregation: { perVariant: [totals('q74-neu')], formulaLineageNotes: [] } })

    const result = diffSummaryTotals(alt, neu, [matched('q74-alt', 'q74-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'otherSurcharges')!

    expect(finding.state).toBe('nicht_ermittelbar')
    expect(finding.missingSide).toBe('both')
  })

  it('a currency swap is its own changed finding, even when the raw numbers are equal — never netted', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q75-alt', { offerPrice: { value: 200, currency: 'EUR' } })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q75-neu', { offerPrice: { value: 200, currency: 'USD' } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q75-alt', 'q75-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'offerPrice')!

    expect(finding.state).toBe('changed')
    expect(finding.currencyChanged).toBe(true)
    expect(finding.currencyGate).toBe('currency_changed')
    // changedMetricSumsByCurrency must NEVER include a currency_changed finding.
    expect(result.changedMetricSumsByCurrency.find((s) => s.currency === 'EUR' || s.currency === 'USD')).toBeUndefined()
  })

  // ── KAR-951 F3/F4 fix (review findings): a currency swap requires BOTH
  // sides' currency to be KNOWN and unequal — `null` (unknown) on one side is
  // `currencyGate === 'currency_unknown'`'s own territory, never a
  // fabricated "changed" currency. ─────────────────────────────────────────

  it('F3/F4: currency unknown on ONE side with the SAME numeric value is never reported as a currency change (state driven by the value delta only)', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q80-alt', { offerBasePrice: { value: 100, currency: null } })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q80-neu', { offerBasePrice: { value: 100, currency: 'EUR' } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q80-alt', 'q80-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'offerBasePrice')!

    expect(finding.currencyChanged).toBe(false)
    expect(finding.currencyGate).toBe('currency_unknown')
    // Same numeric value both sides -> 'konstant' delta -> unchanged, NOT
    // 'changed' merely because one side's currency metadata is unknown.
    expect(finding.state).toBe('unchanged')
    expect(finding.deltaAbsolute).toBe(0)
  })

  it('F3/F4: currency unknown on ONE side with a DIFFERENT numeric value is reported changed via the value delta, still without a fabricated currency swap', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q81-alt', { offerBasePrice: { value: 100, currency: null } })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q81-neu', { offerBasePrice: { value: 120, currency: 'EUR' } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q81-alt', 'q81-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'offerBasePrice')!

    expect(finding.currencyChanged).toBe(false)
    expect(finding.currencyGate).toBe('currency_unknown')
    expect(finding.state).toBe('changed')
    expect(finding.deltaAbsolute).toBe(20)
  })

  it('F3/F4: currency unknown on BOTH sides never sets currencyChanged, regardless of the value delta', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q82-alt', { offerBasePrice: { value: 100, currency: null } })], formulaLineageNotes: [] },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q82-neu', { offerBasePrice: { value: 100, currency: null } })], formulaLineageNotes: [] },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q82-alt', 'q82-neu')])
    const finding = result.findings.find((f) => f.metricKey === 'offerBasePrice')!

    expect(finding.currencyChanged).toBe(false)
    expect(finding.currencyGate).toBe('currency_unknown')
    expect(finding.state).toBe('unchanged')
  })

  // ── KAR-951 F1 fix (review finding): offerBasePriceInclAllocation is a
  // real, independently-tracked metric — the Umlage-inclusive base price row
  // that used to be untracked entirely. ────────────────────────────────────

  it('F1: offerBasePriceInclAllocation diffs and carries sourceRefs independently of offerBasePrice/offerPrice', () => {
    const alt = emptyContainer({
      summaryAggregation: {
        perVariant: [
          totals('q83-alt', {
            offerBasePrice: { value: 200, currency: 'EUR' },
            offerBasePriceInclAllocation: { value: 210, currency: 'EUR' },
          }),
        ],
        formulaLineageNotes: [],
      },
      summaryMoneyRowsByVariant: {
        'q83-alt': {
          ...emptyMoneyRows(),
          offerBasePriceInclAllocation: {
            amount: { value: 210, currency: 'EUR' },
            sourceCell: { sheet: 'Zusammenfassung', cell: 'Q34', row: 34, column: 17 },
          },
        },
      },
    })
    const neu = emptyContainer({
      summaryAggregation: {
        perVariant: [
          totals('q83-neu', {
            offerBasePrice: { value: 200, currency: 'EUR' },
            offerBasePriceInclAllocation: { value: 225, currency: 'EUR' },
          }),
        ],
        formulaLineageNotes: [],
      },
      summaryMoneyRowsByVariant: {
        'q83-neu': {
          ...emptyMoneyRows(),
          offerBasePriceInclAllocation: {
            amount: { value: 225, currency: 'EUR' },
            sourceCell: { sheet: 'Zusammenfassung', cell: 'Q34', row: 34, column: 17 },
          },
        },
      },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q83-alt', 'q83-neu')])
    const inclAllocation = result.findings.find((f) => f.metricKey === 'offerBasePriceInclAllocation')!
    const basePrice = result.findings.find((f) => f.metricKey === 'offerBasePrice')!

    // The Umlage-inclusive row moved — offerBasePrice itself (a DIFFERENT
    // row) did not, proving the two are never conflated.
    expect(inclAllocation.state).toBe('changed')
    expect(inclAllocation.deltaAbsolute).toBe(15)
    expect(inclAllocation.sourceRefs).toHaveLength(2)
    expect(basePrice.state).toBe('unchanged')
    expect(basePrice.sourceRefs).toEqual([])
  })

  it('materialCostsByCurrency: compares per bucket, a currency present on only one side is nicht_ermittelbar for that bucket, others compare normally', () => {
    const alt = emptyContainer({
      summaryAggregation: {
        perVariant: [
          totals('q76-alt', {
            materialCostsByCurrency: [
              { value: 10, currency: 'EUR' },
              { value: 3, currency: 'USD' },
            ],
          }),
        ],
        formulaLineageNotes: [],
      },
    })
    const neu = emptyContainer({
      summaryAggregation: {
        perVariant: [
          totals('q76-neu', {
            materialCostsByCurrency: [{ value: 12, currency: 'EUR' }],
          }),
        ],
        formulaLineageNotes: [],
      },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q76-alt', 'q76-neu')])
    const eur = result.materialCostsByCurrencyFindings.find((f) => f.currency === 'EUR')!
    const usd = result.materialCostsByCurrencyFindings.find((f) => f.currency === 'USD')!

    expect(eur.state).toBe('changed')
    expect(eur.deltaAbsolute).toBe(2)
    expect(usd.state).toBe('nicht_ermittelbar')
    expect(usd.missingSide).toBe('neu')
  })

  it('carries sourceRefs for scrap/otherSurcharges/offerBasePrice/offerPrice from summaryMoneyRowsByVariant, empty for the other metrics', () => {
    const alt = emptyContainer({
      summaryAggregation: { perVariant: [totals('q77-alt', { offerPrice: { value: 100, currency: 'EUR' }, materialCosts: { value: 1, currency: 'EUR' } })], formulaLineageNotes: [] },
      summaryMoneyRowsByVariant: {
        'q77-alt': { ...emptyMoneyRows(), offerPrice: { amount: { value: 100, currency: 'EUR' }, sourceCell: { sheet: 'Zusammenfassung', cell: 'Q38', row: 37, column: 17 } } },
      },
    })
    const neu = emptyContainer({
      summaryAggregation: { perVariant: [totals('q77-neu', { offerPrice: { value: 110, currency: 'EUR' }, materialCosts: { value: 1, currency: 'EUR' } })], formulaLineageNotes: [] },
      summaryMoneyRowsByVariant: {
        'q77-neu': { ...emptyMoneyRows(), offerPrice: { amount: { value: 110, currency: 'EUR' }, sourceCell: { sheet: 'Zusammenfassung', cell: 'Q38', row: 37, column: 17 } } },
      },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q77-alt', 'q77-neu')])
    const offerPriceFinding = result.findings.find((f) => f.metricKey === 'offerPrice')!
    const materialCostsFinding = result.findings.find((f) => f.metricKey === 'materialCosts')!

    expect(offerPriceFinding.sourceRefs).toHaveLength(2)
    expect(materialCostsFinding.sourceRefs).toEqual([])
  })

  it('an ambiguous match is passed through on uncertainMatches, never diffed', () => {
    const alt = emptyContainer({ summaryAggregation: { perVariant: [totals('q78-a1'), totals('q78-a2')], formulaLineageNotes: [] } })
    const neu = emptyContainer({ summaryAggregation: { perVariant: [totals('q78-n1'), totals('q78-n2')], formulaLineageNotes: [] } })

    const result = diffSummaryTotals(alt, neu, [ambiguous(['q78-a1', 'q78-a2'], ['q78-n1', 'q78-n2'])])

    expect(result.findings).toEqual([])
    expect(result.uncertainMatches).toHaveLength(1)
  })

  it('changedMetricSumsByCurrency sums only same-currency changed findings, grouped by currency', () => {
    const alt = emptyContainer({
      summaryAggregation: {
        perVariant: [totals('q79-alt', { otherSurcharges: { value: 10, currency: 'EUR' }, offerBasePrice: { value: 50, currency: 'EUR' } })],
        formulaLineageNotes: [],
      },
    })
    const neu = emptyContainer({
      summaryAggregation: {
        perVariant: [totals('q79-neu', { otherSurcharges: { value: 14, currency: 'EUR' }, offerBasePrice: { value: 55, currency: 'EUR' } })],
        formulaLineageNotes: [],
      },
    })

    const result = diffSummaryTotals(alt, neu, [matched('q79-alt', 'q79-neu')])
    const eurSum = result.changedMetricSumsByCurrency.find((s) => s.currency === 'EUR')!

    expect(eurSum.totalDeltaAbsolute).toBe(9) // 4 + 5
    expect(eurSum.metricFindingCount).toBe(2)
  })
})
