// Tests for WorkbookCapabilityDetector (KAR-959/P2, Master-Prompt §11).

import { describe, expect, it } from 'vitest'
import {
  detectWorkbookCapabilities,
  computeWorkbookCapabilityMatrix,
  facetSignalFromRowArray,
  facetSignalFromSingleRecord,
  facetSignalFromManufacturingSteps,
  facetSignalFromSummary,
  AVAILABLE_CONFIDENCE_THRESHOLD,
  type FacetParseSignal,
} from '../capability-detector'
import { resolveSheetRoles } from '../sheet-resolver'
import type { QafSummary } from '../../types'
import type { QafModule } from '../types'

function emptySummary(): QafSummary {
  const keys: Array<keyof QafSummary> = [
    'partNumber',
    'quotationDate',
    'supplier',
    'partName',
    'variant',
    'project',
    'requestVersion',
    'changeIndex',
    'supplierNo',
    'peakVolumeYear',
    'productionStartSop',
    'deliverySite',
    'shiftsPerWeek',
  ]
  const s = {} as QafSummary
  for (const k of keys) s[k] = { value: null, cell: null }
  return s
}

describe('detectWorkbookCapabilities — AVAILABLE / PARTIAL', () => {
  it('reports AVAILABLE for a fully mapped, high-confidence facet on a recognized sheet', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'MATERIAL' }])
    const facet: FacetParseSignal = {
      module: 'MATERIAL',
      sheet: 'MATERIAL',
      present: true,
      rowCount: 5,
      parseConfidence: 1,
      mappedFieldCount: 20,
      unmappedHeaders: [],
      coreFieldsFound: true,
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { MATERIAL: facet })
    const finding = matrix.modules.find((m) => m.module === 'MATERIAL')
    expect(finding?.status).toBe('AVAILABLE')
    expect(finding?.source?.sheet).toBe('MATERIAL')
    expect(finding?.reason).toBeUndefined()
  })

  it('reports PARTIAL when core fields were found but confidence is below the threshold', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'MATERIAL' }])
    const facet: FacetParseSignal = {
      module: 'MATERIAL',
      sheet: 'MATERIAL',
      present: true,
      rowCount: 5,
      parseConfidence: AVAILABLE_CONFIDENCE_THRESHOLD - 0.2,
      mappedFieldCount: 10,
      unmappedHeaders: ['Sonderfeld'],
      coreFieldsFound: true,
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { MATERIAL: facet })
    expect(matrix.modules.find((m) => m.module === 'MATERIAL')?.status).toBe('PARTIAL')
  })

  it('reports PARTIAL when confidence is high but an unmapped header remains', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'MATERIAL' }])
    const facet: FacetParseSignal = {
      module: 'MATERIAL',
      sheet: 'MATERIAL',
      present: true,
      rowCount: 5,
      parseConfidence: 1,
      mappedFieldCount: 10,
      unmappedHeaders: ['Unbekanntes Feld'],
      coreFieldsFound: true,
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { MATERIAL: facet })
    expect(matrix.modules.find((m) => m.module === 'MATERIAL')?.status).toBe('PARTIAL')
  })
})

describe('detectWorkbookCapabilities — MISSING', () => {
  it('reports MISSING with reason MISSING_IN_WORKBOOK when no sheet role and no facet exist', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'Deckblatt' }])
    const matrix = detectWorkbookCapabilities(sheetRoles, {})
    const finding = matrix.modules.find((m) => m.module === 'SBM')
    expect(finding?.status).toBe('MISSING')
    expect(finding?.reason).toBe('MISSING_IN_WORKBOOK')
    expect(finding?.source).toBeNull()
  })

  it('reports MISSING when a facet ran but found zero data rows', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'MATERIAL' }])
    const facet: FacetParseSignal = {
      module: 'MATERIAL',
      sheet: 'MATERIAL',
      present: true,
      rowCount: 0,
      parseConfidence: 1,
      mappedFieldCount: 15,
      unmappedHeaders: [],
      coreFieldsFound: true,
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { MATERIAL: facet })
    expect(matrix.modules.find((m) => m.module === 'MATERIAL')?.status).toBe('MISSING')
  })

  it('reports MISSING when coreFieldsFound is false and no degradation object was attached (below minimum signal floor)', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'MATERIAL' }])
    const facet: FacetParseSignal = {
      module: 'MATERIAL',
      sheet: 'MATERIAL',
      present: true,
      rowCount: 0,
      parseConfidence: 0,
      mappedFieldCount: 0,
      unmappedHeaders: [],
      coreFieldsFound: false,
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { MATERIAL: facet })
    const finding = matrix.modules.find((m) => m.module === 'MATERIAL')
    expect(finding?.status).toBe('MISSING')
    expect(finding?.reason).toBe('MISSING_IN_WORKBOOK')
  })
})

