// KAR-937/Multi-QAF-Programm P2.3 — container-differ.ts real-file regression
// (env-gated).
//
// Env-gated (describe.skipIf when the confidential input directory/manifest
// is absent — same discipline as container-assembly.real-files.test.ts and
// variant-matcher.real-files.test.ts) real-file proof for two things:
//   (a) self-diff: every one of the 4 real Multi-QAF files' own assembled
//       container, diffed against a serialize/deserialize round-trip copy
//       of itself (matched 100% at stage raw_exact, per variant-matcher.
//       real-files.test.ts's own already-established baseline), produces a
//       MultiQafContainerDiff with ZERO findings in every finding group —
//       the single most important regression anchor for a structural diff:
//       it must never manufacture a change where none exists.
//   (b) one real cross-file diff (Datei 2 vs. Datei 3, 0-based manifest
//       index — both container-assembly.real-files.test.ts's own pinned
//       EXPECTED_FAMILY entries are 'm_qaf_1_0', EXPECTED_ACTIVE_COUNT are 5
//       and 8) produces a plausible structural diff — aggregate counts only.
//
// Confidentiality discipline (KAR-926 F2 precedent, followed exactly): files
// addressed by MANIFEST POSITION (0-3) only, never by name, codename, or
// content. Only AGGREGATE structural facts are asserted (counts, booleans) —
// never a filename, project/vehicle code, part designation, volume figure,
// or price.
//
// tdd-guard:skip — real-file regression/observation test, not a unit-level
// behavior spec (behavior is unit-tested in container-differ.test.ts via
// synthetic fixtures).

import { describe, it, expect } from 'vitest'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { loadExcelWorkbook } from '../../workbook-adapter'
import { detectMultiQaf, multiQafDetectionInputFromExcelJs } from '../../qaf-type-detector'
import { serializeMultiQafContainer, deserializeMultiQafContainer } from '../serialization'
import { assembleMultiQafContainer } from '../container-assembly'
import { matchVariants } from '../variant-matcher'
import { allMultiQafContainerVariants, diffContainers } from '../container-differ'

const INPUT_DIR = '/root/aria/work/qaf-compare-kar824/input'
const MULTI_QAF_MANIFEST = path.join(INPUT_DIR, 'multi-qaf-files.txt')

function readMultiQafManifest(): string[] {
  if (!existsSync(MULTI_QAF_MANIFEST)) return []
  return readFileSync(MULTI_QAF_MANIFEST, 'utf-8')
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line !== '' && !line.startsWith('#'))
}

const MULTI_QAF_FILES: readonly string[] = readMultiQafManifest()

async function assembleFile(index: number) {
  const fileName = MULTI_QAF_FILES[index]
  const buffer = readFileSync(path.join(INPUT_DIR, fileName))
  const wb = await loadExcelWorkbook(buffer)
  const detection = detectMultiQaf(multiQafDetectionInputFromExcelJs(wb))
  return assembleMultiQafContainer({ worksheets: wb.worksheets }, detection, { fileName: null, fileHash: null })
}

