// Tests for the Produktionssicht/Anomalien view-model builders (KAR-960/P3).

import { describe, expect, it } from 'vitest'
import {
  buildProductionRateRows,
  productionViewAvailability,
  buildMasterRateRows,
  anomaliesAvailability,
  scenarioAvailability,
  productionFieldDelta,
  type StepRowLike,
} from '../production-view'
import type { QAFRow } from '../../../../qaf-parser'
import type { FieldCandidate } from '../types'

function row(overrides: Partial<QAFRow>): QAFRow {
  return {
    positionsnummer: '10',
    teilebenennung: '',
    prozessbezeichnung: 'Spritzguss',
    bezeichnungAnlage: '',
    standort: '',
    beschaffungswaehrung: 'EUR',
    zykluszeit: null,
    teileProZyklus: null,
    anzahlMA: null,
    lohnkosten: null,
    lohnzuschlagssaetze: null,
    mss: null,
    ruestkosten: null,
    fek: null,
    rfgk: null,
    fk: null,
    angebotswaehrung: 'EUR',
    wechselkurs: null,
    anzahlProAngebotsteil: null,
    fkAW: null,
    ausschuss: null,
    ausschusskosten: null,
    ...overrides,
  }
}

describe('buildProductionRateRows', () => {
  it('joins ALT and NEU steps on the same row by position_number', () => {
    const steps: StepRowLike[] = [
      { file_id: 'alt', position_number: '10', process_name: 'Spritzguss', raw_values: row({ zykluszeit: 10, mss: 40 }) },
      { file_id: 'neu', position_number: '10', process_name: 'Spritzguss', raw_values: row({ zykluszeit: 12, mss: 42 }) },
    ]
    const rows = buildProductionRateRows(steps, 'alt', 'neu')
    expect(rows).toHaveLength(1)
    expect(rows[0].alt?.zykluszeit).toBe(10)
    expect(rows[0].neu?.zykluszeit).toBe(12)
  })

  it('keeps a one-sided row visible (partial population, §22)', () => {
    const steps: StepRowLike[] = [{ file_id: 'alt', position_number: '10', process_name: 'Spritzguss', raw_values: row({ zykluszeit: 10 }) }]
    const rows = buildProductionRateRows(steps, 'alt', 'neu')
    expect(rows[0].alt).not.toBeNull()
    expect(rows[0].neu).toBeNull()
  })

  it('skips a step row with every rate field null (nothing to show)', () => {
    const steps: StepRowLike[] = [{ file_id: 'alt', position_number: '10', process_name: 'Spritzguss', raw_values: row({}) }]
    expect(buildProductionRateRows(steps, 'alt', 'neu')).toEqual([])
  })

  it('ignores a step belonging to neither baseline nor comparison file', () => {
    const steps: StepRowLike[] = [{ file_id: 'other', position_number: '10', process_name: 'X', raw_values: row({ zykluszeit: 10 }) }]
    expect(buildProductionRateRows(steps, 'alt', 'neu')).toEqual([])
  })
})

describe('productionViewAvailability', () => {
  it('is unavailable with a precise reason when no side has any row', () => {
    const result = productionViewAvailability([])
    expect(result.available).toBe(false)
    expect(result.reason).not.toMatch(/Summary-QAFs enthalten keine Prozessdaten/)
    expect(result.reason).toContain('Fertigungskosten-Blatt')
  })

  it('is available when at least one row has data, with a count in the reason', () => {
    const result = productionViewAvailability([
      { key: '10', label: 'Spritzguss', alt: { zykluszeit: 1, teileProZyklus: 1, anzahlMA: 1, lohnkosten: 1, mss: 1, ausschuss: 1 }, neu: null },
    ])
    expect(result.available).toBe(true)
    expect(result.reason).toContain('1 ALT')
    expect(result.reason).toContain('0 NEU')
  })
})

function candidate(fieldId: string, value: number, cell = 'G10', confidence = 0.7): FieldCandidate<number> {
  return { fieldId, module: 'SUMMARY', value, scope: 'global', source: { sheet: 'Zusammenfassung', cell }, confidence }
}