describe('detectWorkbookCapabilities — DERIVABLE (gate-audit.md B14 generalization)', () => {
  it('reports DERIVABLE/NOT_YET_SUPPORTED when the resolver found a role sheet but no facet was extracted', () => {
    // "LV Detail EU" is recognized by sheet-resolver as a manufacturing-role
    // sheet, but no manufacturingSteps facet was passed for it — the exact
    // "recognized structure, no wired parser" case Master-Prompt §11
    // requires to be distinguished from MISSING.
    const sheetRoles = resolveSheetRoles([{ name: 'LV Detail EU' }])
    const matrix = detectWorkbookCapabilities(sheetRoles, {})
    const finding = matrix.modules.find((m) => m.module === 'MANUFACTURING')
    expect(finding?.status).toBe('DERIVABLE')
    expect(finding?.reason).toBe('NOT_YET_SUPPORTED')
    expect(finding?.source?.sheet).toBe('LV Detail EU')
  })

  it('never represents DERIVABLE and MISSING as the same state for the same input shape', () => {
    const withRoleSheet = detectWorkbookCapabilities(resolveSheetRoles([{ name: 'LV Detail EU' }]), {})
    const withoutAnySheet = detectWorkbookCapabilities(resolveSheetRoles([{ name: 'Deckblatt' }]), {})
    const a = withRoleSheet.modules.find((m) => m.module === 'MANUFACTURING')?.status
    const b = withoutAnySheet.modules.find((m) => m.module === 'MANUFACTURING')?.status
    expect(a).toBe('DERIVABLE')
    expect(b).toBe('MISSING')
    expect(a).not.toBe(b)
  })
})

describe('detectWorkbookCapabilities — PARSE_FAILED (FacetDegradation pass-through)', () => {
  it('reports PARSE_FAILED and carries the degradation message when a facet degradation reason is PARSE_FAILED', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'SBM-DEVICES-FWZ' }])
    const facet: FacetParseSignal = {
      module: 'SBM',
      sheet: 'SBM-DEVICES-FWZ',
      present: true,
      rowCount: 2,
      parseConfidence: 0.3,
      mappedFieldCount: 3,
      unmappedHeaders: ['?'],
      coreFieldsFound: false,
      degradation: { facet: 'sbm', reason: 'PARSE_FAILED', sheet: 'SBM-DEVICES-FWZ', message: 'header unreadable below minimum signal' },
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { SBM: facet })
    const finding = matrix.modules.find((m) => m.module === 'SBM')
    expect(finding?.status).toBe('PARSE_FAILED')
    expect(finding?.reason).toBe('PARSE_FAILED')
    expect(finding?.message).toContain('minimum signal')
  })

  it('maps an INCONSISTENT degradation reason to PARTIAL status', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'MATERIAL' }])
    const facet: FacetParseSignal = {
      module: 'MATERIAL',
      sheet: 'MATERIAL',
      present: true,
      rowCount: 3,
      parseConfidence: 0.5,
      mappedFieldCount: 5,
      unmappedHeaders: [],
      coreFieldsFound: false,
      degradation: { facet: 'material', reason: 'INCONSISTENT', sheet: 'MATERIAL', message: 'conflicting candidate sheets' },
    }
    const matrix = detectWorkbookCapabilities(sheetRoles, { MATERIAL: facet })
    expect(matrix.modules.find((m) => m.module === 'MATERIAL')?.status).toBe('PARTIAL')
  })
})

