import { describe, it, expect } from 'vitest'
import type { QAFRow } from '@/lib/qaf-parser'
import type { QafSummary, QafSummaryKey } from '../types'
import {
  checkPlausibility,
  checkFilenamePartNumber,
  checkMakeOrBuyIndication,
  manufacturingParseDegradationToPlausibilityIssue,
  manufacturingIgnoredCandidatesToPlausibilityIssue,
} from '../plausibility'

function summary(values: Partial<Record<QafSummaryKey, string | null>>): QafSummary {
  const keys: QafSummaryKey[] = [
    'partNumber',
    'quotationDate',
    'supplier',
    'partName',
    'variant',
    'project',
    'requestVersion',
    'changeIndex',
    'supplierNo',
    // KAR-910
    'peakVolumeYear',
    'productionStartSop',
    'deliverySite',
    'shiftsPerWeek',
  ]
  const out = {} as QafSummary
  for (const k of keys) out[k] = { value: values[k] ?? null, cell: null }
  return out
}

function mkRow(overrides: 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,
    ...overrides,
  }
}

const base = {
  altSummary: summary({ partNumber: '7490365', partName: 'Blende', variant: 'A', quotationDate: '2023-01-01' }),
  neuSummary: summary({ partNumber: '7490365', partName: 'Blende', variant: 'A', quotationDate: '2024-01-01' }),
  altSteps: [mkRow()],
  neuSteps: [mkRow({ fk: 110 })],
}

function types(issues: { type: string }[]): string[] {
  return issues.map((i) => i.type)
}

describe('checkPlausibility', () => {
  it('clean comparison yields no critical issues', () => {
    const issues = checkPlausibility(base)
    expect(issues.every((i) => i.severity !== 'kritisch')).toBe(true)
  })

  it('flags different part number as critical', () => {
    const issues = checkPlausibility({ ...base, neuSummary: summary({ partNumber: '9999999' }) })
    const pn = issues.find((i) => i.type === 'part_number_mismatch')
    expect(pn?.severity).toBe('kritisch')
  })

  it('flags differing part name and variant as hinweis', () => {
    const issues = checkPlausibility({
      ...base,
      neuSummary: summary({ partNumber: '7490365', partName: 'Anders', variant: 'B', quotationDate: '2024-01-01' }),
    })
    expect(types(issues)).toContain('part_name_changed')
    expect(types(issues)).toContain('variant_changed')
  })

  it('flags NEU older than ALT as pruefen', () => {
    const issues = checkPlausibility({
      ...base,
      altSummary: summary({ partNumber: '7490365', quotationDate: '2025-01-01' }),
      neuSummary: summary({ partNumber: '7490365', quotationDate: '2024-01-01' }),
    })
    const d = issues.find((i) => i.type === 'quotation_date_order')
    expect(d?.severity).toBe('pruefen')
  })

  it('flags negative cost values as critical', () => {
    const issues = checkPlausibility({ ...base, neuSteps: [mkRow({ fk: -5 })] })
    expect(issues.find((i) => i.type === 'negative_cost')?.severity).toBe('kritisch')
  })

  it('flags a process step with zero FK as pruefen', () => {
    const issues = checkPlausibility({ ...base, neuSteps: [mkRow({ fk: 0 })] })
    expect(types(issues)).toContain('zero_cost')
  })

  it('flags differing quotation currency between ALT and NEU', () => {
    const issues = checkPlausibility({
      ...base,
      altSteps: [mkRow({ angebotswaehrung: 'EUR' })],
      neuSteps: [mkRow({ angebotswaehrung: 'CNY' })],
    })
    const cur = issues.find((i) => i.type === 'currency_change')
    expect(cur?.severity).toBe('pruefen')
  })
})

