package iace

import "testing"

func TestPatternEngine_RobotArm_MechanicalHazards(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"}, // Roboterarm: moving_part, rotating_part, high_force
		EnergySourceIDs:     []string{"EN01", "EN02"}, // kinetic translational + rotational
	})

	if len(result.MatchedPatterns) == 0 {
		t.Fatal("expected matched patterns for robot arm + kinetic energy")
	}

	// Should match mechanical hazard patterns (HP001, HP002, HP006)
	hasMechHazard := false
	for _, h := range result.SuggestedHazards {
		if h.Category == "mechanical_hazard" {
			hasMechHazard = true
			break
		}
	}
	if !hasMechHazard {
		t.Error("expected mechanical_hazard in suggested hazards for robot arm")
	}
}

func TestPatternEngine_Schaltschrank_ElectricalHazards(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C061"}, // Schaltschrank: high_voltage, electrical_part
		EnergySourceIDs:     []string{"EN04"}, // Elektrische Energie
	})

	if len(result.MatchedPatterns) == 0 {
		t.Fatal("expected matched patterns for control cabinet + electrical energy")
	}

	hasElecHazard := false
	for _, h := range result.SuggestedHazards {
		if h.Category == "electrical_hazard" {
			hasElecHazard = true
			break
		}
	}
	if !hasElecHazard {
		t.Error("expected electrical_hazard in suggested hazards for Schaltschrank")
	}
}

func TestPatternEngine_SPS_Switch_SoftwareCyberHazards(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C071", "C111"}, // SPS + Switch: has_software, programmable, networked, it_component
		EnergySourceIDs:     []string{"EN18"},         // Cyber/Data energy
	})

	if len(result.MatchedPatterns) == 0 {
		t.Fatal("expected matched patterns for SPS + Switch + cyber energy")
	}

	categories := make(map[string]bool)
	for _, h := range result.SuggestedHazards {
		categories[h.Category] = true
	}

	if !categories["software_fault"] {
		t.Error("expected software_fault hazard for SPS")
	}
	if !categories["unauthorized_access"] {
		t.Error("expected unauthorized_access hazard for networked components + cyber energy")
	}
}

func TestPatternEngine_AIModule_AISpecificHazards(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C119"}, // KI-Inferenzmodul: has_ai, has_software, networked
		EnergySourceIDs:     []string{"EN19", "EN18"}, // AI model + cyber energy
	})

	if len(result.MatchedPatterns) == 0 {
		t.Fatal("expected matched patterns for AI inference module")
	}

	categories := make(map[string]bool)
	for _, h := range result.SuggestedHazards {
		categories[h.Category] = true
	}

	if !categories["false_classification"] {
		t.Error("expected false_classification hazard for AI module")
	}
	if !categories["model_drift"] {
		t.Error("expected model_drift hazard for AI module")
	}
}

func TestPatternEngine_EmptyInput_EmptyResults(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{})

	if len(result.MatchedPatterns) != 0 {
		t.Errorf("expected no matched patterns for empty input, got %d", len(result.MatchedPatterns))
	}
	if len(result.SuggestedHazards) != 0 {
		t.Errorf("expected no suggested hazards for empty input, got %d", len(result.SuggestedHazards))
	}
}

func TestPatternEngine_ExclusionTags(t *testing.T) {
	engine := NewPatternEngine()

	// HP029 requires has_software+programmable, excludes has_ai
	// C071 (SPS) has has_software+programmable but NOT has_ai → should match HP029
	result1 := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C071"}, // SPS
	})
	hasHP029 := false
	for _, p := range result1.MatchedPatterns {
		if p.PatternID == "HP029" {
			hasHP029 = true
			break
		}
	}
	if !hasHP029 {
		t.Error("HP029 should match for SPS (has_software+programmable, no has_ai)")
	}

	// C119 (KI-Inferenzmodul) has has_ai → HP029 should be excluded
	result2 := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C119"}, // KI-Inferenzmodul: has_ai
	})
	hasHP029_2 := false
	for _, p := range result2.MatchedPatterns {
		if p.PatternID == "HP029" {
			hasHP029_2 = true
			break
		}
	}
	if hasHP029_2 {
		t.Error("HP029 should NOT match for AI module (excluded by has_ai tag)")
	}
}