describe('detectWorkbookCapabilities — sheetRoles/assumptions/modelVersion are always present', () => {
  it('covers exactly the 8 capability modules', () => {
    const matrix = detectWorkbookCapabilities([], {})
    const modules = matrix.modules.map((m) => m.module).sort()
    expect(modules).toEqual(['CO2E', 'LC_CN', 'LOGISTICS', 'MANUFACTURING', 'MATERIAL', 'RMR', 'SBM', 'SUMMARY'].sort() as QafModule[])
  })

  it('returns non-empty assumptions and a modelVersion string', () => {
    const matrix = detectWorkbookCapabilities([], {})
    expect(matrix.assumptions.length).toBeGreaterThan(0)
    expect(typeof matrix.modelVersion).toBe('string')
    expect(matrix.modelVersion.length).toBeGreaterThan(0)
  })

  it('echoes back the given sheetRoles unchanged', () => {
    const roles = resolveSheetRoles([{ name: 'MATERIAL' }, { name: 'Deckblatt' }])
    const matrix = detectWorkbookCapabilities(roles, {})
    expect(matrix.sheetRoles).toEqual(roles)
  })
})

describe('facet adapters', () => {
  it('facetSignalFromRowArray reports present:false for a null result', () => {
    const s = facetSignalFromRowArray('MATERIAL', 'MATERIAL', null)
    expect(s.present).toBe(false)
    expect(s.coreFieldsFound).toBe(false)
  })

  it('facetSignalFromRowArray carries through degradation and rowCount', () => {
    const s = facetSignalFromRowArray('SBM', 'SBM-DEVICES-FWZ', {
      length: 4,
      parseConfidence: 0.4,
      mappedFieldCount: 6,
      coreFieldsFound: false,
      unmappedHeaders: ['x'],
      degradation: { facet: 'sbm', reason: 'INCONSISTENT', message: 'm' },
    })
    expect(s.present).toBe(true)
    expect(s.rowCount).toBe(4)
    expect(s.degradation?.reason).toBe('INCONSISTENT')
  })

  it('facetSignalFromSingleRecord adapts a nested meta object', () => {
    const s = facetSignalFromSingleRecord('LC_CN', 'LC-CN', { meta: { parseConfidence: 1, mappedFieldCount: 15, coreFieldsFound: true } })
    expect(s.present).toBe(true)
    expect(s.parseConfidence).toBe(1)
    expect(s.rowCount).toBeUndefined()
  })

  it('facetSignalFromManufacturingSteps derives coreFieldsFound/present from row count alone (production catch-path has no explicit flag)', () => {
    const empty = facetSignalFromManufacturingSteps('Fertigungskosten', { length: 0, parseConfidence: 0, mappedFieldCount: 0 })
    expect(empty.present).toBe(false)
    expect(empty.coreFieldsFound).toBe(false)

    const withRows = facetSignalFromManufacturingSteps('Fertigungskosten', { length: 12, parseConfidence: 1, mappedFieldCount: 20 })
    expect(withRows.present).toBe(true)
    expect(withRows.coreFieldsFound).toBe(true)
  })

  it('facetSignalFromSummary treats any located identity/metric slot as core-found', () => {
    const summary = emptySummary()
    summary.partNumber = { value: '12345', cell: 'B3' }
    const withoutMetrics = facetSignalFromSummary('SUMMARY', summary, null)
    expect(withoutMetrics.present).toBe(true)
    expect(withoutMetrics.coreFieldsFound).toBe(true)
  })

  it('facetSignalFromSummary reports present:false for an all-null summary and no summaryMetrics', () => {
    const s = facetSignalFromSummary(null, emptySummary(), null)
    expect(s.present).toBe(false)
    expect(s.coreFieldsFound).toBe(false)
  })
})