// KAR-906 / P3.2: every checkPlausibility() issue carries an EN counterpart.
describe('checkPlausibility — bilingual (KAR-906)', () => {
  it('part_number_mismatch has a distinct English explanation', () => {
    const issues = checkPlausibility({ ...base, neuSummary: summary({ partNumber: '9999999' }) })
    const pn = issues.find((i) => i.type === 'part_number_mismatch')
    expect(pn?.explanationEn).toBeTruthy()
    expect(pn?.explanationEn).not.toBe(pn?.explanation)
    expect(pn?.explanationEn).toMatch(/part number/i)
  })

  it('part_name_changed and variant_changed carry English explanations', () => {
    const issues = checkPlausibility({
      ...base,
      neuSummary: summary({ partNumber: '7490365', partName: 'Anders', variant: 'B', quotationDate: '2024-01-01' }),
    })
    expect(issues.find((i) => i.type === 'part_name_changed')?.explanationEn).toMatch(/part name/i)
    expect(issues.find((i) => i.type === 'variant_changed')?.explanationEn).toMatch(/variant/i)
  })

  it('quotation_date_order has an English explanation', () => {
    const issues = checkPlausibility({
      ...base,
      altSummary: summary({ partNumber: '7490365', quotationDate: '2025-01-01' }),
      neuSummary: summary({ partNumber: '7490365', quotationDate: '2024-01-01' }),
    })
    expect(issues.find((i) => i.type === 'quotation_date_order')?.explanationEn).toMatch(/quotation date/i)
  })

  it('currency_change has an English explanation', () => {
    const issues = checkPlausibility({
      ...base,
      altSteps: [mkRow({ angebotswaehrung: 'EUR' })],
      neuSteps: [mkRow({ angebotswaehrung: 'CNY' })],
    })
    expect(issues.find((i) => i.type === 'currency_change')?.explanationEn).toMatch(/currency/i)
  })

  it('negative_cost interpolates the same field/value in the English explanation', () => {
    const issues = checkPlausibility({ ...base, neuSteps: [mkRow({ fk: -5 })] })
    const neg = issues.find((i) => i.type === 'negative_cost')
    expect(neg?.explanationEn).toContain('fk')
    expect(neg?.explanationEn).toContain('-5')
    expect(neg?.explanationEn).toMatch(/negative/i)
  })

  it('zero_cost has an English explanation', () => {
    const issues = checkPlausibility({ ...base, neuSteps: [mkRow({ fk: 0 })] })
    expect(issues.find((i) => i.type === 'zero_cost')?.explanationEn).toMatch(/zero/i)
  })
})

// KAR-893 / P1.2: Fertigungskosten-Parser degradation -> plausibility bridge.
describe('manufacturingParseDegradationToPlausibilityIssue', () => {
  it('returns null when no header was unmapped (clean parse, silent like every other check)', () => {
    const issue = manufacturingParseDegradationToPlausibilityIssue(
      { parseConfidence: 1, unmappedHeaders: [], mappedFieldCount: 22 },
      'NEU',
    )
    expect(issue).toBeNull()
  })

  it('reports a pruefen issue naming the side, the coverage and the unmapped headers', () => {
    const issue = manufacturingParseDegradationToPlausibilityIssue(
      { parseConfidence: 0.9545454545454546, unmappedHeaders: ['Interne Bemerkung'], mappedFieldCount: 21 },
      'ALT',
    )
    expect(issue).not.toBeNull()
    expect(issue?.type).toBe('parser_degraded_manufacturing_headers')
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.step).toBe('ALT')
    expect(issue?.explanation).toContain('21/22')
    expect(issue?.explanation).toContain('Interne Bemerkung')
  })

  it('KAR-906: English explanation names the same side, coverage and unmapped headers', () => {
    const issue = manufacturingParseDegradationToPlausibilityIssue(
      { parseConfidence: 0.9545454545454546, unmappedHeaders: ['Interne Bemerkung'], mappedFieldCount: 21 },
      'ALT',
    )
    expect(issue?.explanationEn).toContain('21/22')
    expect(issue?.explanationEn).toContain('Interne Bemerkung')
    expect(issue?.explanationEn).toMatch(/manufacturing cost/i)
  })

  it('ignores ignoredCandidateSheets entirely — that signal is a separate bridge (KAR-927)', () => {
    const issue = manufacturingParseDegradationToPlausibilityIssue(
      { parseConfidence: 1, unmappedHeaders: [], mappedFieldCount: 22, ignoredCandidateSheets: ['Manufacturing costs'] },
      'NEU',
    )
    expect(issue).toBeNull()
  })
})

