package iace

import (
	"math"
	"testing"
)

// ============================================================================
// Helper
// ============================================================================

const floatTolerance = 1e-9

func almostEqual(a, b float64) bool {
	return math.Abs(a-b) < floatTolerance
}

// ============================================================================
// 1. CalculateInherentRisk — S × E × P
// ============================================================================

func TestCalculateInherentRisk_BasicCases(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name     string
		s, ex, p int
		expected float64
	}{
		// Minimum
		{"min 1×1×1", 1, 1, 1, 1},
		// Maximum
		{"max 5×5×5", 5, 5, 5, 125},
		// Single factor high
		{"5×1×1", 5, 1, 1, 5},
		{"1×5×1", 1, 5, 1, 5},
		{"1×1×5", 1, 1, 5, 5},
		// Typical mid-range
		{"3×3×3", 3, 3, 3, 27},
		{"2×4×3", 2, 4, 3, 24},
		{"4×2×5", 4, 2, 5, 40},
		// Boundary at thresholds
		{"3×5×5 = 75 (critical threshold)", 3, 5, 5, 75},
		{"2×4×5 = 40 (high threshold)", 2, 4, 5, 40},
		{"3×5×1 = 15 (medium threshold)", 3, 5, 1, 15},
		{"5×1×1 = 5 (low threshold)", 5, 1, 1, 5},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateInherentRisk(tt.s, tt.ex, tt.p, 0)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateInherentRisk(%d, %d, %d) = %v, want %v", tt.s, tt.ex, tt.p, result, tt.expected)
			}
		})
	}
}

func TestCalculateInherentRisk_Clamping(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name     string
		s, ex, p int
		expected float64
	}{
		{"below min clamped to 1", 0, 0, 0, 1},
		{"negative clamped to 1", -5, -3, -1, 1},
		{"above max clamped to 5", 10, 8, 6, 125},
		{"mixed out-of-range", 0, 10, 3, 15}, // clamp(0,1,5)=1, clamp(10,1,5)=5, clamp(3,1,5)=3 → 1*5*3=15
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateInherentRisk(tt.s, tt.ex, tt.p, 0)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateInherentRisk(%d, %d, %d) = %v, want %v", tt.s, tt.ex, tt.p, result, tt.expected)
			}
		})
	}
}

// Full S×E×P coverage: verify all 125 combinations produce correct multiplication.
func TestCalculateInherentRisk_FullCoverage(t *testing.T) {
	e := NewRiskEngine()

	for s := 1; s <= 5; s++ {
		for ex := 1; ex <= 5; ex++ {
			for p := 1; p <= 5; p++ {
				expected := float64(s * ex * p)
				result := e.CalculateInherentRisk(s, ex, p, 0)
				if !almostEqual(result, expected) {
					t.Errorf("CalculateInherentRisk(%d, %d, %d) = %v, want %v", s, ex, p, result, expected)
				}
			}
		}
	}
}

// ============================================================================
// 2. CalculateControlEffectiveness
//    C_eff = min(1, 0.2*(maturity/4.0) + 0.5*coverage + 0.3*testEvidence)
// ============================================================================

func TestCalculateControlEffectiveness(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name         string
		maturity     int
		coverage     float64
		testEvidence float64
		expected     float64
	}{
		// All zeros → 0
		{"all zero", 0, 0.0, 0.0, 0.0},
		// All max → min(1, 0.2*1 + 0.5*1 + 0.3*1) = min(1, 1.0) = 1.0
		{"all max", 4, 1.0, 1.0, 1.0},
		// Only maturity max → 0.2 * (4/4) = 0.2
		{"maturity only", 4, 0.0, 0.0, 0.2},
		// Only coverage max → 0.5
		{"coverage only", 0, 1.0, 0.0, 0.5},
		// Only test evidence max → 0.3
		{"evidence only", 0, 0.0, 1.0, 0.3},
		// Half maturity → 0.2 * (2/4) = 0.1
		{"half maturity", 2, 0.0, 0.0, 0.1},
		// Typical mid-range: maturity=2, coverage=0.6, evidence=0.4
		// 0.2*(2/4) + 0.5*0.6 + 0.3*0.4 = 0.1 + 0.3 + 0.12 = 0.52
		{"typical mid", 2, 0.6, 0.4, 0.52},
		// High values exceeding 1.0 should be capped
		// maturity=4, coverage=1.0, evidence=1.0 → 0.2+0.5+0.3 = 1.0
		{"capped at 1.0", 4, 1.0, 1.0, 1.0},
		// maturity=3, coverage=0.8, evidence=0.9
		// 0.2*(3/4) + 0.5*0.8 + 0.3*0.9 = 0.15 + 0.4 + 0.27 = 0.82
		{"high controls", 3, 0.8, 0.9, 0.82},
		// maturity=1, coverage=0.2, evidence=0.1
		// 0.2*(1/4) + 0.5*0.2 + 0.3*0.1 = 0.05 + 0.1 + 0.03 = 0.18
		{"low controls", 1, 0.2, 0.1, 0.18},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateControlEffectiveness(tt.maturity, tt.coverage, tt.testEvidence)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateControlEffectiveness(%d, %v, %v) = %v, want %v",
					tt.maturity, tt.coverage, tt.testEvidence, result, tt.expected)
			}
		})
	}
}