describe('facetSignalFromManufacturingSteps — PARSE_FAILED pass-through (KAR-959 review finding #1)', () => {
  it('sets present:true when a degradation is attached, even at length:0, so the detector reaches the degradation branch', () => {
    const degraded = facetSignalFromManufacturingSteps('Fertigungskosten', {
      length: 0,
      parseConfidence: 0,
      mappedFieldCount: 0,
      unmappedHeaders: [],
      degradation: { facet: 'manufacturing', reason: 'PARSE_FAILED', sheet: null, message: 'Kopfzeile nicht lesbar.' },
    })
    expect(degraded.present).toBe(true)
    expect(degraded.degradation?.reason).toBe('PARSE_FAILED')
  })

  it('detectWorkbookCapabilities reports MANUFACTURING as PARSE_FAILED (not DERIVABLE/NOT_YET_SUPPORTED) when the P1-catch degradation is attached', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'Fertigungskosten' }])
    const degraded = facetSignalFromManufacturingSteps('Fertigungskosten', {
      length: 0,
      parseConfidence: 0,
      mappedFieldCount: 0,
      unmappedHeaders: [],
      degradation: { facet: 'manufacturing', reason: 'PARSE_FAILED', sheet: null, message: 'Die Datei enthält keine Tabellenblätter.' },
    })
    const matrix = detectWorkbookCapabilities(sheetRoles, { MANUFACTURING: degraded })
    const finding = matrix.modules.find((m) => m.module === 'MANUFACTURING')
    expect(finding?.status).toBe('PARSE_FAILED')
    expect(finding?.reason).toBe('PARSE_FAILED')
    expect(finding?.status).not.toBe('DERIVABLE')
  })

  it('computeWorkbookCapabilityMatrix threads a manufacturingSteps.degradation through to a PARSE_FAILED module finding', () => {
    const matrix = computeWorkbookCapabilityMatrix({
      sheets: [{ name: 'SUMMARY', rowCount: 40, colCount: 10 }, { name: 'Fertigungskosten', rowCount: 20, colCount: 15 }],
      summary: emptySummary(),
      summaryMetrics: null,
      manufacturingSteps: {
        length: 0,
        parseConfidence: 0,
        mappedFieldCount: 0,
        unmappedHeaders: [],
        degradation: { facet: 'manufacturing', reason: 'PARSE_FAILED', sheet: null, message: 'Kopfzeile nicht lesbar.' },
      },
      material: null,
      sbm: null,
      rmr: null,
      logistics: null,
      lccn: null,
      co2eMaterial: null,
      co2eSummary: null,
    })
    const finding = matrix.modules.find((m) => m.module === 'MANUFACTURING')
    expect(finding?.status).toBe('PARSE_FAILED')
  })

  it('without a degradation, an empty result still reports DERIVABLE/MISSING as before (no regression for the genuinely-absent case)', () => {
    const sheetRoles = resolveSheetRoles([{ name: 'LV Detail EU' }])
    const empty = facetSignalFromManufacturingSteps('LV Detail EU', { length: 0, parseConfidence: 0, mappedFieldCount: 0, unmappedHeaders: [] })
    expect(empty.present).toBe(false)
    const matrix = detectWorkbookCapabilities(sheetRoles, { MANUFACTURING: empty })
    expect(matrix.modules.find((m) => m.module === 'MANUFACTURING')?.status).toBe('DERIVABLE')
  })
})

