// Canonical QAF field model + alias registry — access-layer tests (KAR-892/P1.1).
//
// The registry data itself (canonical-fields.ts) is pure constant data, same
// "tdd-guard:skip" category as types.ts — these tests drive/verify the pure
// access functions in canonical-model.ts plus structural invariants over the
// real registry (uniqueness, evidence, DE+EN labels on mandatory fields, and
// the two backward-mapping tables staying in sync with the 22 QAFFieldKey /
// 19 SummaryMetricKey values already live in production).

import { describe, expect, it } from "vitest"
import { HEADER_TO_KEY, type QAFFieldKey } from "@/lib/qaf-parser"
import { SUMMARY_METRIC_KEYS, type SummaryMetricKey } from "../summary-metrics"
import {
  byCanonicalId,
  byModule,
  findByAlias,
  normalizeCanonicalLabel,
  validateRegistry,
  type CanonicalField,
  QAF_MODULES,
} from "../canonical-model"
import {
  CANONICAL_FIELDS,
  QAF_FIELD_KEY_TO_CANONICAL,
  SUMMARY_METRIC_KEY_TO_CANONICAL,
} from "../canonical-fields"

describe("normalizeCanonicalLabel", () => {
  it("lowercases and folds umlauts (reuses normalizer.ts umlaut map)", () => {
    expect(normalizeCanonicalLabel("Beschaffungswährung BW")).toBe(
      "beschaffungswaehrung bw"
    )
  })

  it("strips footnote asterisks and leading numbering like the Leitfaden tables use", () => {
    expect(normalizeCanonicalLabel("1. Materialkosten*")).toBe("materialkosten")
    expect(normalizeCanonicalLabel("6a. Rohstoff Preisanteil Material")).toBe(
      "rohstoff preisanteil material"
    )
  })

  it("collapses internal whitespace/newlines like the existing header normalizers", () => {
    expect(normalizeCanonicalLabel("Fertigungskosten\nFK [BW]")).toBe(
      "fertigungskosten fk [bw]"
    )
  })

  it("keeps unit brackets intact — they disambiguate same-stem fields (FK [BW] vs FK [AW])", () => {
    expect(normalizeCanonicalLabel("Fertigungskosten FK [BW]")).not.toBe(
      normalizeCanonicalLabel("Fertigungskosten FK [AW]")
    )
  })
})

describe("byModule", () => {
  it("returns only fields tagged with the requested module", () => {
    const rows = byModule("MANUFACTURING")
    expect(rows.length).toBeGreaterThan(0)
    expect(rows.every((f) => f.module === "MANUFACTURING")).toBe(true)
  })

  it("returns an empty array for a module with no registry entries yet (none expected — every QAF_MODULES entry should have >=1 field)", () => {
    for (const m of QAF_MODULES) {
      expect(byModule(m).length).toBeGreaterThan(0)
    }
  })
})

describe("byCanonicalId", () => {
  it("returns the field for a known id", () => {
    const f = byCanonicalId("mfg_manufacturing_cost_aw")
    expect(f?.id).toBe("mfg_manufacturing_cost_aw")
  })

  it("returns undefined for an unknown id", () => {
    expect(byCanonicalId("does_not_exist")).toBeUndefined()
  })
})

describe("findByAlias", () => {
  it("matches the exact DE label", () => {
    const matches = findByAlias("Fertigungskosten FK [AW]")
    expect(matches.some((f) => f.id === "mfg_manufacturing_cost_aw")).toBe(true)
  })

  it("matches the exact EN label", () => {
    const matches = findByAlias("Manufacturing costs FK [AW]")
    expect(matches.some((f) => f.id === "mfg_manufacturing_cost_aw")).toBe(true)
  })

  it("matches case-insensitively and umlaut-folded", () => {
    const matches = findByAlias("beschaffungswaehrung bw")
    expect(matches.some((f) => f.id === "mfg_procurement_currency")).toBe(true)
  })

  it("matches a documented BMW label typo alias (Teilebennung -> Teilebenennung)", () => { // allow-customer-string
    const matches = findByAlias("Teilebennung")
    expect(matches.some((f) => f.module === "MATERIAL")).toBe(true)
  })

  it("returns an empty array for an unknown label", () => {
    expect(findByAlias("völlig unbekanntes Feld xyz")).toEqual([])
  })

  it("honors the lang filter (only DE label + generic aliases checked)", () => {
    const de = findByAlias("Manufacturing costs FK [AW]", "de")
    expect(de).toEqual([])
    const en = findByAlias("Manufacturing costs FK [AW]", "en")
    expect(en.some((f) => f.id === "mfg_manufacturing_cost_aw")).toBe(true)
  })

  it('surfaces the real documented duplicate-label collision ("Mengeneinheit" 2x in MATERIAL) as multiple matches', () => {
    const matches = findByAlias("Mengeneinheit")
    const ids = matches.filter((f) => f.module === "MATERIAL").map((f) => f.id)
    expect(ids.length).toBeGreaterThanOrEqual(2)
  })
})