func TestCalculateControlEffectiveness_Clamping(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name         string
		maturity     int
		coverage     float64
		testEvidence float64
		expected     float64
	}{
		// Maturity below 0 → clamped to 0
		{"maturity below zero", -1, 0.5, 0.5, 0.5*0.5 + 0.3*0.5},
		// Maturity above 4 → clamped to 4
		{"maturity above max", 10, 0.0, 0.0, 0.2},
		// Coverage below 0 → clamped to 0
		{"coverage below zero", 0, -0.5, 0.0, 0.0},
		// Coverage above 1 → clamped to 1
		{"coverage above max", 0, 2.0, 0.0, 0.5},
		// Evidence below 0 → clamped to 0
		{"evidence below zero", 0, 0.0, -1.0, 0.0},
		// Evidence above 1 → clamped to 1
		{"evidence above max", 0, 0.0, 5.0, 0.3},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateControlEffectiveness(tt.maturity, tt.coverage, tt.testEvidence)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateControlEffectiveness(%d, %v, %v) = %v, want %v",
					tt.maturity, tt.coverage, tt.testEvidence, result, tt.expected)
			}
		})
	}
}

// ============================================================================
// 3. CalculateResidualRisk — R_residual = S × E × P × (1 - C_eff)
// ============================================================================

func TestCalculateResidualRisk(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name     string
		s, ex, p int
		cEff     float64
		expected float64
	}{
		// No controls → residual = inherent
		{"no controls", 5, 5, 5, 0.0, 125.0},
		// Perfect controls → residual = 0
		{"perfect controls", 5, 5, 5, 1.0, 0.0},
		// Half effectiveness
		{"half controls 3×3×3", 3, 3, 3, 0.5, 13.5},
		// Typical scenario: inherent=40, cEff=0.6 → residual=16
		{"typical 2×4×5 cEff=0.6", 2, 4, 5, 0.6, 16.0},
		// Low risk with some controls
		{"low 1×2×3 cEff=0.3", 1, 2, 3, 0.3, 4.2},
		// High risk with strong controls
		{"high 5×4×4 cEff=0.82", 5, 4, 4, 0.82, 14.4},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateResidualRisk(tt.s, tt.ex, tt.p, tt.cEff)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateResidualRisk(%d, %d, %d, %v) = %v, want %v",
					tt.s, tt.ex, tt.p, tt.cEff, result, tt.expected)
			}
		})
	}
}

// ============================================================================
// 4. DetermineRiskLevel — threshold classification
// ============================================================================

func TestDetermineRiskLevel(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name     string
		residual float64
		expected RiskLevel
	}{
		// Critical: >= 75
		{"critical at 75", 75.0, RiskLevelCritical},
		{"critical at 125", 125.0, RiskLevelCritical},
		{"critical at 100", 100.0, RiskLevelCritical},
		// High: >= 40
		{"high at 40", 40.0, RiskLevelHigh},
		{"high at 74.9", 74.9, RiskLevelHigh},
		{"high at 50", 50.0, RiskLevelHigh},
		// Medium: >= 15
		{"medium at 15", 15.0, RiskLevelMedium},
		{"medium at 39.9", 39.9, RiskLevelMedium},
		{"medium at 27", 27.0, RiskLevelMedium},
		// Low: >= 5
		{"low at 5", 5.0, RiskLevelLow},
		{"low at 14.9", 14.9, RiskLevelLow},
		{"low at 10", 10.0, RiskLevelLow},
		// Negligible: < 5
		{"negligible at 4.9", 4.9, RiskLevelNegligible},
		{"negligible at 0", 0.0, RiskLevelNegligible},
		{"negligible at 1", 1.0, RiskLevelNegligible},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.DetermineRiskLevel(tt.residual)
			if result != tt.expected {
				t.Errorf("DetermineRiskLevel(%v) = %v, want %v", tt.residual, result, tt.expected)
			}
		})
	}
}

// ============================================================================
// 5. IsAcceptable — ALARP principle
// ============================================================================

func TestIsAcceptable(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name           string
		residual       float64
		allReduction   bool
		justification  bool
		wantAcceptable bool
		wantReason     string
	}{
		// Below 15 → always acceptable
		{"residual 14.9 always ok", 14.9, false, false, true, "Restrisiko unter Schwellwert"},
		{"residual 0 always ok", 0.0, false, false, true, "Restrisiko unter Schwellwert"},
		{"residual 10 always ok", 10.0, false, false, true, "Restrisiko unter Schwellwert"},
		// 15-39.9 with all reduction + justification → ALARP
		{"ALARP 20 all+just", 20.0, true, true, true, "ALARP-Prinzip: Restrisiko akzeptabel mit vollstaendiger Risikominderung"},
		{"ALARP 39.9 all+just", 39.9, true, true, true, "ALARP-Prinzip: Restrisiko akzeptabel mit vollstaendiger Risikominderung"},
		{"ALARP 15 all+just", 15.0, true, true, true, "ALARP-Prinzip: Restrisiko akzeptabel mit vollstaendiger Risikominderung"},
		// 15-39.9 without all reduction → NOT acceptable
		{"no reduction 20", 20.0, false, true, false, "Restrisiko zu hoch - blockiert CE-Export"},
		// 15-39.9 without justification → NOT acceptable
		{"no justification 20", 20.0, true, false, false, "Restrisiko zu hoch - blockiert CE-Export"},
		// 15-39.9 without either → NOT acceptable
		{"neither 30", 30.0, false, false, false, "Restrisiko zu hoch - blockiert CE-Export"},
		// >= 40 → NEVER acceptable
		{"residual 40 blocked", 40.0, true, true, false, "Restrisiko zu hoch - blockiert CE-Export"},
		{"residual 75 blocked", 75.0, true, true, false, "Restrisiko zu hoch - blockiert CE-Export"},
		{"residual 125 blocked", 125.0, true, true, false, "Restrisiko zu hoch - blockiert CE-Export"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			acceptable, reason := e.IsAcceptable(tt.residual, tt.allReduction, tt.justification)
			if acceptable != tt.wantAcceptable {
				t.Errorf("IsAcceptable(%v, %v, %v) acceptable = %v, want %v",
					tt.residual, tt.allReduction, tt.justification, acceptable, tt.wantAcceptable)
			}
			if reason != tt.wantReason {
				t.Errorf("IsAcceptable(%v, %v, %v) reason = %q, want %q",
					tt.residual, tt.allReduction, tt.justification, reason, tt.wantReason)
			}
		})
	}
}