describe('buildMasterRateRows / anomaliesAvailability', () => {
  it('resolves each side independently — an ALT collision does not affect NEU', () => {
    const alt = [candidate('cap_sum_master_labor_rate', 40, 'G10'), candidate('cap_sum_master_labor_rate', 42, 'G30')]
    const neu = [candidate('cap_sum_master_labor_rate', 41, 'G10')]
    const rows = buildMasterRateRows(alt, neu)
    const labor = rows.find((r) => r.key === 'laborRate')!
    expect(labor.alt?.status).toBe('INCONSISTENT')
    expect(labor.neu?.status).toBe('SINGLE')
    expect(labor.neu?.value).toBe(41)
  })

  it('is unavailable with a precise reason when nothing was found on either side', () => {
    const rows = buildMasterRateRows([], [])
    const result = anomaliesAvailability(rows)
    expect(result.available).toBe(false)
    expect(result.reason).not.toMatch(/kein INPUT-Blatt mit Master-Sätzen/)
    expect(result.reason).toContain('Zusammenfassungs-Blatt')
  })

  it('is available and names the found rates + flags INCONSISTENT ones', () => {
    const alt = [candidate('cap_sum_master_labor_rate', 40), candidate('cap_sum_master_labor_rate', 42)]
    const neu = [candidate('cap_sum_master_scrap_rate', 0.02)]
    const rows = buildMasterRateRows(alt, neu)
    const result = anomaliesAvailability(rows)
    expect(result.available).toBe(true)
    expect(result.reason).toContain('Lohnsatz')
    expect(result.reason).toContain('Scrap-Satz')
    expect(result.reason).toMatch(/widersprüchlich/)
  })
})

// KAR-966 item 2: the Szenario-Editor capability gate (see production-view.ts's
// scenarioAvailability doc comment for the full technical decision) — this is
// a STRUCTURAL constant for qaf-comparison-detail.tsx (always unavailable),
// not a per-instance gap, so these tests pin the reason text's evidence-based
// content, not a true/false toggle.
describe('scenarioAvailability', () => {
  it('is always unavailable — the G60 scenario engine needs G60-workbook columns no Summary comparison has', () => {
    expect(scenarioAvailability(0).available).toBe(false)
    expect(scenarioAvailability(3).available).toBe(false)
  })

  it('names the missing G60-only structural data, not a QAF-type blanket claim', () => {
    const { reason } = scenarioAvailability(0)
    expect(reason).not.toMatch(/G60-Detail-QAFs$/)
    expect(reason).toContain('G60-Rohformat-Spalten')
    expect(reason).toContain('Hochrechnung (Sektion 12)')
  })

  it('is evidence-based: reports the actual process-row count when there is one', () => {
    expect(scenarioAvailability(4).reason).toContain('4 Fertigungsschritt(e)')
  })

  it('reports "keine Fertigungsschritte" when there is nothing at all', () => {
    expect(scenarioAvailability(0).reason).toContain('Keine Fertigungsschritte')
  })
})

// KAR-966 item 3: Produktionssicht ALT/NEU diff-highlight classification.
describe('productionFieldDelta', () => {
  it('classifies an increase as unfavorable for a plain cost field (e.g. zykluszeit)', () => {
    expect(productionFieldDelta(10, 12, 'zykluszeit')).toBe('unfavorable')
  })

  it('classifies a decrease as favorable for a plain cost field', () => {
    expect(productionFieldDelta(12, 10, 'zykluszeit')).toBe('favorable')
  })

  it('classifies no change as unchanged', () => {
    expect(productionFieldDelta(10, 10, 'mss')).toBe('unchanged')
  })

  it('classifies a missing side as unknown, not favorable/unfavorable', () => {
    expect(productionFieldDelta(null, 10, 'ausschuss')).toBe('unknown')
    expect(productionFieldDelta(10, null, 'ausschuss')).toBe('unknown')
    expect(productionFieldDelta(null, null, 'ausschuss')).toBe('unknown')
  })

  it('flips favourability for a HIGHER_IS_BETTER_FIELDS field (e.g. teileProZyklus) — an increase there is favorable', () => {
    expect(productionFieldDelta(1, 2, 'teileProZyklus')).toBe('favorable')
    expect(productionFieldDelta(2, 1, 'teileProZyklus')).toBe('unfavorable')
  })
})
