// QVS-P5 (KAR-974) real-corpus regression — mandatory deliverable. Same
// env-gating/confidentiality discipline as mapper.real-files.test.ts /
// preview.real-files.test.ts (describe.skipIf when the confidential corpus
// is absent; no filename/cell value/price ever a committed literal; files
// discovered dynamically from splits/dev.json at run time; only a numeric
// dev.json index identifies a file in a failure message).
//
// Two checks, per the task's explicit test mandate:
//
// 1. Anchor: a file's mapped nodes diffed against THEMSELVES (source ===
//    snapshot === live) must be 100% UNCHANGED with zero unmatched steps —
//    across several distinct real files, not just one, so the anchor isn't
//    a fluke of one particular file's shape.
//
// 2. Cross-file plausibility: a NEW source diffed against a DIFFERENT file
//    "of the same family" must never crash and must produce an internally
//    consistent (summary counts == step count) classification. "Family" is
//    defined here as "same corpus `stratum`" (splits/dev.json's own
//    pre-existing template classification, e.g.
//    `standard_summary|QAF_LEGACY_DE_SUMMARY`) — reusing the corpus
//    maintainers' own grouping rather than inventing a new one, and
//    restricted to the 2 dominant SINGLE-QAF summary strata (166+48 of 227
//    dev-split entries): `confirmed_multi_qaf`/`g60_detail`/`ambiguous`
//    files are structurally different documents (Multi-QAF containers get
//    no `qaf_manufacturing_step` rows at all per architecture.md/gap-
//    analysis G5; g60_detail is a different sheet shape) and pairing across
//    them would not simulate a real reimport scenario, just an apples-to-
//    oranges comparison. Pairs are formed as CONSECUTIVE (i, i+1) entries
//    within a stratum, in the dev split's own existing (curated, stratified)
//    order — deterministic, no new randomness — capped at 16 pairs total
//    (~10-20 per the task's own guidance: full-sweep parity like the P1/P2
//    real-files suites is unnecessary here since this test targets "does
//    cross-file matching degrade gracefully", not per-file mapping fidelity,
//    which mapper.real-files.test.ts already sweeps in full).
//
// No expected ratios are asserted (real corpus content is unpredictable and
// two arbitrary revisions of two different suppliers' documents may overlap
// a lot or a little) — only that nothing crashes and every pair's counts are
// internally consistent. The aggregate distribution is logged for human
// review, same "compact coverage balance" convention as the P1 sweep test.

import { describe, it, expect } from 'vitest'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { parseQAFTemplate, type QAFRow } from '@/lib/qaf-parser'
import { mapQafRowsToVsmNodes } from '../mapper'
import { computeSyncDelta } from '../sync'
import { EDITABLE_FIELD_KEYS } from '@/components/wertstrom/vsm-field-status'
import type { QvsSyncSummary } from '../types'

const CORPUS_DIR = '/home/aria/work/qaf-corpus'
const QAF_DIR = path.join(CORPUS_DIR, 'incoming/QAFs')
const DEV_SPLIT_PATH = path.join(CORPUS_DIR, 'splits/dev.json')

/** The 2 dominant SINGLE-QAF summary strata — see module header for why
 * Multi-QAF/g60_detail/ambiguous entries are excluded from pairing. */
const PAIRABLE_STRATA = ['standard_summary|QAF_LEGACY_DE_SUMMARY', 'standard_summary|QAF_V9_SUMMARY']
const MAX_PAIRS = 16
const MAX_SELF_ANCHOR_FILES = 6