// ============================================================================
// 6. CalculateCompletenessScore
// ============================================================================

func TestCalculateCompletenessScore(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name                                                                     string
		passedReq, totalReq, passedRec, totalRec, passedOpt, totalOpt           int
		expected                                                                 float64
	}{
		// All passed
		{"all passed", 20, 20, 5, 5, 3, 3, 100.0},
		// Nothing passed
		{"nothing passed", 0, 20, 0, 5, 0, 3, 0.0},
		// Only required fully passed
		{"only required", 20, 20, 0, 5, 0, 3, 80.0},
		// Only recommended fully passed
		{"only recommended", 0, 20, 5, 5, 0, 3, 15.0},
		// Only optional fully passed
		{"only optional", 0, 20, 0, 5, 3, 3, 5.0},
		// Half required, no others
		{"half required", 10, 20, 0, 5, 0, 3, 40.0},
		// All zero totals → 0 (division by zero safety)
		{"all zero totals", 0, 0, 0, 0, 0, 0, 0.0},
		// Typical: 18/20 req + 3/5 rec + 1/3 opt
		// (18/20)*80 + (3/5)*15 + (1/3)*5 = 72 + 9 + 1.6667 = 82.6667
		{"typical", 18, 20, 3, 5, 1, 3, 72.0 + 9.0 + 5.0/3.0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateCompletenessScore(
				tt.passedReq, tt.totalReq, tt.passedRec, tt.totalRec, tt.passedOpt, tt.totalOpt)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateCompletenessScore(%d/%d, %d/%d, %d/%d) = %v, want %v",
					tt.passedReq, tt.totalReq, tt.passedRec, tt.totalRec, tt.passedOpt, tt.totalOpt,
					result, tt.expected)
			}
		})
	}
}

// ============================================================================
// 7. ComputeRisk — integration test
// ============================================================================

func TestComputeRisk_ValidInput(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name             string
		input            RiskComputeInput
		wantInherent     float64
		wantCEff         float64
		wantResidual     float64
		wantLevel        RiskLevel
		wantAcceptable   bool
	}{
		{
			name: "no controls high risk",
			input: RiskComputeInput{
				Severity: 5, Exposure: 5, Probability: 5,
				ControlMaturity: 0, ControlCoverage: 0, TestEvidence: 0,
			},
			wantInherent:   125,
			wantCEff:       0,
			wantResidual:   125,
			wantLevel:      RiskLevelCritical,
			wantAcceptable: false,
		},
		{
			name: "perfect controls zero residual",
			input: RiskComputeInput{
				Severity: 5, Exposure: 5, Probability: 5,
				ControlMaturity: 4, ControlCoverage: 1.0, TestEvidence: 1.0,
			},
			wantInherent:   125,
			wantCEff:       1.0,
			wantResidual:   0,
			wantLevel:      RiskLevelNegligible,
			wantAcceptable: true,
		},
		{
			name: "medium risk acceptable",
			input: RiskComputeInput{
				Severity: 2, Exposure: 2, Probability: 2,
				ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
				// C_eff = 0.2*(2/4) + 0.5*0.5 + 0.3*0.5 = 0.1 + 0.25 + 0.15 = 0.5
				// inherent = 8, residual = 8 * 0.5 = 4 → negligible → acceptable
			},
			wantInherent:   8,
			wantCEff:       0.5,
			wantResidual:   4,
			wantLevel:      RiskLevelNegligible,
			wantAcceptable: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := e.ComputeRisk(tt.input)
			if err != nil {
				t.Fatalf("ComputeRisk returned error: %v", err)
			}
			if !almostEqual(result.InherentRisk, tt.wantInherent) {
				t.Errorf("InherentRisk = %v, want %v", result.InherentRisk, tt.wantInherent)
			}
			if !almostEqual(result.ControlEffectiveness, tt.wantCEff) {
				t.Errorf("ControlEffectiveness = %v, want %v", result.ControlEffectiveness, tt.wantCEff)
			}
			if !almostEqual(result.ResidualRisk, tt.wantResidual) {
				t.Errorf("ResidualRisk = %v, want %v", result.ResidualRisk, tt.wantResidual)
			}
			if result.RiskLevel != tt.wantLevel {
				t.Errorf("RiskLevel = %v, want %v", result.RiskLevel, tt.wantLevel)
			}
			if result.IsAcceptable != tt.wantAcceptable {
				t.Errorf("IsAcceptable = %v, want %v", result.IsAcceptable, tt.wantAcceptable)
			}
		})
	}
}

func TestComputeRisk_InvalidInput(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name  string
		input RiskComputeInput
	}{
		{"severity zero", RiskComputeInput{Severity: 0, Exposure: 3, Probability: 3}},
		{"exposure zero", RiskComputeInput{Severity: 3, Exposure: 0, Probability: 3}},
		{"probability zero", RiskComputeInput{Severity: 3, Exposure: 3, Probability: 0}},
		{"all zero", RiskComputeInput{Severity: 0, Exposure: 0, Probability: 0}},
		{"negative values", RiskComputeInput{Severity: -1, Exposure: -2, Probability: -3}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := e.ComputeRisk(tt.input)
			if err == nil {
				t.Errorf("ComputeRisk expected error for input %+v, got result %+v", tt.input, result)
			}
		})
	}
}