func TestPatternEngine_HydraulicPatterns(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C042"}, // Hydraulikzylinder
		EnergySourceIDs:     []string{"EN05"}, // Hydraulische Energie
	})

	hasHydraulicHazard := false
	for _, h := range result.SuggestedHazards {
		if h.Category == "pneumatic_hydraulic" {
			hasHydraulicHazard = true
			break
		}
	}
	if !hasHydraulicHazard {
		t.Error("expected pneumatic_hydraulic hazard for hydraulic cylinder + hydraulic energy")
	}
}

func TestPatternEngine_MeasuresAndEvidence(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"}, // Roboterarm
		EnergySourceIDs:     []string{"EN01"}, // Kinetische Energie
	})

	if len(result.SuggestedMeasures) == 0 {
		t.Error("expected suggested measures for robot arm")
	}
	if len(result.SuggestedEvidence) == 0 {
		t.Error("expected suggested evidence for robot arm")
	}
}

func TestPatternEngine_ResolvedTags(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		CustomTags:          []string{"my_custom_tag"},
	})

	tagSet := toSet(result.ResolvedTags)
	if !tagSet["moving_part"] {
		t.Error("expected 'moving_part' in resolved tags")
	}
	if !tagSet["kinetic"] {
		t.Error("expected 'kinetic' in resolved tags")
	}
	if !tagSet["my_custom_tag"] {
		t.Error("expected 'my_custom_tag' in resolved tags")
	}
}

func TestPatternEngine_SafetyDevice_HighPriority(t *testing.T) {
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C101"}, // Not-Halt-Taster: safety_device, emergency_stop
	})

	hasSafetyFail := false
	for _, h := range result.SuggestedHazards {
		if h.Category == "safety_function_failure" {
			hasSafetyFail = true
			break
		}
	}
	if !hasSafetyFail {
		t.Error("expected safety_function_failure for Not-Halt-Taster")
	}
}

// ── Operational State Graph tests ──────────────────────────────────

func TestPatternEngine_OperationalState_NilFiresAlways(t *testing.T) {
	// Patterns with nil OperationalStates should fire regardless of input states
	engine := NewPatternEngine()
	result1 := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
	})
	result2 := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		OperationalStates:   []string{"automatic_operation"},
	})

	if len(result1.MatchedPatterns) == 0 {
		t.Fatal("expected patterns to fire without operational states")
	}
	if len(result2.MatchedPatterns) == 0 {
		t.Fatal("expected patterns to fire with automatic_operation state")
	}
	// nil-state patterns should fire in both cases
	if len(result1.MatchedPatterns) != len(result2.MatchedPatterns) {
		t.Logf("patterns without states: %d, with automatic_operation: %d", len(result1.MatchedPatterns), len(result2.MatchedPatterns))
		// This is OK — some patterns may have OperationalStates set and filter differently
	}
}