describe('mergeCo2eFacetSignals — present beats not-present on a confidence tie (KAR-959 review finding #2)', () => {
  it('prefers a present-but-low-quality materialSignal over an absent summarySignal at equal (zero) confidence', () => {
    const matrix = computeWorkbookCapabilityMatrix({
      sheets: [{ name: 'CO2e', rowCount: 10, colCount: 5 }],
      summary: emptySummary(),
      summaryMetrics: null,
      manufacturingSteps: { length: 0, parseConfidence: 0, mappedFieldCount: 0, unmappedHeaders: [] },
      material: null,
      sbm: null,
      rmr: null,
      logistics: null,
      lccn: null,
      co2eMaterial: { length: 3, parseConfidence: 0, mappedFieldCount: 0, coreFieldsFound: false, unmappedHeaders: [] },
      co2eSummary: null,
    })
    const co2e = matrix.modules.find((m) => m.module === 'CO2E')
    // Pre-fix: summarySignal.present=false wins the >= 0/0 tie, producing
    // DERIVABLE/NOT_YET_SUPPORTED ("sheet recognized, nothing extracted") —
    // wrong, since 3 CO2e material rows WERE extracted (just below the
    // core-fields-found quality bar). Post-fix: the present materialSignal
    // wins, producing MISSING/MISSING_IN_WORKBOOK ("core fields not found"),
    // an honest reflection of what was actually attempted.
    expect(co2e?.status).not.toBe('DERIVABLE')
    expect(co2e?.reason).not.toBe('NOT_YET_SUPPORTED')
    expect(co2e?.status).toBe('MISSING')
    expect(co2e?.reason).toBe('MISSING_IN_WORKBOOK')
  })

  it('keeps preferring higher parseConfidence when both signals are present (no regression on the original tie-break)', () => {
    const matrix = computeWorkbookCapabilityMatrix({
      sheets: [{ name: 'CO2e', rowCount: 30, colCount: 10 }],
      summary: emptySummary(),
      summaryMetrics: null,
      manufacturingSteps: { length: 0, parseConfidence: 0, mappedFieldCount: 0, unmappedHeaders: [] },
      material: null,
      sbm: null,
      rmr: null,
      logistics: null,
      lccn: null,
      co2eMaterial: { length: 3, parseConfidence: 1, mappedFieldCount: 10, coreFieldsFound: true, unmappedHeaders: [] },
      co2eSummary: { meta: { parseConfidence: 0.2, mappedFieldCount: 1, coreFieldsFound: false } },
    })
    const co2e = matrix.modules.find((m) => m.module === 'CO2E')
    expect(co2e?.status).toBe('AVAILABLE')
  })

  it('still returns the not-present summarySignal when neither sub-part is present', () => {
    const matrix = computeWorkbookCapabilityMatrix({
      sheets: [{ name: 'Deckblatt', rowCount: 5, colCount: 5 }],
      summary: emptySummary(),
      summaryMetrics: null,
      manufacturingSteps: { length: 0, parseConfidence: 0, mappedFieldCount: 0, unmappedHeaders: [] },
      material: null,
      sbm: null,
      rmr: null,
      logistics: null,
      lccn: null,
      co2eMaterial: null,
      co2eSummary: null,
    })
    const co2e = matrix.modules.find((m) => m.module === 'CO2E')
    expect(co2e?.status).toBe('MISSING')
    expect(co2e?.reason).toBe('MISSING_IN_WORKBOOK')
  })
})

describe('computeWorkbookCapabilityMatrix — end-to-end orchestrator', () => {
  it('produces AVAILABLE for MATERIAL when a real-shaped row-array facet is passed through the full pipeline', () => {
    const matrix = computeWorkbookCapabilityMatrix({
      sheets: [{ name: 'SUMMARY', rowCount: 40, colCount: 10 }, { name: 'MATERIAL', rowCount: 20, colCount: 15 }],
      summary: emptySummary(),
      summaryMetrics: null,
      manufacturingSteps: { length: 0, parseConfidence: 0, mappedFieldCount: 0, unmappedHeaders: [] },
      material: { length: 5, parseConfidence: 1, mappedFieldCount: 25, coreFieldsFound: true, unmappedHeaders: [] },
      sbm: null,
      rmr: null,
      logistics: null,
      lccn: null,
      co2eMaterial: null,
      co2eSummary: null,
    })
    expect(matrix.modules.find((m) => m.module === 'MATERIAL')?.status).toBe('AVAILABLE')
    expect(matrix.modules.find((m) => m.module === 'MATERIAL')?.source?.sheet).toBe('MATERIAL')
  })

  it('merges CO2E summary + material sub-parses by preferring the higher-confidence one', () => {
    const matrix = computeWorkbookCapabilityMatrix({
      sheets: [{ name: 'CO2e', rowCount: 30, colCount: 10 }],
      summary: emptySummary(),
      summaryMetrics: null,
      manufacturingSteps: { length: 0, parseConfidence: 0, mappedFieldCount: 0, unmappedHeaders: [] },
      material: null,
      sbm: null,
      rmr: null,
      logistics: null,
      lccn: null,
      co2eMaterial: { length: 3, parseConfidence: 1, mappedFieldCount: 10, coreFieldsFound: true, unmappedHeaders: [] },
      co2eSummary: { meta: { parseConfidence: 0.2, mappedFieldCount: 1, coreFieldsFound: false } },
    })
    const co2e = matrix.modules.find((m) => m.module === 'CO2E')
    expect(co2e?.status).toBe('AVAILABLE')
  })
})