// ============================================================================
// 8. Golden Test Suite — 10 Referenzmaschinen (Industrial Machine Scenarios)
//
// Each machine scenario tests the full pipeline:
//   Inherent Risk → Control Effectiveness → Residual Risk → Risk Level → Acceptability
//
// The control parameters reflect realistic mitigation states for each machine type.
// These serve as regression tests: if any threshold or formula changes,
// these tests will catch the impact immediately.
// ============================================================================

// referenceMachine defines a complete end-to-end test scenario for a real machine type.
type referenceMachine struct {
	name        string
	description string

	// Inherent risk factors (pre-mitigation)
	severity    int // 1-5
	exposure    int // 1-5
	probability int // 1-5

	// Control parameters (mitigation state)
	controlMaturity  int     // 0-4
	controlCoverage  float64 // 0-1
	testEvidence     float64 // 0-1
	hasJustification bool

	// Expected outputs
	expectedInherentRisk float64
	expectedCEff         float64
	expectedResidualRisk float64
	expectedRiskLevel    RiskLevel
	expectedAcceptable   bool
}

// computeExpectedCEff calculates the expected control effectiveness for documentation/verification.
func computeExpectedCEff(maturity int, coverage, testEvidence float64) float64 {
	cEff := 0.2*(float64(maturity)/4.0) + 0.5*coverage + 0.3*testEvidence
	if cEff > 1.0 {
		return 1.0
	}
	return cEff
}