async function tryParse(fullPath: string, fileName: string): Promise<QAFRow[] | null> {
  try {
    const buffer = readFileSync(fullPath)
    const asFile = new File([new Uint8Array(buffer)], fileName, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
    return await parseQAFTemplate(asFile)
  } catch {
    return null // pre-existing parseQAFTemplate behavior for structurally different files — not a QVS-P5 concern, see mapper.real-files.test.ts
  }
}

function emptySummaryTally(): QvsSyncSummary {
  return { unchanged: 0, sourceChanged: 0, localChanged: 0, bothChanged: 0, newInSource: 0, removedFromSource: 0, unmatched: 0 }
}

describe.skipIf(!existsSync(QAF_DIR) || !existsSync(DEV_SPLIT_PATH))('computeSyncDelta — real-corpus regression (QVS-P5 mandatory deliverable)', () => {
  const dev = existsSync(DEV_SPLIT_PATH) ? (JSON.parse(readFileSync(DEV_SPLIT_PATH, 'utf8')) as Array<{ file: string; stratum: string }>) : []

  it(
    'a file diffed against ITSELF is 100% UNCHANGED with zero unmatched steps (anchor, across several distinct real files)',
    async () => {
      expect(dev.length).toBeGreaterThan(0)
      let anchored = 0

      for (let idx = 0; idx < dev.length && anchored < MAX_SELF_ANCHOR_FILES; idx++) {
        const fullPath = path.join(QAF_DIR, dev[idx].file)
        if (!existsSync(fullPath)) continue
        const rows = await tryParse(fullPath, dev[idx].file)
        if (!rows || !rows.some((r) => r.prozessbezeichnung?.trim())) continue

        const nodes = mapQafRowsToVsmNodes(rows).nodes
        const delta = computeSyncDelta({ sourceNodes: nodes, snapshotNodes: nodes, liveNodes: nodes })

        expect(delta.unmatched, `dev-split index ${idx}: self-diff produced unmatched steps`).toEqual([])
        expect(delta.steps.every((s) => s.status === 'UNCHANGED'), `dev-split index ${idx}: self-diff produced a non-UNCHANGED step`).toBe(true)
        expect(delta.summary.unchanged).toBe(delta.steps.length)
        anchored++
      }

      expect(anchored, 'no dev-split file could be parsed into ≥1 step to anchor against').toBeGreaterThan(0)
    },
    120_000,
  )

  it(
    'delta against a DIFFERENT file of the same corpus stratum never crashes and produces an internally consistent, plausible class distribution',
    async () => {
      const byStratum = new Map<string, string[]>()
      for (const { file, stratum } of dev) {
        if (!PAIRABLE_STRATA.includes(stratum)) continue
        const list = byStratum.get(stratum) ?? []
        list.push(file)
        byStratum.set(stratum, list)
      }

      const pairs: Array<[string, string]> = []
      for (const files of byStratum.values()) {
        for (let i = 0; i + 1 < files.length && pairs.length < MAX_PAIRS; i += 2) {
          pairs.push([files[i], files[i + 1]])
        }
        if (pairs.length >= MAX_PAIRS) break
      }
      expect(pairs.length, 'no pairable-stratum files found — corpus/split layout likely drifted').toBeGreaterThan(0)

      const tally = emptySummaryTally()
      let pairsCompared = 0
      let pairsSkippedUnparseable = 0

      for (let i = 0; i < pairs.length; i++) {
        const [fileA, fileB] = pairs[i]
        const pathA = path.join(QAF_DIR, fileA)
        const pathB = path.join(QAF_DIR, fileB)
        if (!existsSync(pathA) || !existsSync(pathB)) {
          pairsSkippedUnparseable++
          continue
        }
        const [rowsA, rowsB] = await Promise.all([tryParse(pathA, fileA), tryParse(pathB, fileB)])
        if (!rowsA || !rowsB || !rowsA.some((r) => r.prozessbezeichnung?.trim()) || !rowsB.some((r) => r.prozessbezeichnung?.trim())) {
          pairsSkippedUnparseable++
          continue
        }

        const newSourceNodes = mapQafRowsToVsmNodes(rowsA).nodes
        const previouslyImportedNodes = mapQafRowsToVsmNodes(rowsB).nodes

        let delta: ReturnType<typeof computeSyncDelta>
        try {
          delta = computeSyncDelta({ sourceNodes: newSourceNodes, snapshotNodes: previouslyImportedNodes, liveNodes: previouslyImportedNodes })
        } catch (e) {
          throw new Error(`computeSyncDelta threw for real-corpus pair #${i}: ${e instanceof Error ? e.message : String(e)}`)
        }

        const summedFromSteps =
          delta.summary.unchanged + delta.summary.sourceChanged + delta.summary.localChanged + delta.summary.bothChanged + delta.summary.newInSource + delta.summary.removedFromSource
        expect(summedFromSteps, `pair #${i}: summary counts don't add up to steps.length`).toBe(delta.steps.length)
        expect(delta.summary.unmatched, `pair #${i}: summary.unmatched doesn't match unmatched.length`).toBe(delta.unmatched.length)

        for (const step of delta.steps) {
          const expectedFieldCount = step.status === 'NEW_IN_SOURCE' || step.status === 'REMOVED_FROM_SOURCE' ? 0 : EDITABLE_FIELD_KEYS.length
          expect(step.fields.length, `pair #${i}: step "${step.name}" (${step.status}) has an unexpected field-delta count`).toBe(expectedFieldCount)
        }

        tally.unchanged += delta.summary.unchanged
        tally.sourceChanged += delta.summary.sourceChanged
        tally.localChanged += delta.summary.localChanged
        tally.bothChanged += delta.summary.bothChanged
        tally.newInSource += delta.summary.newInSource
        tally.removedFromSource += delta.summary.removedFromSource
        tally.unmatched += delta.summary.unmatched
        pairsCompared++
      }

      const totalSteps = tally.unchanged + tally.sourceChanged + tally.localChanged + tally.bothChanged + tally.newInSource + tally.removedFromSource
      // no-console is off for *.test.ts (consoleDisciplineTestOverrides, eslint.config.mjs).
      console.log(
        `[QVS-P5 real-corpus cross-file] pairsPlanned=${pairs.length} pairsCompared=${pairsCompared} pairsSkipped=${pairsSkippedUnparseable} ` +
          `totalSteps=${totalSteps} unchanged=${tally.unchanged} sourceChanged=${tally.sourceChanged} localChanged=${tally.localChanged} ` +
          `bothChanged=${tally.bothChanged} newInSource=${tally.newInSource} removedFromSource=${tally.removedFromSource} unmatched=${tally.unmatched}`,
      )

      expect(pairsCompared, 'every planned pair failed to parse — corpus layout likely drifted').toBeGreaterThan(0)
      // localChanged/bothChanged require a local edit, which neither side of a
      // pure file-vs-file comparison ever produces (liveNodes === snapshotNodes
      // here) — asserted as an explicit, understood-not-a-bug zero, not an
      // accidental omission.
      expect(tally.localChanged, 'a pure file-vs-file comparison (no local edits) should never produce LOCAL_CHANGED').toBe(0)
      expect(tally.bothChanged, 'a pure file-vs-file comparison (no local edits) should never produce BOTH_CHANGED').toBe(0)
      expect(totalSteps + tally.unmatched, 'no steps were classified at all across every compared pair').toBeGreaterThan(0)
    },
    180_000,
  )
})