describe.skipIf(!existsSync(INPUT_DIR) || !existsSync(MULTI_QAF_MANIFEST) || readMultiQafManifest().length < 4)(
  'KAR-937 — container-differ real-file regression (env-gated)',
  () => {
    it.each([0, 1, 2, 3])('Datei %i: self-diff (round-trip copy) has zero findings in every group', async (index) => {
      const container = await assembleFile(index)
      // Round-trip through the module's own canonical serialize/deserialize
      // pair (serialization.ts) rather than a raw structural-clone helper —
      // this is the SAME persistence path a real P2.3-compare-flow caller
      // would use to load "the other side" from storage, so the self-diff
      // anchor also exercises that round-trip stays diff-invisible.
      const selfCopy = deserializeMultiQafContainer(serializeMultiQafContainer(container))

      const matchResult = matchVariants(allMultiQafContainerVariants(container), allMultiQafContainerVariants(selfCopy))
      const diff = diffContainers(container, selfCopy, matchResult)

      console.log(
        `[KAR-937 self-diff Datei ${index}] added=${diff.variants.added.length} removed=${diff.variants.removed.length} renamed=${diff.variants.renamed.length} reordered=${diff.variants.reordered.length} uncertain=${diff.variants.uncertainMatches.length} activeState=${diff.variants.activeStateChanges.length} detailLinkage=${diff.variants.detailLinkageTransitions.length} detailWithoutSummary=${diff.variants.detailWithoutSummary.added.length + diff.variants.detailWithoutSummary.removed.length} summaryWithoutDetail=${diff.variants.summaryWithoutDetail.length} dims=${diff.dimensions.added.length + diff.dimensions.removed.length} material=${diff.sharedMaterial.addedRows.length + diff.sharedMaterial.removedRows.length + diff.sharedMaterial.changedRows.length} profiles=${diff.profiles.added.length + diff.profiles.removed.length + diff.profiles.volumeBandThresholdChanges.length + diff.profiles.bindingChanges.length} templateFamilyChanged=${diff.template.family.changed} sheetSet=${diff.template.sheetSet.added.length + diff.template.sheetSet.removed.length + diff.template.sheetSet.becameHidden.length + diff.template.sheetSet.becameVisible.length}`,
      )

      expect(diff.variants.added).toEqual([])
      expect(diff.variants.removed).toEqual([])
      expect(diff.variants.renamed).toEqual([])
      expect(diff.variants.reordered).toEqual([])
      expect(diff.variants.uncertainMatches).toEqual([])
      expect(diff.variants.activeStateChanges).toEqual([])
      expect(diff.variants.detailLinkageTransitions).toEqual([])
      expect(diff.variants.detailWithoutSummary.added).toEqual([])
      expect(diff.variants.detailWithoutSummary.removed).toEqual([])
      expect(diff.variants.summaryWithoutDetail).toEqual([])
      expect(diff.dimensions.added).toEqual([])
      expect(diff.dimensions.removed).toEqual([])
      expect(diff.sharedMaterial.addedRows).toEqual([])
      expect(diff.sharedMaterial.removedRows).toEqual([])
      expect(diff.sharedMaterial.changedRows).toEqual([])
      expect(diff.sharedMaterial.structure.altRowCount).toBe(diff.sharedMaterial.structure.neuRowCount)
      expect(diff.profiles.added).toEqual([])
      expect(diff.profiles.removed).toEqual([])
      expect(diff.profiles.volumeBandThresholdChanges).toEqual([])
      expect(diff.profiles.bindingChanges).toEqual([])
      expect(diff.template.family.changed).toBe(false)
      expect(diff.template.sheetSet).toEqual({ added: [], removed: [], becameHidden: [], becameVisible: [] })
    }, 120_000)

    it('Datei 2 vs. Datei 3 (0-based manifest index; both m_qaf_1_0 per container-assembly.real-files.test.ts): a plausible, internally consistent cross-file structural diff', async () => {
      const alt = await assembleFile(2)
      const neu = await assembleFile(3)
      const altAll = allMultiQafContainerVariants(alt)
      const neuAll = allMultiQafContainerVariants(neu)
      const matchResult = matchVariants(altAll, neuAll)
      const diff = diffContainers(alt, neu, matchResult)

      const matchedCount = matchResult.filter((r) => r.kind === 'matched').length

      console.log(
        `[KAR-937 cross-diff Datei2-vs-Datei3] altVariants=${altAll.length} neuVariants=${neuAll.length} matched=${matchedCount} added=${diff.variants.added.length} removed=${diff.variants.removed.length} renamed=${diff.variants.renamed.length} reordered=${diff.variants.reordered.length} uncertain=${diff.variants.uncertainMatches.length} activeState=${diff.variants.activeStateChanges.length} dims.added=${diff.dimensions.added.length} dims.removed=${diff.dimensions.removed.length} templateFamilyChanged=${diff.template.family.changed} reviewRequired=${diff.reviewRequired}`,
      )

      // Fail-closed contract: every alt/neu variant index is covered EXACTLY
      // once across added/removed/renamed-or-matched/uncertain (inherited
      // from matchVariants — this asserts the container-differ.ts layer
      // does not lose or duplicate anything on top of that guarantee).
      const uncertainLeftCount = diff.variants.uncertainMatches.reduce(
        (n, r) => n + (r.kind === 'ambiguous' ? r.leftIndices.length : r.kind === 'split_suspected' ? 1 : r.kind === 'merge_suspected' ? r.leftIndices.length : 0),
        0,
      )
      const uncertainRightCount = diff.variants.uncertainMatches.reduce(
        (n, r) => n + (r.kind === 'ambiguous' ? r.rightIndices.length : r.kind === 'split_suspected' ? r.rightIndices.length : r.kind === 'merge_suspected' ? 1 : 0),
        0,
      )
      expect(diff.variants.removed.length + matchedCount + uncertainLeftCount).toBe(altAll.length)
      expect(diff.variants.added.length + matchedCount + uncertainRightCount).toBe(neuAll.length)

      // Real-measured, pinned aggregate shape (observed via this suite's own
      // console.log — Datei 2/Datei 3 are two structurally unrelated real
      // vehicle programmes that both happen to be m_qaf_1_0, container-
      // assembly.real-files.test.ts's own pinned EXPECTED_ACTIVE_COUNT are 5
      // and 8): zero raw_exact/core_subset_exact/fuzzy identity overlap at
      // all between the two — every one of Datei 2's 5 variants reports
      // 'removed', every one of Datei 3's 8 reports 'added', 0 matched, 0
      // uncertain. A future change that silently creates a spurious match
      // between two unrelated real files' variants now fails a pinned
      // assertion instead of staying green under a loose
      // `toBeGreaterThan(0)`. Container-wide dimension-key churn (4
      // added/5 removed) and `reviewRequired: true` (5+8 add/remove
      // findings) are pinned the same way.
      expect(matchedCount).toBe(0)
      expect(diff.variants.removed.length).toBe(5)
      expect(diff.variants.added.length).toBe(8)
      expect(diff.variants.renamed).toEqual([])
      expect(diff.variants.reordered).toEqual([])
      expect(diff.variants.uncertainMatches).toEqual([])
      expect(diff.dimensions.added.length).toBe(4)
      expect(diff.dimensions.removed.length).toBe(5)
      expect(diff.template.family.changed).toBe(false)
      expect(diff.reviewRequired).toBe(true)
    }, 120_000)
  },
)