func getReferenceMachines() []referenceMachine {
	return []referenceMachine{
		// ---------------------------------------------------------------
		// 1. Industrieroboter-Zelle mit Schutzzaun
		//    6-Achs-Roboter, Materialhandling, Schutzzaun + Lichtschranke.
		//    Hauptgefaehrdung: Quetschung/Kollision bei Betreten.
		//    Hohe Massnahmen: Sicherheits-SPS, zweikanalige Tuerueberwachung.
		// ---------------------------------------------------------------
		{
			name:             "Industrieroboter-Zelle",
			description:      "6-Achs-Roboter mit Schutzzaun, Quetsch-/Kollisionsgefahr",
			severity:         5, // Lebensgefaehrlich
			exposure:         3, // Regelmaessiger Zugang (Wartung)
			probability:      4, // Wahrscheinlich bei offenem Zugang
			controlMaturity:  3,
			controlCoverage:  0.8,
			testEvidence:     0.7,
			hasJustification: false,
			// Inherent: 5*3*4 = 60
			// C_eff: 0.2*(3/4) + 0.5*0.8 + 0.3*0.7 = 0.15 + 0.4 + 0.21 = 0.76
			// Residual: 60 * (1-0.76) = 60 * 0.24 = 14.4
			// Level: medium (>=5, <15) → actually 14.4 < 15 → low
			// Acceptable: 14.4 < 15 → yes
			expectedInherentRisk: 60,
			expectedCEff:         0.76,
			expectedResidualRisk: 60 * (1 - 0.76),
			expectedRiskLevel:    RiskLevelLow,
			expectedAcceptable:   true,
		},
		// ---------------------------------------------------------------
		// 2. CNC-Fraesmaschine mit automatischem Werkzeugwechsel
		//    Werkzeugbruch, Spaeneflug. Vollschutzkabine + Drehzahlueberwachung.
		// ---------------------------------------------------------------
		{
			name:             "CNC-Fraesmaschine",
			description:      "CNC mit Werkzeugwechsel, Werkzeugbruch/Spaeneflug",
			severity:         4, // Schwere Verletzung
			exposure:         3, // Bediener steht regelmaessig davor
			probability:      3, // Moeglich
			controlMaturity:  3,
			controlCoverage:  0.9,
			testEvidence:     0.8,
			hasJustification: true,
			// Inherent: 4*3*3 = 36
			// C_eff: 0.2*(3/4) + 0.5*0.9 + 0.3*0.8 = 0.15 + 0.45 + 0.24 = 0.84
			// Residual: 36 * 0.16 = 5.76
			// Level: low (>=5, <15)
			// Acceptable: 5.76 < 15 → yes
			expectedInherentRisk: 36,
			expectedCEff:         0.84,
			expectedResidualRisk: 36 * (1 - 0.84),
			expectedRiskLevel:    RiskLevelLow,
			expectedAcceptable:   true,
		},
		// ---------------------------------------------------------------
		// 3. Verpackungsmaschine mit Schneideeinheit
		//    Foerderband + Klinge. Schnittverletzung.
		//    Zweihandbedienung, Sicherheitsrelais, Abdeckung.
		// ---------------------------------------------------------------
		{
			name:             "Verpackungsmaschine",
			description:      "Foerderband + Schneideeinheit, Schnittverletzungsgefahr",
			severity:         4, // Schwere Schnittverletzung
			exposure:         4, // Dauerbetrieb mit Bediener
			probability:      3, // Moeglich
			controlMaturity:  2,
			controlCoverage:  0.7,
			testEvidence:     0.5,
			hasJustification: true,
			// Inherent: 4*4*3 = 48
			// C_eff: 0.2*(2/4) + 0.5*0.7 + 0.3*0.5 = 0.1 + 0.35 + 0.15 = 0.6
			// Residual: 48 * 0.4 = 19.2
			// Level: medium (>=15, <40)
			// Acceptable: 19.2 >= 15, allReduction=false (ComputeRisk default) → NOT acceptable
			expectedInherentRisk: 48,
			expectedCEff:         0.6,
			expectedResidualRisk: 48 * (1 - 0.6),
			expectedRiskLevel:    RiskLevelMedium,
			expectedAcceptable:   false, // ComputeRisk sets allReductionStepsApplied=false
		},
		// ---------------------------------------------------------------
		// 4. Automatisierte Pressanlage
		//    Quetschung im Pressbereich. Hoechste Gefaehrdung.
		//    Lichtvorhang, Kat-4-Steuerung, mechanische Verriegelung.
		// ---------------------------------------------------------------
		{
			name:             "Pressanlage",
			description:      "Automatische Presse, Quetschgefahr im Pressbereich",
			severity:         5, // Toedlich
			exposure:         4, // Bediener staendig im Bereich
			probability:      4, // Wahrscheinlich ohne Schutz
			controlMaturity:  4,
			controlCoverage:  0.9,
			testEvidence:     0.9,
			hasJustification: true,
			// Inherent: 5*4*4 = 80
			// C_eff: 0.2*(4/4) + 0.5*0.9 + 0.3*0.9 = 0.2 + 0.45 + 0.27 = 0.92
			// Residual: 80 * 0.08 = 6.4
			// Level: low (>=5, <15)
			// Acceptable: 6.4 < 15 → yes
			expectedInherentRisk: 80,
			expectedCEff:         0.92,
			expectedResidualRisk: 80 * (1 - 0.92),
			expectedRiskLevel:    RiskLevelLow,
			expectedAcceptable:   true,
		},
		// ---------------------------------------------------------------
		// 5. Lasergravur-Anlage (Klasse 4)
		//    Augenverletzung durch Laserstrahl.
		//    Geschlossene Kabine, Interlock.
		// ---------------------------------------------------------------
		{
			name:             "Lasergravur-Anlage",
			description:      "Klasse-4-Laser, Augenverletzungsgefahr",
			severity:         5, // Irreversible Augenschaeden
			exposure:         2, // Selten direkter Zugang
			probability:      2, // Selten bei geschlossener Kabine
			controlMaturity:  3,
			controlCoverage:  0.95,
			testEvidence:     0.8,
			hasJustification: false,
			// Inherent: 5*2*2 = 20
			// C_eff: 0.2*(3/4) + 0.5*0.95 + 0.3*0.8 = 0.15 + 0.475 + 0.24 = 0.865
			// Residual: 20 * 0.135 = 2.7
			// Level: negligible (<5)
			// Acceptable: 2.7 < 15 → yes
			expectedInherentRisk: 20,
			expectedCEff:         0.865,
			expectedResidualRisk: 20 * (1 - 0.865),
			expectedRiskLevel:    RiskLevelNegligible,
			expectedAcceptable:   true,
		},
		// ---------------------------------------------------------------
		// 6. Fahrerloses Transportsystem (AGV)
		//    Kollision mit Personen in Produktionshalle.
		//    Laserscanner, Not-Aus, Geschwindigkeitsbegrenzung.
		// ---------------------------------------------------------------
		{
			name:             "AGV (Fahrerloses Transportsystem)",
			description:      "Autonomes Fahrzeug, Kollisionsgefahr mit Personen",
			severity:         4, // Schwere Verletzung
			exposure:         4, // Dauerhaft Personen in der Naehe
			probability:      3, // Moeglich in offener Umgebung
			controlMaturity:  3,
			controlCoverage:  0.7,
			testEvidence:     0.6,
			hasJustification: true,
			// Inherent: 4*4*3 = 48
			// C_eff: 0.2*(3/4) + 0.5*0.7 + 0.3*0.6 = 0.15 + 0.35 + 0.18 = 0.68
			// Residual: 48 * 0.32 = 15.36
			// Level: medium (>=15, <40)
			// Acceptable: 15.36 >= 15, allReduction=false → NOT acceptable
			expectedInherentRisk: 48,
			expectedCEff:         0.68,
			expectedResidualRisk: 48 * (1 - 0.68),
			expectedRiskLevel:    RiskLevelMedium,
			expectedAcceptable:   false, // ComputeRisk sets allReductionStepsApplied=false
		},
		// ---------------------------------------------------------------
		// 7. Abfuellanlage fuer Chemikalien
		//    Kontakt mit gefaehrlichem Medium.
		//    Geschlossene Leitungen, Leckageerkennung.
		// ---------------------------------------------------------------
		{
			name:             "Abfuellanlage Chemikalien",
			description:      "Chemikalien-Abfuellung, Kontaktgefahr",
			severity:         4, // Schwere Veraetzung
			exposure:         2, // Gelegentlicher Zugang
			probability:      2, // Selten bei geschlossenen Leitungen
			controlMaturity:  3,
			controlCoverage:  0.85,
			testEvidence:     0.7,
			hasJustification: false,
			// Inherent: 4*2*2 = 16
			// C_eff: 0.2*(3/4) + 0.5*0.85 + 0.3*0.7 = 0.15 + 0.425 + 0.21 = 0.785
			// Residual: 16 * 0.215 = 3.44
			// Level: negligible (<5)
			// Acceptable: 3.44 < 15 → yes
			expectedInherentRisk: 16,
			expectedCEff:         0.785,
			expectedResidualRisk: 16 * (1 - 0.785),
			expectedRiskLevel:    RiskLevelNegligible,
			expectedAcceptable:   true,
		},
		// ---------------------------------------------------------------
		// 8. Industrie-3D-Drucker (Metallpulver)
		//    Feinstaub-Inhalation, Explosionsgefahr.
		//    Absauganlage, Explosionsschutz.
		// ---------------------------------------------------------------
		{
			name:             "Industrie-3D-Drucker Metallpulver",
			description:      "Metallpulver-Drucker, Feinstaub und ATEX",
			severity:         4, // Schwere Lungenschaeden / Explosion
			exposure:         3, // Regelmaessig (Druckjob-Wechsel)
			probability:      3, // Moeglich
			controlMaturity:  2,
			controlCoverage:  0.6,
			testEvidence:     0.5,
			hasJustification: true,
			// Inherent: 4*3*3 = 36
			// C_eff: 0.2*(2/4) + 0.5*0.6 + 0.3*0.5 = 0.1 + 0.3 + 0.15 = 0.55
			// Residual: 36 * 0.45 = 16.2
			// Level: medium (>=15, <40)
			// Acceptable: 16.2 >= 15, allReduction=false → NOT acceptable
			expectedInherentRisk: 36,
			expectedCEff:         0.55,
			expectedResidualRisk: 36 * (1 - 0.55),
			expectedRiskLevel:    RiskLevelMedium,
			expectedAcceptable:   false,
		},
		// ---------------------------------------------------------------
		// 9. Automatisches Hochregallager
		//    Absturz von Lasten, Regalbediengeraet.
		//    Lastsicherung, Sensoren, regelmaessige Wartung.
		// ---------------------------------------------------------------
		{
			name:             "Hochregallager",
			description:      "Automatisches Regallager, Lastabsturzgefahr",
			severity:         5, // Toedlich bei Absturz schwerer Lasten
			exposure:         2, // Selten (automatisiert, Wartungszugang)
			probability:      2, // Selten bei ordnungsgemaessem Betrieb
			controlMaturity:  3,
			controlCoverage:  0.8,
			testEvidence:     0.7,
			hasJustification: false,
			// Inherent: 5*2*2 = 20
			// C_eff: 0.2*(3/4) + 0.5*0.8 + 0.3*0.7 = 0.15 + 0.4 + 0.21 = 0.76
			// Residual: 20 * 0.24 = 4.8
			// Level: negligible (<5)
			// Acceptable: 4.8 < 15 → yes
			expectedInherentRisk: 20,
			expectedCEff:         0.76,
			expectedResidualRisk: 20 * (1 - 0.76),
			expectedRiskLevel:    RiskLevelNegligible,
			expectedAcceptable:   true,
		},
		// ---------------------------------------------------------------
		// 10. KI-Bildverarbeitung Qualitaetskontrolle
		//     Fehlklassifikation → sicherheitsrelevantes Bauteil wird freigegeben.
		//     Redundante Pruefung, Validierungsdatensatz, KI-Risikobeurteilung.
		//     AI Act relevant.
		// ---------------------------------------------------------------
		{
			name:             "KI-Qualitaetskontrolle",
			description:      "KI-Vision fuer Bauteilpruefung, Fehlklassifikationsgefahr (AI Act relevant)",
			severity:         4, // Fehlerhaftes Sicherheitsbauteil im Feld
			exposure:         5, // Kontinuierlich (jedes Bauteil)
			probability:      3, // Moeglich (ML nie 100% korrekt)
			controlMaturity:  2,
			controlCoverage:  0.5,
			testEvidence:     0.6,
			hasJustification: true,
			// Inherent: 4*5*3 = 60
			// C_eff: 0.2*(2/4) + 0.5*0.5 + 0.3*0.6 = 0.1 + 0.25 + 0.18 = 0.53
			// Residual: 60 * 0.47 = 28.2
			// Level: medium (>=15, <40)
			// Acceptable: 28.2 >= 15, allReduction=false → NOT acceptable
			expectedInherentRisk: 60,
			expectedCEff:         0.53,
			expectedResidualRisk: 60 * (1 - 0.53),
			expectedRiskLevel:    RiskLevelMedium,
			expectedAcceptable:   false,
		},
	}
}

