import { describe, it, expect } from 'vitest'
import { parseSummary, gridFromCells } from '../summary-parser'

// Build a sheet grid from an { A1: value } map (mirrors the verified real layout:
// labels in columns B/F/L, values in C/I/M). gridFromCells is the same helper the
// ExcelJS adapter uses, exported for tests.

describe('parseSummary — German Zusammenfassung layout', () => {
  const grid = gridFromCells({
    B5: 'Bearbeiter Lieferant:',
    C5: 'Jost Reinschmidt',
    F5: 'Anbieter/Lieferant:',
    I5: 'Karl HESS ',
    L5: 'Teilebenennung:',
    M5: 'LI / RE Blende D Säule',
    F6: 'BMW Lieferanten-Nr.:', // allow-customer-string
    I6: 21390810,
    L6: 'Änderungsindex (AI):',
    M6: '12',
    F7: 'BMW Projekt:', // allow-customer-string
    I7: 'G26',
    L7: 'Anfragenummer / Version:',
    M7: 'C1A',
    B8: 'Angebotsdatum:',
    C8: new Date('2025-04-29T00:00:00.000Z'),
    F8: 'BMW Sachnummer:', // allow-customer-string
    I8: 7490365,
    L8: 'Variante:',
    M8: '3. Gea Tape ACH141 - AEB245',
  })
  const s = parseSummary(grid)

  it('reads part number from the primary cell I8 with provenance', () => {
    expect(s.partNumber.value).toBe('7490365')
    expect(s.partNumber.cell).toBe('I8')
  })

  it('reads supplier, part name, variant', () => {
    expect(s.supplier.value).toBe('Karl HESS')
    expect(s.partName.value).toBe('LI / RE Blende D Säule')
    expect(s.variant.value).toBe('3. Gea Tape ACH141 - AEB245')
  })

  it('converts a Date quotation date to ISO', () => {
    expect(s.quotationDate.value).toBe('2025-04-29T00:00:00.000Z')
    expect(s.quotationDate.cell).toBe('C8')
  })

  it('reads project, request/version, change index, supplier no', () => {
    expect(s.project.value).toBe('G26')
    expect(s.requestVersion.value).toBe('C1A')
    expect(s.changeIndex.value).toBe('12')
    expect(s.supplierNo.value).toBe('21390810')
  })
})

describe('parseSummary — English Summary layout', () => {
  const grid = gridFromCells({
    F5: 'Vendor/supplier:',
    I5: 'AGC Automotive Europe',
    L5: 'Parts designation:',
    M5: 'Skyroof',
    B8: 'Quotation date:',
    C8: '2023-04-12T00:00:00.000Z',
    F8: 'BMW Part Number:', // allow-customer-string
    // I8 intentionally empty — real EN files leave the part number blank
    L8: 'Variant:',
    M8: 'Skyroof TTS12,5%',
  })
  const s = parseSummary(grid)

  it('parses EN labels', () => {
    expect(s.supplier.value).toBe('AGC Automotive Europe')
    expect(s.partName.value).toBe('Skyroof')
    expect(s.variant.value).toBe('Skyroof TTS12,5%')
  })

  it('returns null part number when the value cell is empty (triggers filename fallback later)', () => {
    expect(s.partNumber.value).toBeNull()
  })
})

describe('parseSummary — label fallback when value is off the standard column', () => {
  it('finds the value via label scan when the primary cell is empty', () => {
    const grid = gridFromCells({
      F5: 'Anbieter/Lieferant:',
      H5: 'Shifted Supplier GmbH', // value not in I5
      F8: 'BMW Sachnummer:', // allow-customer-string
      H8: '5A1CEF6', // alphanumeric, not in I8
    })
    const s = parseSummary(grid)
    expect(s.supplier.value).toBe('Shifted Supplier GmbH')
    expect(s.supplier.cell).toBe('H5')
    expect(s.partNumber.value).toBe('5A1CEF6')
    expect(s.partNumber.cell).toBe('H8')
  })

  it('keeps an alphanumeric part number intact', () => {
    const grid = gridFromCells({ I8: '5A62582', F8: 'BMW Sachnummer:' }) // allow-customer-string
    expect(parseSummary(grid).partNumber.value).toBe('5A62582')
  })
})

// KAR-917: labelScan (the fallback path for the 4 label-scan-only fields —
// peakVolumeYear/productionStartSop/deliverySite/shiftsPerWeek, none of
// which has a FieldDef.primary anchor) does SUBSTRING containment matching,
// not exact-label matching — deliberately, so shortened real-file label
// variants still resolve (see FIELD_DEFS comments, e.g. "peak volume" for
// the real V8.8 EN Fehlerreport column text). That substring match can also
// fire on an unrelated free-text "Bemerkungsfeld"/remarks cell that merely
// MENTIONS the label word in prose — before this fix, labelScan silently
// returned the value next to whichever matching cell it found FIRST in scan
// order, which could be the free-text neighbor's value instead of the real
// field's value. This block reproduces exactly that scenario and asserts
// the KAR-917 collision guard now returns null/null instead of a
// wrong-but-plausible-looking value.
describe('parseSummary — KAR-917 labelScan collision guard', () => {
  it('returns null for peakVolumeYear when a Bemerkungsfeld free-text cell also contains "Peakvolumen"', () => {
    const grid = gridFromCells({
      // Genuine label + value, "Allgemeine Prämissen" section (Leitfaden [11]).
      B20: 'Peakvolumen Jahr [Teile / Jahr]:',
      C20: 45000,
      // A completely unrelated remarks/free-text field elsewhere on the
      // sheet whose PROSE happens to mention "Peakvolumen" — invented text,
      // not from any real file — with its own neighboring non-empty cell
      // that a naive first-match scan would misread as the peakVolumeYear
      // value.
      B30: 'Offenes Bemerkungsfeld:',
      C30: 'Hinweis: Peakvolumen ggf. spaeter final anzupassen, siehe Anhang.',
      D30: 99999999, // decoy — must NEVER be picked up as peakVolumeYear
    })
    const s = parseSummary(grid)
    expect(s.peakVolumeYear.value).toBeNull()
    expect(s.peakVolumeYear.cell).toBeNull()
  })

  it('still resolves peakVolumeYear normally when only ONE cell matches (no collision, no regression)', () => {
    const grid = gridFromCells({
      B20: 'Peakvolumen Jahr [Teile / Jahr]:',
      C20: 45000,
    })
    const s = parseSummary(grid)
    expect(s.peakVolumeYear.value).toBe('45000')
  })

  it('returns null for shiftsPerWeek when a Bemerkungsfeld cell also contains "Shifts"', () => {
    const grid = gridFromCells({
      B22: 'Schichten / Woche:',
      C22: 3,
      B31: 'Comments:',
      C31: 'Shifts might change after ramp-up.',
      D31: 7, // decoy
    })
    const s = parseSummary(grid)
    expect(s.shiftsPerWeek.value).toBeNull()
  })
})

