// Tests for multi-qaf/identity.ts (KAR-929 / Multi-QAF-Programm P1.1).
//
// Covers: composite-canonical-key determinism/stability (Master-Prompt §9
// reorder/rename scenarios), and active/inactive/reserved derivation
// calibrated against the real MX 25/12/10-slot pattern (10-analyse-mx.md C).

import { describe, expect, it } from 'vitest'
import {
  buildCompositeCanonicalKey,
  deriveActiveState,
  detectCanonicalKeyCollisions,
  disambiguateCanonicalKeys,
  hasVariantIdentity,
  makeDimensionValue,
  normalizeDimensionValue,
} from '../identity'
import type { VariantDimensions } from '../types'

function dims(entries: Record<string, string>): VariantDimensions {
  const out: Record<string, ReturnType<typeof makeDimensionValue>> = {}
  for (const [k, v] of Object.entries(entries)) out[k] = makeDimensionValue(v)
  return out
}

describe('normalizeDimensionValue', () => {
  it('lowercases and folds umlauts/whitespace (reuses normalizer.ts)', () => {
    expect(normalizeDimensionValue('Rechtslenker')).toBe('rechtslenker')
    expect(normalizeDimensionValue('  AWD  ')).toBe('awd')
    expect(normalizeDimensionValue('Grösse')).toBe('groesse')
  })

  it('returns empty string for blank input', () => {
    expect(normalizeDimensionValue('')).toBe('')
    expect(normalizeDimensionValue('   ')).toBe('')
  })
})

describe('buildCompositeCanonicalKey', () => {
  it('is deterministic for the same dimension values regardless of Record key insertion order', () => {
    const a = dims({ vehicle: 'Q7X', driveType: 'AWD', region: 'EU' })
    const b: VariantDimensions = {
      region: makeDimensionValue('EU'),
      vehicle: makeDimensionValue('Q7X'),
      driveType: makeDimensionValue('AWD'),
    }
    expect(buildCompositeCanonicalKey(a)).toBe(buildCompositeCanonicalKey(b))
  })

  it('known dimensions are emitted in KNOWN_VARIANT_DIMENSION_KEYS order', () => {
    const key = buildCompositeCanonicalKey(dims({ motor: 'ENG-A', vehicle: 'Q7X', driveType: 'AWD' }))
    // vehicle, then driveType, then motor — matches KNOWN_VARIANT_DIMENSION_KEYS order.
    expect(key).toBe('vehicle=q7x|driveType=awd|motor=eng-a')
  })

  it('is column-reorder-invariant: identical dimensions produce identical keys regardless of where the caller found them', () => {
    // Simulates the same logical variant appearing in column Q in ALT and
    // column AF in NEU (Master-Prompt §9 "reordered variants").
    const altVariantDims = dims({ vehicle: 'Q71', driveType: 'RWD', steeringSide: 'LL' })
    const neuVariantDims = dims({ vehicle: 'Q71', driveType: 'RWD', steeringSide: 'LL' })
    expect(buildCompositeCanonicalKey(altVariantDims)).toBe(buildCompositeCanonicalKey(neuVariantDims))
  })

  it('renamed-but-equivalent labels (case/whitespace) collapse to the same key', () => {
    const a = buildCompositeCanonicalKey(dims({ driveType: 'AWD', steeringSide: 'LL' }))
    const b = buildCompositeCanonicalKey(dims({ driveType: '  awd', steeringSide: 'll ' }))
    expect(a).toBe(b)
  })

  it('drops empty/blank dimension values instead of encoding empty segments', () => {
    const key = buildCompositeCanonicalKey(dims({ vehicle: 'Q7X', region: '', driveType: '   ' }))
    expect(key).toBe('vehicle=q7x')
    expect(key).not.toContain('region=')
    expect(key).not.toContain('driveType=')
  })

  it('unknown dimension keys are appended alphabetically after known keys', () => {
    const key = buildCompositeCanonicalKey(dims({ zCustomDim: 'Z1', aCustomDim: 'A1', vehicle: 'Q7X' }))
    expect(key).toBe('vehicle=q7x|aCustomDim=a1|zCustomDim=z1')
  })

  it('extends to a brand-new dimension key with zero code change (extensibility requirement, Master-Prompt §6)', () => {
    const key = buildCompositeCanonicalKey(dims({ neverSeenBeforeDimension: 'Foo' }))
    expect(key).toBe('neverSeenBeforeDimension=foo')
  })

  it('falls back to the normalized fallback string when every dimension is empty', () => {
    expect(buildCompositeCanonicalKey({}, 'Q71.BL2K')).toBe('fallback=q71.bl2k')
  })

  it('returns an empty string when there are no dimensions and no fallback', () => {
    expect(buildCompositeCanonicalKey({})).toBe('')
    expect(buildCompositeCanonicalKey({}, null)).toBe('')
    expect(buildCompositeCanonicalKey({}, '')).toBe('')
  })
})