func TestReferenceMachines_ComputeRisk(t *testing.T) {
	engine := NewRiskEngine()
	machines := getReferenceMachines()

	for _, m := range machines {
		t.Run(m.name, func(t *testing.T) {
			input := RiskComputeInput{
				Severity:         m.severity,
				Exposure:         m.exposure,
				Probability:      m.probability,
				ControlMaturity:  m.controlMaturity,
				ControlCoverage:  m.controlCoverage,
				TestEvidence:     m.testEvidence,
				HasJustification: m.hasJustification,
			}

			result, err := engine.ComputeRisk(input)
			if err != nil {
				t.Fatalf("ComputeRisk returned error: %v", err)
			}

			// Verify inherent risk
			if !almostEqual(result.InherentRisk, m.expectedInherentRisk) {
				t.Errorf("InherentRisk = %v, want %v", result.InherentRisk, m.expectedInherentRisk)
			}

			// Verify control effectiveness
			if !almostEqual(result.ControlEffectiveness, m.expectedCEff) {
				t.Errorf("ControlEffectiveness = %v, want %v", result.ControlEffectiveness, m.expectedCEff)
			}

			// Verify residual risk
			if !almostEqual(result.ResidualRisk, m.expectedResidualRisk) {
				t.Errorf("ResidualRisk = %v, want %v (diff: %v)",
					result.ResidualRisk, m.expectedResidualRisk,
					math.Abs(result.ResidualRisk-m.expectedResidualRisk))
			}

			// Verify risk level
			if result.RiskLevel != m.expectedRiskLevel {
				t.Errorf("RiskLevel = %v, want %v (residual=%v)",
					result.RiskLevel, m.expectedRiskLevel, result.ResidualRisk)
			}

			// Verify acceptability
			if result.IsAcceptable != m.expectedAcceptable {
				t.Errorf("IsAcceptable = %v, want %v (residual=%v, level=%v)",
					result.IsAcceptable, m.expectedAcceptable, result.ResidualRisk, result.RiskLevel)
			}
		})
	}
}