func TestPatternEngine_OperationalState_MaintenanceFilter(t *testing.T) {
	// HP073 (LOTO) has OperationalStates: ["maintenance"]
	// It should only fire when maintenance is in the input states
	engine := NewPatternEngine()

	// Without maintenance state — HP073 should still fire if no states in input
	// (because empty input states = no filtering)
	resultNoState := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"maintenance"},
	})

	// With maintenance state — HP073 should fire
	resultMaint := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"maintenance"},
		OperationalStates:   []string{"maintenance"},
	})

	// With only automatic_operation — HP073 should NOT fire
	resultAuto := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"maintenance"},
		OperationalStates:   []string{"automatic_operation"},
	})

	// HP073 should be in resultMaint but not in resultAuto
	hasHP073Maint := false
	for _, p := range resultMaint.MatchedPatterns {
		if p.PatternID == "HP073" {
			hasHP073Maint = true
			break
		}
	}

	hasHP073Auto := false
	for _, p := range resultAuto.MatchedPatterns {
		if p.PatternID == "HP073" {
			hasHP073Auto = true
			break
		}
	}

	if !hasHP073Maint {
		t.Error("HP073 should fire with maintenance operational state")
	}
	if hasHP073Auto {
		t.Error("HP073 should NOT fire with automatic_operation operational state")
	}

	_ = resultNoState // HP073 fires here too because empty input states = no filter
}

func TestPatternEngine_StateTransition_UnexpectedRestart(t *testing.T) {
	// HP068 has StateTransitions: ["maintenance→automatic_operation", "emergency_stop→recovery_mode"]
	engine := NewPatternEngine()

	// HP068 requires moving_part + programmable — C001 (Roboterarm) + C071 (SPS)
	// With matching transition
	resultMatch := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001", "C071"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"fault_clearing"},
		OperationalStates:   []string{"recovery_mode", "emergency_stop"},
		StateTransitions:    []string{"maintenance→automatic_operation"},
	})

	// With non-matching transition
	resultNoMatch := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001", "C071"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"fault_clearing"},
		OperationalStates:   []string{"recovery_mode", "emergency_stop"},
		StateTransitions:    []string{"startup→homing"},
	})

	hasHP068Match := false
	for _, p := range resultMatch.MatchedPatterns {
		if p.PatternID == "HP068" {
			hasHP068Match = true
			// Verify explainability reasons include state transition
			hasTransReason := false
			for _, r := range p.MatchReasons {
				if r.Type == "state_transition" {
					hasTransReason = true
					break
				}
			}
			if !hasTransReason {
				t.Error("HP068 match should include state_transition reason")
			}
			break
		}
	}

	hasHP068NoMatch := false
	for _, p := range resultNoMatch.MatchedPatterns {
		if p.PatternID == "HP068" {
			hasHP068NoMatch = true
			break
		}
	}

	if !hasHP068Match {
		t.Error("HP068 should fire with matching state transition")
	}
	if hasHP068NoMatch {
		t.Error("HP068 should NOT fire with non-matching state transition")
	}
}

func TestPatternEngine_MatchReasons_IncludeOperationalState(t *testing.T) {
	// Verify that MatchReasons include operational_state entries
	engine := NewPatternEngine()
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"maintenance"},
		OperationalStates:   []string{"maintenance"},
	})

	for _, p := range result.MatchedPatterns {
		if p.PatternID == "HP073" {
			hasStateReason := false
			for _, r := range p.MatchReasons {
				if r.Type == "operational_state" && r.Tag == "maintenance" && r.Met {
					hasStateReason = true
					break
				}
			}
			if !hasStateReason {
				t.Error("HP073 MatchReasons should include operational_state:maintenance with Met=true")
			}
			return
		}
	}
	t.Error("HP073 not found in matched patterns")
}

func TestAllOperationalStates_Count(t *testing.T) {
	states := AllOperationalStates()
	if len(states) != 9 {
		t.Errorf("expected 9 operational states, got %d", len(states))
	}
}

func TestStandardStateTransitions_Valid(t *testing.T) {
	transitions := StandardStateTransitions()
	if len(transitions) < 15 {
		t.Errorf("expected at least 15 state transitions, got %d", len(transitions))
	}
	// Verify all transitions reference valid states
	stateSet := make(map[string]bool)
	for _, s := range AllOperationalStates() {
		stateSet[s] = true
	}
	for _, tr := range transitions {
		// Format: "from→to" (→ is multi-byte UTF-8)
		parts := splitTransition(tr)
		if len(parts) != 2 {
			t.Errorf("invalid transition format: %q", tr)
			continue
		}
		if !stateSet[parts[0]] {
			t.Errorf("transition %q references unknown state: %q", tr, parts[0])
		}
		if !stateSet[parts[1]] {
			t.Errorf("transition %q references unknown state: %q", tr, parts[1])
		}
	}
}