describe("validateRegistry", () => {
  it("passes on the real production registry (no duplicate ids, every entry has evidence, mandatory fields have DE+EN labels)", () => {
    const result = validateRegistry()
    expect(result.errors).toEqual([])
    expect(result.ok).toBe(true)
  })

  it("flags a duplicate canonical id", () => {
    const dupe: CanonicalField = { ...CANONICAL_FIELDS[0] }
    const result = validateRegistry([CANONICAL_FIELDS[0], dupe])
    expect(result.ok).toBe(false)
    expect(
      result.errors.some((e) => e.includes("Duplicate canonical id"))
    ).toBe(true)
  })

  it("flags an entry with no evidence", () => {
    const noEvidence: CanonicalField = {
      ...CANONICAL_FIELDS[0],
      id: "test_no_evidence",
      evidence: [],
    }
    const result = validateRegistry([noEvidence])
    expect(result.ok).toBe(false)
    expect(result.errors.some((e) => e.includes("missing evidence"))).toBe(true)
  })

  it("flags a mandatory field missing an EN label", () => {
    const noEn: CanonicalField = {
      ...CANONICAL_FIELDS[0],
      id: "test_no_en",
      requirement: "mandatory",
      labelDe: "Testfeld",
      labelEn: "",
    }
    const result = validateRegistry([noEn])
    expect(result.ok).toBe(false)
    expect(
      result.errors.some((e) => e.includes("Mandatory field missing"))
    ).toBe(true)
  })

  it("flags an UNINTENDED alias collision between two different fields in the same module", () => {
    const a: CanonicalField = {
      ...CANONICAL_FIELDS[0],
      id: "test_collide_a",
      module: "MATERIAL",
      labelDe: "Kollisionstest",
      labelEn: "Collision Test",
      aliases: [],
      collisionGroup: undefined,
    }
    const b: CanonicalField = { ...a, id: "test_collide_b" }
    const result = validateRegistry([a, b])
    expect(result.ok).toBe(false)
    expect(
      result.errors.some((e) => e.includes("Unintended alias collision"))
    ).toBe(true)
  })

  it("allows an INTENTIONAL alias collision when both entries share the same collisionGroup", () => {
    const a: CanonicalField = {
      ...CANONICAL_FIELDS[0],
      id: "test_collide_c",
      module: "MATERIAL",
      labelDe: "Bewusste Kollision",
      labelEn: "Deliberate Collision",
      aliases: [],
      collisionGroup: "test_group",
    }
    const b: CanonicalField = { ...a, id: "test_collide_d" }
    const result = validateRegistry([a, b])
    expect(
      result.errors.some((e) => e.includes("Unintended alias collision"))
    ).toBe(false)
  })
})

describe("backward mapping: 22 existing QAFFieldKey -> canonical id", () => {
  const allKeys = Object.values(HEADER_TO_KEY) as QAFFieldKey[]
  const uniqueKeys = Array.from(new Set(allKeys))

  it("HEADER_TO_KEY still has exactly 22 unique field keys (sanity guard — this test must be updated, not silently pass, if the parser vocabulary changes)", () => {
    expect(uniqueKeys.length).toBe(22)
  })

  it("every QAFFieldKey used by the parser has a canonical mapping", () => {
    for (const key of uniqueKeys) {
      expect(
        QAF_FIELD_KEY_TO_CANONICAL[key],
        `missing canonical mapping for QAFFieldKey "${key}"`
      ).toBeDefined()
    }
  })

  it("every mapped canonical id actually exists in the registry and is tagged MANUFACTURING", () => {
    for (const key of uniqueKeys) {
      const canonicalId = QAF_FIELD_KEY_TO_CANONICAL[key]
      const field = byCanonicalId(canonicalId)
      expect(
        field,
        `canonical id "${canonicalId}" (from QAFFieldKey "${key}") not found in registry`
      ).toBeDefined()
      expect(field?.module).toBe("MANUFACTURING")
    }
  })
})

describe("backward mapping: 19 existing SummaryMetricKey -> canonical id", () => {
  it("every SummaryMetricKey has a canonical mapping", () => {
    for (const key of SUMMARY_METRIC_KEYS) {
      expect(
        SUMMARY_METRIC_KEY_TO_CANONICAL[key],
        `missing canonical mapping for SummaryMetricKey "${key}"`
      ).toBeDefined()
    }
  })

  it("every mapped canonical id actually exists in the registry and is tagged SUMMARY", () => {
    for (const key of SUMMARY_METRIC_KEYS) {
      const canonicalId =
        SUMMARY_METRIC_KEY_TO_CANONICAL[key as SummaryMetricKey]
      const field = byCanonicalId(canonicalId)
      expect(
        field,
        `canonical id "${canonicalId}" (from SummaryMetricKey "${key}") not found in registry`
      ).toBeDefined()
      expect(field?.module).toBe("SUMMARY")
    }
  })
})

describe("registry-wide structural invariants (KAR-892 acceptance criteria)", () => {
  it("has no duplicate canonical ids", () => {
    const ids = CANONICAL_FIELDS.map((f) => f.id)
    expect(new Set(ids).size).toBe(ids.length)
  })

  it("every entry has at least one source-evidence reference", () => {
    for (const f of CANONICAL_FIELDS) {
      expect(
        f.evidence.length,
        `field "${f.id}" has no evidence`
      ).toBeGreaterThan(0)
    }
  })

  it("every mandatory field has both a DE and an EN label", () => {
    for (const f of CANONICAL_FIELDS) {
      if (f.requirement !== "mandatory") continue
      expect(
        f.labelDe.trim().length,
        `mandatory field "${f.id}" missing DE label`
      ).toBeGreaterThan(0)
      expect(
        f.labelEn.trim().length,
        `mandatory field "${f.id}" missing EN label`
      ).toBeGreaterThan(0)
    }
  })

  it("every field is tagged with a valid QAF module", () => {
    for (const f of CANONICAL_FIELDS) {
      expect(QAF_MODULES).toContain(f.module)
    }
  })
})