// KAR-927 (Multi-QAF-Programm P0.2): MANUFACTURING candidate-sheet visibility
// bridge, sibling to manufacturingParseDegradationToPlausibilityIssue above.
describe('manufacturingIgnoredCandidatesToPlausibilityIssue', () => {
  it('returns null when ignoredCandidateSheets is absent (standard QAF — no spam)', () => {
    const issue = manufacturingIgnoredCandidatesToPlausibilityIssue({}, 'NEU')
    expect(issue).toBeNull()
  })

  it('returns null when ignoredCandidateSheets is an empty array', () => {
    const issue = manufacturingIgnoredCandidatesToPlausibilityIssue({ ignoredCandidateSheets: [] }, 'NEU')
    expect(issue).toBeNull()
  })

  it('reports a pruefen issue naming the side and the ignored sheet names', () => {
    const issue = manufacturingIgnoredCandidatesToPlausibilityIssue(
      { ignoredCandidateSheets: ['Manufacturing costs', 'Manufactering costs'] },
      'ALT',
    )
    expect(issue).not.toBeNull()
    expect(issue?.type).toBe('parser_ignored_manufacturing_candidate_sheets')
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.step).toBe('ALT')
    expect(issue?.explanation).toContain('Manufacturing costs')
    expect(issue?.explanation).toContain('Manufactering costs')
    expect(issue?.explanationEn).toContain('Manufacturing costs')
    expect(issue?.explanationEn).toMatch(/candidate sheet/i)
  })
})

// D16 identity_mismatch (Spec-Erhebung 08.08.2026, PO-Linie „weiche Befunde"):
// die vier restlichen Identitätsfelder des Summary-Kopfs — supplier/supplierNo
// als _mismatch (pruefen), requestVersion/changeIndex als _changed (hinweis).
describe('checkSummaryIdentityPlausibility — D16 identity fields', () => {
  const identityCases: ReadonlyArray<{
    key: 'supplier' | 'supplierNo' | 'requestVersion' | 'changeIndex'
    type: string
    severity: string
    altValue: string
    neuValue: string
  }> = [
    { key: 'supplier', type: 'supplier_mismatch', severity: 'pruefen', altValue: 'Musterlieferant A', neuValue: 'Musterlieferant B' },
    { key: 'supplierNo', type: 'supplier_no_mismatch', severity: 'pruefen', altValue: '111111', neuValue: '222222' },
    { key: 'requestVersion', type: 'request_version_changed', severity: 'hinweis', altValue: 'V1', neuValue: 'V2' },
    { key: 'changeIndex', type: 'change_index_changed', severity: 'hinweis', altValue: 'AI01', neuValue: 'AI02' },
  ]

  for (const c of identityCases) {
    it(`flags a differing ${c.key} as ${c.type} (${c.severity}) with both values as evidence`, () => {
      const issues = checkPlausibility({
        ...base,
        altSummary: summary({ partNumber: '7490365', [c.key]: c.altValue }),
        neuSummary: summary({ partNumber: '7490365', [c.key]: c.neuValue }),
      })
      const issue = issues.find((i) => i.type === c.type)
      expect(issue).toBeDefined()
      expect(issue?.severity).toBe(c.severity)
      expect(issue?.field).toBe(c.key)
      expect(issue?.explanation).toContain(c.altValue)
      expect(issue?.explanation).toContain(c.neuValue)
      expect(issue?.explanationEn).toContain(c.altValue)
      expect(issue?.explanationEn).toContain(c.neuValue)
    })

    it(`stays silent on ${c.key} when one side is missing (conservative, no guessing)`, () => {
      const issues = checkPlausibility({
        ...base,
        altSummary: summary({ partNumber: '7490365', [c.key]: c.altValue }),
        neuSummary: summary({ partNumber: '7490365' }),
      })
      expect(issues.find((i) => i.type === c.type)).toBeUndefined()
    })
  }

  it('stays silent when every identity field matches (comparison keeps running clean)', () => {
    const both = {
      partNumber: '7490365',
      supplier: 'Musterlieferant A',
      supplierNo: '111111',
      requestVersion: 'V1',
      changeIndex: 'AI01',
    }
    const issues = checkPlausibility({ ...base, altSummary: summary(both), neuSummary: summary(both) })
    const d16Types = ['supplier_mismatch', 'supplier_no_mismatch', 'request_version_changed', 'change_index_changed']
    expect(issues.filter((i) => d16Types.includes(i.type))).toEqual([])
  })
})