// ── Human Interaction Model tests ──────────────────────────────────

func TestPatternEngine_HumanRole_NilFiresAlways(t *testing.T) {
	engine := NewPatternEngine()
	result1 := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
	})
	result2 := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		HumanRoles:          []string{"operator"},
	})
	if len(result1.MatchedPatterns) == 0 {
		t.Fatal("expected patterns without roles")
	}
	if len(result2.MatchedPatterns) == 0 {
		t.Fatal("expected patterns with operator role")
	}
}

func TestPatternEngine_HumanRole_MaintenanceTechFilter(t *testing.T) {
	// HP073 has HumanRoles: ["maintenance_tech"]
	engine := NewPatternEngine()

	// With maintenance_tech → HP073 should fire
	resultMT := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"maintenance"},
		OperationalStates:   []string{"maintenance"},
		HumanRoles:          []string{"maintenance_tech"},
	})

	// With only operator → HP073 should NOT fire
	resultOp := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C001"},
		EnergySourceIDs:     []string{"EN01"},
		LifecyclePhases:     []string{"maintenance"},
		OperationalStates:   []string{"maintenance"},
		HumanRoles:          []string{"operator"},
	})

	hasHP073MT := false
	for _, p := range resultMT.MatchedPatterns {
		if p.PatternID == "HP073" {
			hasHP073MT = true
			break
		}
	}
	hasHP073Op := false
	for _, p := range resultOp.MatchedPatterns {
		if p.PatternID == "HP073" {
			hasHP073Op = true
			break
		}
	}

	if !hasHP073MT {
		t.Error("HP073 should fire for maintenance_tech role")
	}
	if hasHP073Op {
		t.Error("HP073 should NOT fire for operator role")
	}
}

func TestPatternEngine_HumanRole_ProgrammerTeachMode(t *testing.T) {
	// HP062 has OperationalStates: ["teach_mode"], HumanRoles: ["programmer"]
	engine := NewPatternEngine()

	// Programmer in teach mode with cobot components
	result := engine.Match(MatchInput{
		ComponentLibraryIDs: []string{"C139"}, // Cobot: moving_part, programmable, collaborative_operation
		EnergySourceIDs:     []string{"EN01"},
		OperationalStates:   []string{"teach_mode"},
		HumanRoles:          []string{"programmer"},
	})

	hasHP062 := false
	for _, p := range result.MatchedPatterns {
		if p.PatternID == "HP062" {
			hasHP062 = true
			// Verify explainability
			hasRoleReason := false
			for _, r := range p.MatchReasons {
				if r.Type == "human_role" && r.Tag == "programmer" && r.Met {
					hasRoleReason = true
					break
				}
			}
			if !hasRoleReason {
				t.Error("HP062 should include human_role:programmer reason")
			}
			break
		}
	}
	if !hasHP062 {
		t.Error("HP062 should fire for programmer in teach_mode with cobot")
	}
}

func TestAllHumanRoles_Count(t *testing.T) {
	roles := AllHumanRoles()
	if len(roles) != 6 {
		t.Errorf("expected 6 human roles, got %d", len(roles))
	}
}

func splitTransition(tr string) []string {
	// Split on → (UTF-8: 0xE2 0x86 0x92)
	idx := 0
	for i := 0; i < len(tr); i++ {
		if tr[i] == 0xE2 && i+2 < len(tr) && tr[i+1] == 0x86 && tr[i+2] == 0x92 {
			return []string{tr[:i], tr[i+3:]}
		}
		idx = i
	}
	_ = idx
	return []string{tr}
}
