// KAR-939/Multi-QAF-Programm P2.5 — profile-differ.ts real-file regression
// (env-gated).
//
// Env-gated (describe.skipIf when the confidential input directory/manifest
// is absent — same discipline as container-differ.real-files.test.ts and
// material-differ.real-files.test.ts) real-file proof for two things:
//   (a) self-diff anchor: 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 ProfileDiffResult with ZERO findings in every
//       one of the 8 groups — the single most important regression anchor
//       for a value-level diff: it must never manufacture a change where
//       none exists, even though (per profile-parser.real-files.test.ts's
//       own "Datei 2" evidence) some real files carry HONEST pre-existing
//       gaps (unbound variant columns) — see profile-differ.ts's own module
//       header "Fail-closed self-diff-safety discipline" for why groups 7/8
//       are set-diffs specifically so this anchor holds regardless.
//   (b) in-memory profile-total mutation proof: take one real file's own
//       assembled container, deep-clone it, and mutate ONE bound profile's
//       `values.totalPerUnit` by +5% (leaving `formulaAndCachedValue.
//       cachedValue` untouched — see profile-differ.ts's own module header
//       "Item 8 honesty fix" for why this specific asymmetric mutation is
//       what exercises BOTH item 2 and item 8 in one proof) — diffProfiles
//       must report EXACTLY 1 `totalChanges` finding, whose
//       `affectedVariantIds` are exactly the variants bound to that profile.
//       Re-audited post KAR-939 adversarial review F1/F4 (item 8's
//       PRIMARY comparison — `boundProfileValue` vs `virtualVariantTotal` —
//       is now provably tautological, since both derive from the SAME
//       profile's `values.totalPerUnit`; ONLY `cachedValueDrift`, an
//       in-profile `cachedValue`-vs-`totalPerUnit` comparison, can fire):
//       `summaryReconciliation.added` must independently corroborate the
//       SAME variant set as a genuine `cachedValueDrift`-driven
//       `'abweichung'`, and `summaryReconciliation.removed` must show the
//       SAME variants' ALT-side `nicht_pruefbar`/
//       `same_source_no_independent_check` entries disappearing — NEVER a
//       false 'bestanden' (empty `added`) the way the pre-fix silent-skip
//       behavior could have produced regardless of whether the underlying
//       comparison logic was even correct.
//
// 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 profile-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 } from '../container-differ'
import { diffProfiles } from '../profile-differ'
import type { MultiQafContainer, SharedCostProfile } from '../types'

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): Promise<MultiQafContainer> {
  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 })
}

/** Locates a real, currently-bound profile with a numeric `totalPerUnit`
 * (mutation target) plus which of the 3 profile arrays it lives in (needed
 * to rebuild the mutated container immutably) and the set of variant ids
 * currently bound to it — a representative, always-present shape rather than
 * a hand-picked profile, so this test stays stable across a future re-parse
 * of the same file. */
function findMutationTarget(container: MultiQafContainer): {
  arrayKey: 'sharedManufacturingProfiles' | 'setupCostProfiles' | 'sharedToolingData'
  profile: SharedCostProfile
  originalTotal: number
  boundVariantIds: string[]
} | null {
  const arrayKeys = ['sharedManufacturingProfiles', 'setupCostProfiles', 'sharedToolingData'] as const
  for (const arrayKey of arrayKeys) {
    for (const profile of container[arrayKey]) {
      const originalTotal = profile.values.totalPerUnit
      if (typeof originalTotal !== 'number') continue
      const boundVariantIds = container.variantProfileBindings.filter((b) => b.profileId === profile.profileId).map((b) => b.variantId)
      if (boundVariantIds.length === 0) continue
      return { arrayKey, profile, originalTotal, boundVariantIds: boundVariantIds.sort() }
    }
  }
  return null
}