// D15 filename_mismatch (Spec-Erhebung 08.08.2026): Dateiname vs. Inhalt-
// Sachnummer, je Seite, bewusst konservativ (Mindestlänge, Normalisierung).
describe('checkFilenamePartNumber — D15', () => {
  it('returns null when the normalized part number appears in the file name (separators ignored)', () => {
    expect(
      checkFilenamePartNumber({ fileName: 'QAF_74-90.365_v2.xlsx', partNumber: '7490 365' }, 'ALT'),
    ).toBeNull()
  })

  it('reports a hinweis issue naming side, file and part number when the file name does not contain it', () => {
    const issue = checkFilenamePartNumber({ fileName: 'QAF_9999999_final.xlsx', partNumber: '7490365' }, 'NEU')
    expect(issue).not.toBeNull()
    expect(issue?.type).toBe('filename_part_number_mismatch')
    expect(issue?.severity).toBe('hinweis')
    expect(issue?.step).toBe('NEU')
    expect(issue?.explanation).toContain('QAF_9999999_final.xlsx')
    expect(issue?.explanation).toContain('7490365')
    expect(issue?.explanationEn).toContain('7490365')
  })

  it('stays silent for part numbers shorter than the minimum length (collision noise)', () => {
    expect(checkFilenamePartNumber({ fileName: 'QAF_irgendwas.xlsx', partNumber: 'A123' }, 'ALT')).toBeNull()
  })

  it('stays silent when file name or part number is missing', () => {
    expect(checkFilenamePartNumber({ fileName: null, partNumber: '7490365' }, 'ALT')).toBeNull()
    expect(checkFilenamePartNumber({ fileName: 'QAF.xlsx', partNumber: null }, 'ALT')).toBeNull()
    expect(checkFilenamePartNumber({ fileName: '  ', partNumber: '7490365' }, 'ALT')).toBeNull()
  })
})

// D17 make_or_buy_indication (Spec-Erhebung 08.08.2026): Kostenstruktur-
// Verschiebung als Indiz — Bewertung beim Menschen, Schwelle als Named-Config.
describe('checkMakeOrBuyIndication — D17', () => {
  function diffs(input: {
    matAlt?: number | null
    matNeu?: number | null
    mfgAlt?: number | null
    mfgNeu?: number | null
    totalAlt?: number | null
    totalNeu?: number | null
  }) {
    return [
      { metricKey: 'materialCosts', altValue: input.matAlt ?? null, neuValue: input.matNeu ?? null },
      { metricKey: 'manufacturingCosts', altValue: input.mfgAlt ?? null, neuValue: input.mfgNeu ?? null },
      { metricKey: 'totalProductionCosts', altValue: input.totalAlt ?? null, neuValue: input.totalNeu ?? null },
    ]
  }

  it('reports a hinweis issue with both share pairs when the material share shifts above the threshold', () => {
    // Materialanteil 40 % → 55 % (15 pp > 10 pp Schwelle); Fertigungsanteil 50 % → 35 %.
    const issue = checkMakeOrBuyIndication(
      diffs({ matAlt: 40, matNeu: 55, mfgAlt: 50, mfgNeu: 35, totalAlt: 100, totalNeu: 100 }),
    )
    expect(issue).not.toBeNull()
    expect(issue?.type).toBe('make_or_buy_indication')
    expect(issue?.severity).toBe('hinweis')
    expect(issue?.explanation).toContain('40.0 %')
    expect(issue?.explanation).toContain('55.0 %')
    expect(issue?.explanation).toContain('50.0 %')
    expect(issue?.explanation).toContain('35.0 %')
    expect(issue?.explanationEn).toContain('make-or-buy')
  })

  it('stays silent below the threshold', () => {
    // Verschiebung 5 pp < 10 pp.
    expect(
      checkMakeOrBuyIndication(diffs({ matAlt: 40, matNeu: 45, mfgAlt: 50, mfgNeu: 45, totalAlt: 100, totalNeu: 100 })),
    ).toBeNull()
  })

  it('stays silent when a side has no computable share (missing or zero total)', () => {
    expect(
      checkMakeOrBuyIndication(diffs({ matAlt: 40, matNeu: 55, totalAlt: 100, totalNeu: null })),
    ).toBeNull()
    expect(
      checkMakeOrBuyIndication(diffs({ matAlt: 40, matNeu: 55, totalAlt: 100, totalNeu: 0 })),
    ).toBeNull()
    expect(checkMakeOrBuyIndication([])).toBeNull()
  })

  it('fires on a manufacturing-share shift even when the material share is not computable', () => {
    const issue = checkMakeOrBuyIndication(
      diffs({ mfgAlt: 30, mfgNeu: 45, totalAlt: 100, totalNeu: 100 }),
    )
    expect(issue).not.toBeNull()
    expect(issue?.explanation).toContain('Fertigungsanteil')
    expect(issue?.explanation).not.toContain('Materialanteil')
  })

  it('produces nothing when disabled (structurally impossible, not just filtered)', () => {
    expect(
      checkMakeOrBuyIndication(
        diffs({ matAlt: 40, matNeu: 80, totalAlt: 100, totalNeu: 100 }),
        { enabled: false, shareShiftPercentagePoints: 10 },
      ),
    ).toBeNull()
  })
})