describe('hasVariantIdentity', () => {
  it('is true when at least one dimension is non-empty', () => {
    expect(hasVariantIdentity(dims({ vehicle: 'Q7X' }), [])).toBe(true)
  })

  it('is true when at least one original label is non-empty, even with no dimensions', () => {
    expect(hasVariantIdentity({}, ['V201_LC1'])).toBe(true)
  })

  it('is false when both dimensions and labels are empty/blank', () => {
    expect(hasVariantIdentity({}, [])).toBe(false)
    expect(hasVariantIdentity(dims({ region: '' }), ['   '])).toBe(false)
  })
})

describe('deriveActiveState', () => {
  it('is reserved when there is no identity at all (MX slots 13-25)', () => {
    expect(
      deriveActiveState(false, { annualVolume: null, peakVolume: null, lifetimeVolume: null })
    ).toBe('reserved')
    // Even a stray positive volume without identity is still reserved —
    // identity is the gating signal, not volume.
    expect(deriveActiveState(false, { annualVolume: 100, peakVolume: null, lifetimeVolume: null })).toBe(
      'reserved'
    )
  })

  it('is inactive when there is identity but no positive volume on any field (MX slots 4/8)', () => {
    expect(deriveActiveState(true, { annualVolume: 0, peakVolume: 0, lifetimeVolume: 0 })).toBe('inactive')
    expect(deriveActiveState(true, { annualVolume: null, peakVolume: null, lifetimeVolume: null })).toBe(
      'inactive'
    )
  })

  it('is active when there is identity and any volume field is positive', () => {
    expect(deriveActiveState(true, { annualVolume: 84000, peakVolume: null, lifetimeVolume: null })).toBe(
      'active'
    )
    expect(deriveActiveState(true, { annualVolume: 0, peakVolume: 0, lifetimeVolume: 220000 })).toBe(
      'active'
    )
  })

  it('matches the real 25/12/10-slot wide-slot pattern end to end', () => {
    // 12 slots with identity, 10 of which also have volume > 0, 2 with
    // identity but zero volume, 13 fully reserved (10-analyse-mx.md C
    // structural pattern — see synthetic-fixtures.ts buildWideSlotFixture).
    const activeSlots = Array.from({ length: 10 }, () =>
      deriveActiveState(true, { annualVolume: 1, peakVolume: null, lifetimeVolume: null })
    )
    const inactiveSlots = Array.from({ length: 2 }, () =>
      deriveActiveState(true, { annualVolume: 0, peakVolume: 0, lifetimeVolume: 0 })
    )
    const reservedSlots = Array.from({ length: 13 }, () =>
      deriveActiveState(false, { annualVolume: null, peakVolume: null, lifetimeVolume: null })
    )
    expect(activeSlots.filter((s) => s === 'active')).toHaveLength(10)
    expect(inactiveSlots.filter((s) => s === 'inactive')).toHaveLength(2)
    expect(reservedSlots.filter((s) => s === 'reserved')).toHaveLength(13)
  })
})

// ── Canonical-key collision detection + disambiguation (KAR-929 adversarial
// review F2) ──────────────────────────────────────────────────────────────

describe('detectCanonicalKeyCollisions', () => {
  it('flags two REAL variants that share every dimension value (same-dimensions collision)', () => {
    const key = buildCompositeCanonicalKey(dims({ vehicle: 'Q71', driveType: 'AWD' }))
    const warnings = detectCanonicalKeyCollisions([
      { stableInternalId: 'v-a', compositeCanonicalKey: key },
      { stableInternalId: 'v-b', compositeCanonicalKey: key },
    ])
    expect(warnings).toHaveLength(1)
    expect(warnings[0].code).toBe('canonical_key_collision')
    expect(warnings[0].message).toContain('v-a')
    expect(warnings[0].message).toContain('v-b')
    // KAR-935 adversarial-review F5 fix: this message never starts with the
    // "Variante ${id}: " DE-prefix convention (it names BOTH colliding
    // variants at once) — container-assembly.ts's own `variantWarningsFor`
    // used to rely on that prefix exclusively, so a collision like this one
    // could never reach either variant's own `.warnings`. `variantIds` is
    // the structural fix.
    expect(warnings[0].variantIds).toEqual(['v-a', 'v-b'])
    // KAR-935 F4 fix: a genuine collision is review-worthy.
    expect(warnings[0].reviewRelevant).toBe(true)
  })

  it('flags two identity-less reserved slots that both fall back to the empty key (double-reserved collision)', () => {
    const emptyKey = buildCompositeCanonicalKey({})
    expect(emptyKey).toBe('')
    const warnings = detectCanonicalKeyCollisions([
      { stableInternalId: 'reserved-1', compositeCanonicalKey: emptyKey },
      { stableInternalId: 'reserved-2', compositeCanonicalKey: emptyKey },
    ])
    expect(warnings).toHaveLength(1)
    expect(warnings[0].code).toBe('canonical_key_collision')
    expect(warnings[0].severity).toBe('warning')
    expect(warnings[0].variantIds).toEqual(['reserved-1', 'reserved-2'])
    expect(warnings[0].reviewRelevant).toBe(true)
  })

  it('reports no collision when every key is unique', () => {
    const warnings = detectCanonicalKeyCollisions([
      { stableInternalId: 'v-a', compositeCanonicalKey: 'vehicle=q71' },
      { stableInternalId: 'v-b', compositeCanonicalKey: 'vehicle=q72' },
    ])
    expect(warnings).toHaveLength(0)
  })
})

