package iace

import (
	"sort"
	"testing"
)

// patternCategoryCompatibility mirrors the Go runtime helper
// `acceptableMeasureCategories` in internal/api/handlers/iace_handler_init_helpers.go.
// Kept here so the test does not depend on the handlers package (which would
// introduce an import cycle). If the runtime map changes, this table must be
// updated in lockstep — there is no fallback any more, so a drift would mean
// hazards going to the operator with zero mitigations.
var patternCategoryCompatibility = map[string]map[string]bool{
	"mechanical_hazard":             {"mechanical": true},
	"electrical_hazard":             {"electrical": true},
	"thermal_hazard":                {"thermal": true, "material_environmental": true},
	"noise_hazard":                  {"noise_vibration": true, "ergonomic": true},
	"vibration_hazard":              {"noise_vibration": true, "ergonomic": true},
	"noise_vibration":               {"noise_vibration": true, "ergonomic": true},
	"pneumatic_hydraulic":           {"pneumatic_hydraulic": true, "mechanical": true},
	"material_environmental":        {"material_environmental": true},
	"chemical_risk":                 {"material_environmental": true, "thermal": true},
	"ergonomic":                     {"ergonomic": true},
	"ergonomic_hazard":              {"ergonomic": true},
	"fire_explosion":                {"thermal": true, "material_environmental": true},
	"radiation_hazard":              {"material_environmental": true},
	"emc_hazard":                    {"electrical": true, "software_control": true},
	"maintenance_hazard":            {"mechanical": true},
	"safety_function_failure":       {"safety_function": true, "software_control": true},
	"software_fault":                {"software_control": true},
	"sensor_fault":                  {"software_control": true},
	"configuration_error":           {"software_control": true},
	"update_failure":                {"software_control": true},
	"hmi_error":                     {"software_control": true},
	"mode_confusion":                {"software_control": true},
	"unauthorized_access":           {"cyber_network": true, "software_control": true},
	"communication_failure":         {"cyber_network": true, "software_control": true},
	"firmware_corruption":           {"cyber_network": true, "software_control": true},
	"logging_audit_failure":         {"cyber_network": true, "software_control": true},
	"ai_misclassification":          {"ai_specific": true, "software_control": true},
	"false_classification":          {"ai_specific": true, "software_control": true},
	"model_drift":                   {"ai_specific": true, "software_control": true},
	"data_poisoning":                {"ai_specific": true, "software_control": true},
	"sensor_spoofing":               {"ai_specific": true, "software_control": true},
	"unintended_bias":               {"ai_specific": true, "software_control": true},
	"noise_source":                  {"noise_vibration": true, "ergonomic": true},
	"vibration_source":              {"noise_vibration": true, "ergonomic": true},
	"high_temperature":              {"thermal": true, "material_environmental": true},
	"material_environmental_hazard": {"material_environmental": true},
	"cyber_resilience":              {"cyber_resilience": true, "cyber_network": true, "software_control": true},
}

// TestEveryPattern_HasCategoryCompatibleMeasure is the contract that replaces
// the old category fallback: every hazard pattern must reference at least one
// measure whose HazardCategory is compatible with the pattern's hazard cats,
// or with "general". Without this, the operator-facing UI shows a hazard with
// zero mitigations and a "consult expert" placeholder — which is fine for
// rare edge cases but must not become the default state.
//
// New patterns added to the library must satisfy this test. The 2026-05
// Bremse benchmark drove down zero-surv from 237 → 0 — that floor is now
// enforced.
//
// Patterns explicitly known to have no library coverage today should be added
// to AllowlistKnownGaps with a TODO and an issue link; the test still fails
// if anything not on that list has zero coverage.
var AllowlistKnownGaps = map[string]string{
	// hp-id -> rationale (must be filled when adding)
	//
	// HP2000/HP2001 are deliberate secondary-harm-chain DEMO patterns
	// (GetSecondaryHarmDemoPatterns). Their value is the SecondaryHarms field
	// (consumer-safety / product-liability chain), not a primary mitigation, so
	// they intentionally carry no SuggestedMeasureIDs. Allowlisted rather than
	// forced to inherit an ill-fitting measure.
	"HP2000": "Secondary-harm DEMO (Cola-Flasche/Splitter): kein Primaer-Measure by design; Wert ist die SecondaryHarms-Kette. TODO: Primaer-Mechanik-Measure ergaenzen, falls aus Demo zu Produktiv-Pattern befoerdert.",
	"HP2001": "Secondary-harm DEMO (Pharma Kreuzkontamination): Library hat kein Pharma-CIP-Measure; Wert ist die SecondaryHarms-Kette. TODO: CIP/material_environmental-Measure ergaenzen, falls befoerdert.",
}

func TestEveryPattern_HasCategoryCompatibleMeasure(t *testing.T) {
	measureCat := map[string]string{}
	for _, m := range GetProtectiveMeasureLibrary() {
		measureCat[m.ID] = m.HazardCategory
	}

	patterns := collectAllPatterns()
	gaps := []string{}
	for _, p := range patterns {
		if _, allowed := AllowlistKnownGaps[p.ID]; allowed {
			continue
		}
		accepted := map[string]bool{"general": true}
		for _, hc := range p.GeneratedHazardCats {
			for c := range patternCategoryCompatibility[hc] {
				accepted[c] = true
			}
		}
		hasCompatible := false
		for _, mid := range p.SuggestedMeasureIDs {
			mc, ok := measureCat[mid]
			if !ok {
				continue
			}
			if mc == "" || accepted[mc] {
				hasCompatible = true
				break
			}
		}
		if !hasCompatible {
			gaps = append(gaps, p.ID+" (cats="+joinCats(p.GeneratedHazardCats)+")")
		}
	}
	sort.Strings(gaps)
	if len(gaps) > 0 {
		t.Errorf("%d patterns have no category-compatible measure (add to AllowlistKnownGaps or fix SuggestedMeasureIDs):", len(gaps))
		const maxList = 20
		for i, g := range gaps {
			if i >= maxList {
				t.Errorf("  ... and %d more", len(gaps)-maxList)
				break
			}
			t.Errorf("  - %s", g)
		}
	}
}

// TestAllPatterns_UniqueIDs pins that collectAllPatterns() — i.e. every
// pattern that the engine actually sees at runtime — has globally unique
// HP-IDs. The older TestGetBuiltinHazardPatterns_UniqueIDs only checks the
// 44 builtin ones and missed 58 duplicates between extended.go/extended2.go
// and cobot/press/operational/extended_dguv.go that lived undetected for
// months (a hazard with the same HP-ID but different scenario would silently
// shadow its sibling in the persistence layer).
func TestAllPatterns_UniqueIDs(t *testing.T) {
	seen := map[string]string{} // HP-ID -> first NameDE
	dups := []string{}
	for _, p := range collectAllPatterns() {
		if p.ID == "" {
			t.Errorf("pattern with empty ID: %s", p.NameDE)
			continue
		}
		if prev, ok := seen[p.ID]; ok {
			dups = append(dups, p.ID+" ("+prev+" vs "+p.NameDE+")")
			continue
		}
		seen[p.ID] = p.NameDE
	}
	if len(dups) > 0 {
		t.Errorf("%d duplicate HP-IDs across all pattern sources:", len(dups))
		const maxList = 20
		for i, d := range dups {
			if i >= maxList {
				t.Errorf("  ... and %d more", len(dups)-maxList)
				break
			}
			t.Errorf("  - %s", d)
		}
	}
}

func joinCats(cats []string) string {
	out := ""
	for i, c := range cats {
		if i > 0 {
			out += ","
		}
		out += c
	}
	return out
}
