// Loop 10 View-Spec-Adoption, Modul 4: buildManufacturingRun — die aus
// Schale + Renderer an die Build-Zeit gezogenen Ableitungen der
// Fertigungskosten-Sektionen (view-spec-adoption-analyse.md, Abschnitt 3).
//
// Die Richtungs-/Label-REGELN selbst (diffDirection/diffStatusLabel) bleiben
// über den Re-Export in qaf-diff-status.test.ts vollständig geprüft — hier
// wird ihre VERDRAHTUNG in die Zeilen-Specs geprüft, plus alles, was vorher
// ungetestet im Render-Loop lag (Label-/Unsicherheits-/Provenienz-Lookups,
// Facts-Zählungen, Grenzen-Aggregation, populated-Filter).
//
// FIXTURE-DATEN-REGEL: alle Werte sind freie Erfindung, aus keiner realen
// Mappe entnommen.

import { describe, expect, it } from 'vitest'
import {
  buildManufacturingRun,
  ruleFindingResolver,
  type ManufacturingDiff,
  type ManufacturingStepRow,
} from '../manufacturing-run'
import type { AppendixStepMatch } from '../appendix-run'
import type { FieldCandidate } from '../capability/types'

const EMPTY_INPUT = {
  diffs: [] as ManufacturingDiff[],
  steps: [] as ManufacturingStepRow[],
  stepMatches: [] as AppendixStepMatch[],
  plausibility: [],
  baselineFileId: null,
  comparisonFileId: null,
  baselineSummaryFieldCandidates: [] as FieldCandidate<number>[],
  comparisonSummaryFieldCandidates: [] as FieldCandidate<number>[],
  baselineFileName: 'alt-fixture.xlsx',
  comparisonFileName: 'neu-fixture.xlsx',
  baselineManufacturingParseConfidence: 0.91,
  comparisonManufacturingParseConfidence: 0.87,
  baselineTemplateProfile: 'FIXTURE_PROFILE (known)',
  comparisonTemplateProfile: null,
  engineVersion: { parser: '9.9.9-fixture' },
  engineVersionParser: '9.9.9-fixture',
  createdAt: '2026-08-01T00:00:00Z',
}

function match(over: Partial<AppendixStepMatch>): AppendixStepMatch {
  return {
    id: 'm-1',
    alt_step_id: null,
    neu_step_id: null,
    match_status: 'matched',
    confidence_score: null,
    match_method: null,
    explanation: null,
    requires_review: false,
    ...over,
  }
}

function diffRow(over: Partial<ManufacturingDiff>): ManufacturingDiff {
  return {
    step_match_id: null,
    field: 'fk',
    alt_value: null,
    neu_value: null,
    delta_absolute: null,
    delta_percent: null,
    delta_percentage_points: null,
    status: null,
    ...over,
  }
}

const STEP_ALT: ManufacturingStepRow = {
  id: 's-alt-1',
  file_id: 'f-alt',
  process_name: 'Station Alt-Fixture',
  position_number: '10',
  source_cells: { fk: 'Fertigungskosten!W15' },
  manual_override: {
    fk: {
      sourceDescription: 'Fixture-Zelle X9',
      setBy: 'fixture-user',
      setAt: '2026-07-31T00:00:00Z',
    },
  },
  raw_values: {
    zykluszeit: 12.5,
    teileProZyklus: null,
    anzahlMA: 2,
    lohnkosten: null,
    mss: null,
    ausschuss: null,
  } as never,
}

const STEP_NEU: ManufacturingStepRow = {
  id: 's-neu-1',
  file_id: 'f-neu',
  process_name: 'Station Neu-Fixture',
  position_number: '10',
  source_cells: { fk: 'Fertigungskosten!W22' },
  raw_values: {
    zykluszeit: 11.0,
    teileProZyklus: null,
    anzahlMA: 2,
    lohnkosten: null,
    mss: null,
    ausschuss: null,
  } as never,
}

const LABOR_CANDIDATE: FieldCandidate<number> = {
  fieldId: 'cap_sum_master_labor_rate',
  module: 'SUMMARY',
  value: 42.5,
  scope: 'global',
  source: { sheet: 'Zusammenfassung', cell: 'B7' },
  confidence: 0.9,
}