// TestReferenceMachines_InherentRiskDistribution verifies that the 10 machines
// cover a meaningful range of inherent risk values (not all clustered).
func TestReferenceMachines_InherentRiskDistribution(t *testing.T) {
	machines := getReferenceMachines()

	var minRisk, maxRisk float64
	minRisk = 999
	levelCounts := map[RiskLevel]int{}

	for _, m := range machines {
		if m.expectedInherentRisk < minRisk {
			minRisk = m.expectedInherentRisk
		}
		if m.expectedInherentRisk > maxRisk {
			maxRisk = m.expectedInherentRisk
		}
		levelCounts[m.expectedRiskLevel]++
	}

	// Should span a meaningful range
	if maxRisk-minRisk < 40 {
		t.Errorf("Inherent risk range too narrow: [%v, %v], want spread >= 40", minRisk, maxRisk)
	}

	// Should cover at least 3 different risk levels
	if len(levelCounts) < 3 {
		t.Errorf("Only %d risk levels covered, want at least 3: %v", len(levelCounts), levelCounts)
	}
}

// TestReferenceMachines_AcceptabilityMix verifies that the test suite has both
// acceptable and unacceptable outcomes.
func TestReferenceMachines_AcceptabilityMix(t *testing.T) {
	machines := getReferenceMachines()

	acceptableCount := 0
	unacceptableCount := 0
	for _, m := range machines {
		if m.expectedAcceptable {
			acceptableCount++
		} else {
			unacceptableCount++
		}
	}

	if acceptableCount == 0 {
		t.Error("No acceptable machines in test suite — need at least one")
	}
	if unacceptableCount == 0 {
		t.Error("No unacceptable machines in test suite — need at least one")
	}

	t.Logf("Acceptability mix: %d acceptable, %d unacceptable out of %d machines",
		acceptableCount, unacceptableCount, len(machines))
}

// ============================================================================
// 9. Edge Cases
// ============================================================================

func TestClamp(t *testing.T) {
	tests := []struct {
		name       string
		v, lo, hi  int
		expected   int
	}{
		{"in range", 3, 1, 5, 3},
		{"at low", 1, 1, 5, 1},
		{"at high", 5, 1, 5, 5},
		{"below low", 0, 1, 5, 1},
		{"above high", 10, 1, 5, 5},
		{"negative", -100, 0, 4, 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := clamp(tt.v, tt.lo, tt.hi)
			if result != tt.expected {
				t.Errorf("clamp(%d, %d, %d) = %d, want %d", tt.v, tt.lo, tt.hi, result, tt.expected)
			}
		})
	}
}

// ============================================================================
// 10. ISO 12100 Mode — S × F × P × A (direct multiplication)
// ============================================================================

func TestCalculateInherentRisk_ISO12100Mode(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name               string
		s, f, p, a         int
		expected           float64
	}{
		// Minimum: 1×1×1×1 = 1
		{"min 1×1×1×1", 1, 1, 1, 1, 1},
		// Maximum: 5×5×5×5 = 625
		{"max 5×5×5×5", 5, 5, 5, 5, 625},
		// Typical mid-range: 3×3×3×3 = 81
		{"mid 3×3×3×3", 3, 3, 3, 3, 81},
		// High severity, low avoidance: 5×3×4×1 = 60
		{"high S low A", 5, 3, 4, 1, 60},
		// All factors high: 4×4×4×4 = 256
		{"high 4×4×4×4", 4, 4, 4, 4, 256},
		// Low risk: 2×1×2×1 = 4
		{"low risk", 2, 1, 2, 1, 4},
		// At not_acceptable boundary: 5×5×3×5 = 375
		{"above 300", 5, 5, 3, 5, 375},
		// At very_high boundary: 4×3×4×4 = 192
		{"very high range", 4, 3, 4, 4, 192},
		// At high boundary: 3×3×3×3 = 81
		{"high range", 3, 3, 3, 3, 81},
		// At medium range: 2×3×3×2 = 36
		{"medium range", 2, 3, 3, 2, 36},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.CalculateInherentRisk(tt.s, tt.f, tt.p, tt.a)
			if !almostEqual(result, tt.expected) {
				t.Errorf("CalculateInherentRisk(%d, %d, %d, %d) = %v, want %v",
					tt.s, tt.f, tt.p, tt.a, result, tt.expected)
			}
		})
	}
}

func TestDetermineRiskLevelISO(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name     string
		risk     float64
		expected RiskLevel
	}{
		// not_acceptable: > 300
		{"not_acceptable at 301", 301, RiskLevelNotAcceptable},
		{"not_acceptable at 625", 625, RiskLevelNotAcceptable},
		{"not_acceptable at 400", 400, RiskLevelNotAcceptable},
		// very_high: 151-300
		{"very_high at 300", 300, RiskLevelVeryHigh},
		{"very_high at 151", 151, RiskLevelVeryHigh},
		{"very_high at 200", 200, RiskLevelVeryHigh},
		// high: 61-150
		{"high at 150", 150, RiskLevelHigh},
		{"high at 61", 61, RiskLevelHigh},
		{"high at 100", 100, RiskLevelHigh},
		// medium: 21-60
		{"medium at 60", 60, RiskLevelMedium},
		{"medium at 21", 21, RiskLevelMedium},
		{"medium at 40", 40, RiskLevelMedium},
		// low: 1-20
		{"low at 20", 20, RiskLevelLow},
		{"low at 1", 1, RiskLevelLow},
		{"low at 10", 10, RiskLevelLow},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := e.DetermineRiskLevelISO(tt.risk)
			if result != tt.expected {
				t.Errorf("DetermineRiskLevelISO(%v) = %v, want %v", tt.risk, result, tt.expected)
			}
		})
	}
}