describe('disambiguateCanonicalKeys', () => {
  const collidingPair = () => {
    const key = buildCompositeCanonicalKey(dims({ vehicle: 'Q71', driveType: 'AWD' }))
    return [
      { stableInternalId: 'v-a', compositeCanonicalKey: key, originalVariantNumber: '3', originalColumn: 'Q' },
      { stableInternalId: 'v-b', compositeCanonicalKey: key, originalVariantNumber: '7', originalColumn: 'R' },
    ]
  }

  it('appends a distinct structural suffix to every member of a same-dimensions collision', () => {
    const [a, b] = disambiguateCanonicalKeys(collidingPair())
    expect(a.compositeCanonicalKey).not.toBe(b.compositeCanonicalKey)
    expect(a.compositeCanonicalKey).toContain('#dup-3')
    expect(b.compositeCanonicalKey).toContain('#dup-7')
  })

  it('disambiguates a double-reserved collision using originalColumn when originalVariantNumber is absent', () => {
    const emptyKey = buildCompositeCanonicalKey({})
    const [a, b] = disambiguateCanonicalKeys([
      { stableInternalId: 'reserved-1', compositeCanonicalKey: emptyKey, originalVariantNumber: null, originalColumn: 'AF' },
      { stableInternalId: 'reserved-2', compositeCanonicalKey: emptyKey, originalVariantNumber: null, originalColumn: 'AG' },
    ])
    expect(a.compositeCanonicalKey).not.toBe(b.compositeCanonicalKey)
    expect(a.compositeCanonicalKey).toContain('#dup-af')
    expect(b.compositeCanonicalKey).toContain('#dup-ag')
  })

  it('never uses volume as the disambiguation source (only originalVariantNumber/originalColumn)', () => {
    // Same collidingPair regardless of any volume the caller might separately
    // track — disambiguateCanonicalKeys doesn't even accept a volume field,
    // so this is a compile-time guarantee as much as a runtime one; this
    // test documents that the suffix is fully determined by the structural
    // fields alone.
    const [a] = disambiguateCanonicalKeys(collidingPair())
    expect(a.compositeCanonicalKey).toBe(`${buildCompositeCanonicalKey(dims({ vehicle: 'Q71', driveType: 'AWD' }))}#dup-3`)
  })

  it('leaves non-colliding keys untouched', () => {
    const [a, b] = disambiguateCanonicalKeys([
      { stableInternalId: 'v-a', compositeCanonicalKey: 'vehicle=q71', originalVariantNumber: '1', originalColumn: 'Q' },
      { stableInternalId: 'v-b', compositeCanonicalKey: 'vehicle=q72', originalVariantNumber: '2', originalColumn: 'R' },
    ])
    expect(a.compositeCanonicalKey).toBe('vehicle=q71')
    expect(b.compositeCanonicalKey).toBe('vehicle=q72')
  })

  it('is deterministic under reorder: the same colliding pair, passed in a different array order, resolves each variant to the same suffixed key', () => {
    const [alt1, alt2] = collidingPair()
    const forward = disambiguateCanonicalKeys([alt1, alt2])
    const reversed = disambiguateCanonicalKeys([alt2, alt1])

    const forwardById = new Map(forward.map((v) => [v.stableInternalId, v.compositeCanonicalKey]))
    const reversedById = new Map(reversed.map((v) => [v.stableInternalId, v.compositeCanonicalKey]))

    expect(forwardById.get('v-a')).toBe(reversedById.get('v-a'))
    expect(forwardById.get('v-b')).toBe(reversedById.get('v-b'))
  })
})