describe('buildManufacturingRun — Produktionssicht (§6)', () => {
  it('baut ALT/NEU-Zeilen aus den Steps beider Dateien und meldet Verfügbarkeit', () => {
    const run = buildManufacturingRun({
      ...EMPTY_INPUT,
      baselineFileId: 'f-alt',
      comparisonFileId: 'f-neu',
      steps: [STEP_ALT, STEP_NEU],
    })
    expect(run.production.rows).toHaveLength(1)
    expect(run.production.rows[0].alt?.zykluszeit).toBe(12.5)
    expect(run.production.rows[0].neu?.zykluszeit).toBe(11.0)
    expect(run.production.availability.available).toBe(true)
    expect(run.production.count).toBe(1)
    expect(run.production.factsDe[0]).toContain('Stationen mit Raten')
  })

  it('meldet ohne auswertbare Zeilen den ehrlichen Leer-Zustand (count undefined)', () => {
    const run = buildManufacturingRun(EMPTY_INPUT)
    expect(run.production.rows).toHaveLength(0)
    expect(run.production.availability.available).toBe(false)
    expect(run.production.count).toBeUndefined()
    expect(run.production.factsDe).toEqual(['keine Raten in den Dateien'])
  })
})

describe('buildManufacturingRun — Anomalien/Master-Sätze (§7)', () => {
  it('liefert NUR belegte Master-Satz-Zeilen (der frühere Doppel-Filter des Renderers)', () => {
    const run = buildManufacturingRun({
      ...EMPTY_INPUT,
      baselineSummaryFieldCandidates: [LABOR_CANDIDATE],
    })
    // buildMasterRateRows erzeugt immer 4 Kandidaten-Zeilen — sichtbar ist
    // nur die belegte.
    expect(run.anomalies.rows).toHaveLength(1)
    expect(run.anomalies.rows[0].key).toBe('laborRate')
    expect(run.anomalies.rows[0].alt?.value).toBe(42.5)
    expect(run.anomalies.availability.available).toBe(true)
    expect(run.anomalies.factsDe[0]).toContain('Master-Sätze belegt')
  })

  it('meldet ohne Kandidaten den Leer-Zustand', () => {
    const run = buildManufacturingRun(EMPTY_INPUT)
    expect(run.anomalies.rows).toHaveLength(0)
    expect(run.anomalies.availability.available).toBe(false)
    expect(run.anomalies.factsDe).toEqual(['keine Master-Sätze gefunden'])
  })
})

describe('buildManufacturingRun — Szenario-Grenze (§11) und Methodische Grenzen', () => {
  it('pinnt die strukturelle Szenario-Grenze (immer unavailable, konstante Facts)', () => {
    const run = buildManufacturingRun(EMPTY_INPUT)
    expect(run.scenario.availability.available).toBe(false)
    expect(run.scenario.factsDe).toEqual([
      'für diesen Vergleichstyp nicht verfügbar',
    ])
  })

  it('sammelt die Gründe aller nicht befüllbaren Abschnitte (leerer Vergleich: 3)', () => {
    const run = buildManufacturingRun(EMPTY_INPUT)
    expect(run.limits.hidden).toBe(false)
    expect(run.limits.reasons).toHaveLength(3)
    expect(run.limits.factsDe[0]).toContain('3')
    expect(run.limits.factsDe[0]).toContain('Abschnitte nicht befüllbar')
  })

  it('reduziert die Grenzen auf die Szenario-Grenze, wenn §6 und §7 befüllt sind', () => {
    const run = buildManufacturingRun({
      ...EMPTY_INPUT,
      baselineFileId: 'f-alt',
      comparisonFileId: 'f-neu',
      steps: [STEP_ALT, STEP_NEU],
      baselineSummaryFieldCandidates: [LABOR_CANDIDATE],
    })
    expect(run.limits.reasons).toHaveLength(1)
    expect(run.limits.reasons[0]).toContain('Szenario-Editor')
    expect(run.limits.factsDe[0]).toContain('1')
  })
})