func TestAvoidanceBackwardCompat(t *testing.T) {
	e := NewRiskEngine()

	// With avoidance=0, formula must remain S×E×P (legacy mode)
	tests := []struct {
		s, ex, p int
		expected float64
	}{
		{5, 5, 5, 125},
		{3, 3, 3, 27},
		{1, 1, 1, 1},
		{4, 2, 5, 40},
	}

	for _, tt := range tests {
		result := e.CalculateInherentRisk(tt.s, tt.ex, tt.p, 0)
		if !almostEqual(result, tt.expected) {
			t.Errorf("Legacy mode: CalculateInherentRisk(%d,%d,%d,0) = %v, want %v",
				tt.s, tt.ex, tt.p, result, tt.expected)
		}
	}
}

func TestValidateProtectiveMeasureHierarchy(t *testing.T) {
	e := NewRiskEngine()

	tests := []struct {
		name          string
		reductionType ReductionType
		existing      []Mitigation
		wantWarnings  int
	}{
		{
			name:          "design measure — no warning",
			reductionType: ReductionTypeDesign,
			existing:      nil,
			wantWarnings:  0,
		},
		{
			name:          "information without any — full warning",
			reductionType: ReductionTypeInformation,
			existing:      nil,
			wantWarnings:  1,
		},
		{
			name:          "information with design — no warning",
			reductionType: ReductionTypeInformation,
			existing: []Mitigation{
				{ReductionType: ReductionTypeDesign, Status: MitigationStatusImplemented},
				{ReductionType: ReductionTypeProtective, Status: MitigationStatusPlanned},
			},
			wantWarnings: 0,
		},
		{
			name:          "information with only protective — missing design warning",
			reductionType: ReductionTypeInformation,
			existing: []Mitigation{
				{ReductionType: ReductionTypeProtective, Status: MitigationStatusImplemented},
			},
			wantWarnings: 1,
		},
		{
			name:          "information with rejected measures — full warning",
			reductionType: ReductionTypeInformation,
			existing: []Mitigation{
				{ReductionType: ReductionTypeDesign, Status: MitigationStatusRejected},
				{ReductionType: ReductionTypeProtective, Status: MitigationStatusRejected},
			},
			wantWarnings: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			warnings := e.ValidateProtectiveMeasureHierarchy(tt.reductionType, tt.existing)
			if len(warnings) != tt.wantWarnings {
				t.Errorf("got %d warnings, want %d: %v", len(warnings), tt.wantWarnings, warnings)
			}
		})
	}
}

func TestComputeRisk_ISOMode(t *testing.T) {
	e := NewRiskEngine()

	// ISO mode: avoidance >= 1 → uses DetermineRiskLevelISO for inherent risk classification
	tests := []struct {
		name       string
		input      RiskComputeInput
		wantLevel  RiskLevel
	}{
		{
			name: "ISO low risk",
			input: RiskComputeInput{
				Severity: 2, Exposure: 1, Probability: 2, Avoidance: 1,
				ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
			},
			wantLevel: RiskLevelLow, // 2×1×2×1 = 4 → low
		},
		{
			name: "ISO medium risk",
			input: RiskComputeInput{
				Severity: 3, Exposure: 2, Probability: 3, Avoidance: 2,
				ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
			},
			wantLevel: RiskLevelMedium, // 3×2×3×2 = 36 → medium
		},
		{
			name: "ISO high risk",
			input: RiskComputeInput{
				Severity: 4, Exposure: 3, Probability: 3, Avoidance: 3,
				ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
			},
			wantLevel: RiskLevelHigh, // 4×3×3×3 = 108 → high
		},
		{
			name: "ISO very high risk",
			input: RiskComputeInput{
				Severity: 4, Exposure: 4, Probability: 4, Avoidance: 3,
				ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
			},
			wantLevel: RiskLevelVeryHigh, // 4×4×4×3 = 192 → very_high
		},
		{
			name: "ISO not acceptable",
			input: RiskComputeInput{
				Severity: 5, Exposure: 5, Probability: 4, Avoidance: 4,
				ControlMaturity: 4, ControlCoverage: 1.0, TestEvidence: 1.0,
			},
			wantLevel: RiskLevelNotAcceptable, // 5×5×4×4 = 400 → not_acceptable
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := e.ComputeRisk(tt.input)
			if err != nil {
				t.Fatalf("ComputeRisk returned error: %v", err)
			}
			if result.RiskLevel != tt.wantLevel {
				t.Errorf("RiskLevel = %v, want %v (inherent=%v)",
					result.RiskLevel, tt.wantLevel, result.InherentRisk)
			}
		})
	}
}

// ============================================================================
// 11. Edge Cases (continued)
// ============================================================================

func TestClampFloat(t *testing.T) {
	tests := []struct {
		name           string
		v, lo, hi      float64
		expected       float64
	}{
		{"in range", 0.5, 0, 1, 0.5},
		{"at low", 0, 0, 1, 0},
		{"at high", 1.0, 0, 1, 1.0},
		{"below low", -0.5, 0, 1, 0},
		{"above high", 2.5, 0, 1, 1.0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := clampFloat(tt.v, tt.lo, tt.hi)
			if !almostEqual(result, tt.expected) {
				t.Errorf("clampFloat(%v, %v, %v) = %v, want %v", tt.v, tt.lo, tt.hi, result, tt.expected)
			}
		})
	}
}