// Review-Fix 5 (KAR-973 adversarial review, MAJOR): labelScan's shared
// MAX_SCAN_RIGHT=6 window can walk PAST a genuinely blank intended-value cell
// and land on a NEIGHBORING SUMMARY_METRIC label's own un-colon'd text (e.g.
// summary-metrics.ts's "Enthaltene Zölle" / "Enthaltene Verpackung und
// Transport", anchored at column G on the same Summary sheet) — reproduced
// here with invented cell coordinates matching the real-corpus row shape
// (own label ends with ':', 3 genuinely blank cells, then the collision text
// 5 columns right, inside the 6-column window). Opt-in via
// FieldDef.strictValueCheck (only plannedCapacity/lotSize) — the 4 KAR-910
// fields deliberately keep their exact prior (unguarded) behavior, see the
// "byte-identical" block further below.
describe('parseSummary — Review-Fix 5 labelScan plausibility guard (plannedCapacity/lotSize only)', () => {
  it('rejects a plannedCapacity candidate that is a neighboring SUMMARY_METRIC label leaking through a blank value cell', () => {
    const grid = gridFromCells({
      B15: 'Plankapazität [Teile / Jahr] :',
      // C15/D15/E15/F15 intentionally blank (no value cells set at all).
      G15: 'Enthaltene Zölle', // allow-customer-string — another module's own label, no digit, no colon
    })
    const s = parseSummary(grid)
    expect(s.plannedCapacity.value).toBeNull()
    expect(s.plannedCapacity.cell).toBeNull()
  })

  it('rejects a lotSize candidate under the same collision shape', () => {
    const grid = gridFromCells({
      B16: 'Fertigungslosgröße [Teile] :',
      G16: 'Enthaltene Verpackung und Transport', // allow-customer-string
    })
    const s = parseSummary(grid)
    expect(s.lotSize.value).toBeNull()
  })

  it('still resolves a genuine numeric plannedCapacity value normally (no false-positive rejection)', () => {
    const grid = gridFromCells({
      B15: 'Plankapazität [Teile / Jahr] :',
      C15: 221500,
    })
    const s = parseSummary(grid)
    expect(s.plannedCapacity.value).toBe('221500')
    expect(s.plannedCapacity.cell).toBe('C15')
  })

  it('still resolves a genuine numeric lotSize value normally (no false-positive rejection)', () => {
    const grid = gridFromCells({
      B16: 'Fertigungslosgröße [Teile] :',
      C16: 250,
    })
    const s = parseSummary(grid)
    expect(s.lotSize.value).toBe('250')
  })

  it('skips an implausible first candidate and still finds a plausible one further right within the scan window', () => {
    const grid = gridFromCells({
      B15: 'Plankapazität [Teile / Jahr] :',
      D15: 'Enthaltene Zölle', // allow-customer-string — implausible, no digit — rejected
      F15: 221500, // genuine value, 2 cells further right, still inside MAX_SCAN_RIGHT
    })
    const s = parseSummary(grid)
    expect(s.plannedCapacity.value).toBe('221500')
  })

  it('a candidate containing a digit is still rejected when it normalizes to ANOTHER FIELD_DEFS label (defense-in-depth check b)', () => {
    const grid = gridFromCells({
      B15: 'Plankapazität [Teile / Jahr] :',
      // Deliberately contains a digit AND another field's own label text —
      // must still be rejected via the "matches another FIELD_DEFS label" check.
      C15: 'Peakvolumen 2026',
    })
    const s = parseSummary(grid)
    expect(s.plannedCapacity.value).toBeNull()
  })

  // Byte-identical behavior lock for the 4 already-shipped KAR-910 fields —
  // none of them opt into strictValueCheck, so the EXACT same collision shape
  // that now nulls plannedCapacity/lotSize above must still resolve (wrongly,
  // unchanged, documented as a disclosed follow-up) for peakVolumeYear, i.e.
  // this fix must NOT silently change their behavior.
  it('does NOT apply the new guard to peakVolumeYear (KAR-910 field, unguarded, unchanged) — same collision shape still "succeeds" (wrongly) exactly as before', () => {
    const grid = gridFromCells({
      B20: 'Peakvolumen Jahr [Teile / Jahr]:',
      G20: 'Enthaltene Zölle', // allow-customer-string — no digit, would be rejected if strictValueCheck applied
    })
    const s = parseSummary(grid)
    expect(s.peakVolumeYear.value).toBe('Enthaltene Zölle') // allow-customer-string
  })
})