describe('buildManufacturingRun — A1-Zeilen-Specs', () => {
  const FULL_INPUT = {
    ...EMPTY_INPUT,
    baselineFileId: 'f-alt',
    comparisonFileId: 'f-neu',
    steps: [STEP_ALT, STEP_NEU],
    stepMatches: [
      match({ id: 'm-1', alt_step_id: 's-alt-1', neu_step_id: 's-neu-1' }),
      match({ id: 'm-unsicher', alt_step_id: 's-alt-1', match_status: 'candidate_match' }),
    ],
    diffs: [
      diffRow({
        step_match_id: 'm-1',
        field: 'fk',
        alt_value: 10,
        neu_value: 12,
        delta_absolute: 2,
        delta_percent: 0.2,
        status: 'anstieg',
      }),
      diffRow({ step_match_id: 'm-unsicher', field: 'zykluszeit' }),
      diffRow({ field: 'mss' }),
    ],
  }

  it('löst Step-Label (NEU vor ALT vor „—"), Unsicherheit und Provenienz-Zellen je Zeile auf', () => {
    const run = buildManufacturingRun(FULL_INPUT)
    const [matched, unsicher, ohneMatch] = run.deltas.rows
    expect(matched.stepLabelDe).toBe('Station Neu-Fixture')
    expect(matched.uncertain).toBe(false)
    expect(matched.altCell).toBe('Fertigungskosten!W15')
    expect(matched.neuCell).toBe('Fertigungskosten!W22')
    expect(matched.altOverride?.sourceDescription).toBe('Fixture-Zelle X9')
    expect(matched.neuOverride).toBeNull()

    // Match ohne NEU-Seite fällt aufs ALT-Label zurück; candidate_match
    // zählt als unsicher (beide Unsicherheits-Quellen leben in
    // isUncertainMatch, appendix-run.ts).
    expect(unsicher.stepLabelDe).toBe('Station Alt-Fixture')
    expect(unsicher.uncertain).toBe(true)
    // zykluszeit hat auf keiner Seite eine source_cell für dieses Feld.
    expect(unsicher.altCell).toBeNull()

    expect(ohneMatch.stepLabelDe).toBe('—')
    expect(ohneMatch.uncertain).toBe(false)

    expect(run.deltas.anyUncertain).toBe(true)
    expect(run.deltas.count).toBe(3)
  })

  it('hängt Richtung und Status-Label zur Build-Zeit an die Zeile', () => {
    const run = buildManufacturingRun(FULL_INPUT)
    expect(run.deltas.rows[0].dir).toBe('up')
    expect(run.deltas.rows[0].statusLabelDe).toBe('Anstieg ▲')
    // Zeile ohne Status: leeres Label — der Renderer zeigt dann kein Pill,
    // exakt der bisherige `{d.status && …}`-Guard.
    expect(run.deltas.rows[2].statusLabelDe).toBe('')
  })

  it('zählt die Facts über Schritte mit Abweichung — und meldet sonst „unverändert"', () => {
    const run = buildManufacturingRun(FULL_INPUT)
    expect(run.deltas.factsDe[0]).toContain('Schritte mit Abweichung')
    expect(run.deltas.factsDe[0]).toContain('1')

    const still = buildManufacturingRun({
      ...FULL_INPUT,
      diffs: [diffRow({ field: 'fk', delta_absolute: 0 })],
    })
    expect(still.deltas.factsDe[0]).toContain('Alle Prozessschritte')
  })

  it('verdrahtet Explain-Attribute inkl. Regel-Befund und reicht die konstante Provenienz durch', () => {
    const run = buildManufacturingRun({
      ...FULL_INPUT,
      plausibility: [
        {
          issue_type: 'rule_r2_fixture',
          severity: 'pruefen',
          step_label: null,
          field: 'fk',
          explanation: 'Fixture-Regel-Befund fk.',
        },
      ],
    })
    const attrs = JSON.stringify(run.deltas.rows[0].explainAttrs)
    expect(attrs).toContain('alt-fixture.xlsx')
    expect(attrs).toContain('Fixture-Regel-Befund fk.')
    // Die zykluszeit-Zeile trifft der fk-Befund NICHT.
    expect(JSON.stringify(run.deltas.rows[1].explainAttrs)).not.toContain(
      'Fixture-Regel-Befund',
    )
    expect(run.deltas.altParseConfidence).toBe(0.91)
    expect(run.deltas.neuParseConfidence).toBe(0.87)
    expect(run.deltas.engineVersionParser).toBe('9.9.9-fixture')
  })
})

describe('ruleFindingResolver — Feld-Match mit Step-Label-Scoping', () => {
  const ISSUES = [
    {
      issue_type: 'rule_step_scoped',
      severity: 'pruefen',
      step_label: 'Station Neu-Fixture',
      field: 'fk',
      explanation: 'Nur für diese Station.',
    },
    {
      issue_type: 'rule_global',
      severity: 'hinweis',
      step_label: null,
      field: 'zykluszeit',
      explanation: 'Gilt überall.',
    },
  ]

  it('trifft step-gebundene Befunde nur beim passenden Label, globale immer', () => {
    const resolve = ruleFindingResolver(ISSUES)
    expect(resolve('fk', 'Station Neu-Fixture')?.rule).toBe('rule_step_scoped')
    expect(resolve('fk', 'Andere Station')).toBeNull()
    expect(resolve('zykluszeit', 'Irgendwas')?.rule).toBe('rule_global')
    expect(resolve('zykluszeit', null)?.rule).toBe('rule_global')
    expect(resolve('unbekanntes_feld', null)).toBeNull()
  })

  it('dekodiert die Erklärung nach de/en statt den Rohstring zu fixieren', () => {
    const resolve = ruleFindingResolver(ISSUES)
    const hit = resolve('zykluszeit', null)
    expect(hit?.explanation).toBe('Gilt überall.')
  })
})