describe.skipIf(!existsSync(INPUT_DIR) || !existsSync(MULTI_QAF_MANIFEST) || readMultiQafManifest().length < 4)(
  'KAR-939 — profile-differ real-file regression (env-gated)',
  () => {
    it.each([0, 1, 2, 3])('Datei %i: self-diff (round-trip copy) has zero findings in every one of the 8 groups', async (index) => {
      const container = await assembleFile(index)
      const selfCopy = deserializeMultiQafContainer(serializeMultiQafContainer(container))

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

      console.log(
        `[KAR-939 self-diff Datei ${index}] componentValueChanges=${diff.componentValueChanges.length} totalChanges=${diff.totalChanges.length} bindingValueImpacts=${diff.bindingValueImpacts.length} added=${diff.added.length} removed=${diff.removed.length} inconsistentBindings.added=${diff.inconsistentBindings.added.length} inconsistentBindings.removed=${diff.inconsistentBindings.removed.length} summaryReconciliation.added=${diff.summaryReconciliation.added.length} summaryReconciliation.removed=${diff.summaryReconciliation.removed.length} summaryReconciliation.changed=${diff.summaryReconciliation.changed.length} uncertain=${diff.uncertainMatches.length}`,
      )

      expect(diff.componentValueChanges).toEqual([])
      expect(diff.totalChanges).toEqual([])
      expect(diff.bindingValueImpacts).toEqual([])
      expect(diff.added).toEqual([])
      expect(diff.removed).toEqual([])
      expect(diff.inconsistentBindings.added).toEqual([])
      expect(diff.inconsistentBindings.removed).toEqual([])
      // KAR-939 adversarial review F1: post-fix, every resolvable binding on
      // a real freshly-parsed file now produces an EXPLICIT
      // nicht_pruefbar/same_source_no_independent_check entry (values.
      // totalPerUnit and formulaAndCachedValue.cachedValue are populated
      // from the identical parse-time source — profile-parser.ts's
      // `buildProfileFromCandidate`) instead of the old silent skip. Both
      // sides of this self-diff produce the IDENTICAL entry (same variant/
      // profile/status/reason, deltaAbsolute both null), so it still
      // cancels out to zero here — see profile-differ.ts's own module
      // header "Item 8 honesty fix".
      expect(diff.summaryReconciliation.added).toEqual([])
      expect(diff.summaryReconciliation.removed).toEqual([])
      expect(diff.summaryReconciliation.changed).toEqual([])
      expect(diff.uncertainMatches).toEqual([])
    }, 120_000)

    it('a real bound profile total mutated by +5% (values.totalPerUnit only) produces exactly 1 totalChanges finding with the correct affected variants, corroborated by summaryReconciliation', async () => {
      let container: MultiQafContainer | null = null
      let target: ReturnType<typeof findMutationTarget> = null
      for (let index = 0; index < 4; index++) {
        const candidate = await assembleFile(index)
        const found = findMutationTarget(candidate)
        if (found) {
          container = candidate
          target = found
          break
        }
      }
      expect(container).not.toBeNull()
      expect(target).not.toBeNull()
      const { arrayKey, profile, originalTotal, boundVariantIds } = target!

      const mutatedTotal = originalTotal * 1.05
      const mutated: MultiQafContainer = {
        ...container!,
        [arrayKey]: container![arrayKey].map((p) => (p.profileId === profile.profileId ? { ...p, values: { ...p.values, totalPerUnit: mutatedTotal } } : p)),
      }

      const matchResult = matchVariants(allMultiQafContainerVariants(container!), allMultiQafContainerVariants(mutated))
      const diff = diffProfiles(container!, mutated, matchResult)

      console.log(
        `[KAR-939 mutation-proof] boundVariants=${boundVariantIds.length} totalChanges=${diff.totalChanges.length} affectedVariants=${diff.totalChanges[0]?.affectedVariantIds.length ?? 0} summaryReconciliation.added=${diff.summaryReconciliation.added.length} componentValueChanges=${diff.componentValueChanges.length} bindingValueImpacts=${diff.bindingValueImpacts.length}`,
      )

      expect(diff.totalChanges).toHaveLength(1)
      const totalFinding = diff.totalChanges[0]!
      expect(totalFinding.altTotal).toBeCloseTo(originalTotal, 6)
      expect(totalFinding.neuTotal).toBeCloseTo(mutatedTotal, 6)
      expect(totalFinding.affectedVariantIds).toEqual(boundVariantIds)

      // No unrelated finding groups fired from this single-field mutation.
      expect(diff.componentValueChanges).toEqual([])
      expect(diff.added).toEqual([])
      expect(diff.removed).toEqual([])
      expect(diff.bindingValueImpacts).toEqual([])
      expect(diff.inconsistentBindings.added).toEqual([])
      expect(diff.inconsistentBindings.removed).toEqual([])

      // Item-8 cross-check (KAR-939 adversarial review F1/F4 — re-verified
      // post-fix, see profile-differ.ts's own module header "Item 8 honesty
      // fix"): `formulaAndCachedValue.cachedValue` was left untouched by the
      // mutation while `values.totalPerUnit` (the PRIMARY source for BOTH
      // `boundProfileValue` and `generateVirtualVariants`' own total, F4)
      // moved. On ALT, cachedValue === totalPerUnit (same parse-time source)
      // -> `nicht_pruefbar`/`same_source_no_independent_check` for every one
      // of this profile's bound variants (an honest "cannot check" report,
      // not a fabricated pass). On NEU, cachedValue (unchanged) now diverges
      // from totalPerUnit (mutated) WITHIN that single profile object — the
      // ONE genuinely independent signal this module can catch today
      // (`cachedValueDrift`) — so NEU reports a REAL `'abweichung'` for the
      // very same variants. Different status on each side -> different full
      // keys -> NEU's abweichung entries surface in `added`, ALT's
      // same-source entries surface in `removed` — this must NEVER present
      // as a false 'bestanden' (empty added) the way the pre-fix silent-skip
      // behavior could have masked.
      const cachedValueAlsoMutated = typeof profile.formulaAndCachedValue?.cachedValue === 'number' && profile.formulaAndCachedValue.cachedValue === mutatedTotal
      if (!cachedValueAlsoMutated) {
        const addedForProfile = diff.summaryReconciliation.added.filter((e) => e.profileId === profile.profileId)
        const removedForProfile = diff.summaryReconciliation.removed.filter((e) => e.profileId === profile.profileId)

        expect(addedForProfile.map((e) => e.variantId).sort()).toEqual(boundVariantIds)
        expect(addedForProfile.every((e) => e.status === 'abweichung')).toBe(true)
        expect(addedForProfile.every((e) => e.cachedValueDrift !== null)).toBe(true)

        expect(removedForProfile.map((e) => e.variantId).sort()).toEqual(boundVariantIds)
        expect(removedForProfile.every((e) => e.status === 'nicht_pruefbar' && e.nichtPruefbarReason === 'same_source_no_independent_check')).toBe(true)

        // Same base key, DIFFERENT status on both sides -> already fully
        // visible via added+removed above -> never ALSO double-reported as
        // `changed` (F3 is reserved for same-status/different-delta pairs).
        expect(diff.summaryReconciliation.changed.filter((e) => e.profileId === profile.profileId)).toEqual([])
      }
    }, 120_000)
  },
)
