package iace

import (
	"encoding/json"
	"math"
	"testing"

	"github.com/google/uuid"
)

// helper to build metadata with the given keys set to non-empty string values.
func metadataWith(keys ...string) json.RawMessage {
	m := make(map[string]interface{})
	for _, k := range keys {
		m[k] = "defined"
	}
	data, _ := json.Marshal(m)
	return data
}

func TestCompletenessCheck_EmptyContext(t *testing.T) {
	checker := NewCompletenessChecker()

	ctx := &CompletenessContext{
		Project: nil,
	}

	result := checker.Check(ctx)

	if result.CanExport {
		t.Error("CanExport should be false for empty context")
	}
	// With nil project, most gates fail. However, some auto-pass:
	// G06 (AI classification): auto-passes when HasAI=false
	// G22 (critical/high mitigated): auto-passes when no critical/high assessments exist
	// G23 (mitigations verified): auto-passes when no mitigations (empty list)
	// G42 (AI documents): auto-passes when HasAI=false
	// That gives 4 required gates passing even with empty context.
	if result.PassedRequired != 4 {
		t.Errorf("PassedRequired = %d, want 4 (G06, G22, G23, G42 auto-pass)", result.PassedRequired)
	}
	// Score should be low: 4/20 * 80 = 16
	if result.Score > 20 {
		t.Errorf("Score = %f, expected <= 20 for empty context", result.Score)
	}
	if len(result.Gates) == 0 {
		t.Error("Gates should not be empty")
	}
}

func TestCompletenessCheck_MinimalValidProject(t *testing.T) {
	checker := NewCompletenessChecker()

	projectID := uuid.New()
	hazardID := uuid.New()
	componentID := uuid.New()

	ctx := &CompletenessContext{
		Project: &Project{
			ID:              projectID,
			MachineName:     "TestMachine",
			Description:     "A test machine for unit testing",
			Manufacturer:    "TestCorp",
			CEMarkingTarget: "2023/1230",
			Metadata:        metadataWith("operating_limits", "foreseeable_misuse"),
		},
		Components: []Component{
			{ID: componentID, Name: "SafetyPLC", ComponentType: ComponentTypeSoftware, IsSafetyRelevant: true},
		},
		Classifications: []RegulatoryClassification{
			{Regulation: RegulationAIAct},
			{Regulation: RegulationMachineryRegulation},
			{Regulation: RegulationNIS2},
			{Regulation: RegulationCRA},
		},
		Hazards: []Hazard{
			{ID: hazardID, ProjectID: projectID, ComponentID: componentID, Name: "TestHazard", Category: "test"},
		},
		Assessments: []RiskAssessment{
			{ID: uuid.New(), HazardID: hazardID, RiskLevel: RiskLevelLow, IsAcceptable: true},
		},
		Mitigations: []Mitigation{
			{ID: uuid.New(), HazardID: hazardID, Status: MitigationStatusVerified},
		},
		Evidence: []Evidence{
			{ID: uuid.New(), ProjectID: projectID, FileName: "test.pdf"},
		},
		TechFileSections: []TechFileSection{
			{ID: uuid.New(), ProjectID: projectID, SectionType: "risk_assessment_report"},
			{ID: uuid.New(), ProjectID: projectID, SectionType: "hazard_log_combined"},
		},
		HasAI:                    false,
		PatternMatchingPerformed: true,
	}

	result := checker.Check(ctx)

	if !result.CanExport {
		t.Error("CanExport should be true for fully valid project")
		for _, g := range result.Gates {
			if g.Required && !g.Passed {
				t.Errorf("  Required gate %s (%s) not passed: %s", g.ID, g.Label, g.Details)
			}
		}
	}
	if result.PassedRequired != result.TotalRequired {
		t.Errorf("PassedRequired = %d, TotalRequired = %d, want all passed", result.PassedRequired, result.TotalRequired)
	}
	// Score should be at least 80 (all required) + 15 (evidence recommended) = 95
	if result.Score < 80 {
		t.Errorf("Score = %f, expected >= 80 for fully valid project", result.Score)
	}
}

func TestCompletenessCheck_PartialRequiredGates(t *testing.T) {
	checker := NewCompletenessChecker()

	// Provide only some required data: machine name, manufacturer, description, one component, one safety-relevant.
	// Missing: operating_limits, foreseeable_misuse, classifications, hazards, assessments, tech files.
	ctx := &CompletenessContext{
		Project: &Project{
			MachineName:  "PartialMachine",
			Description:  "Some description",
			Manufacturer: "TestCorp",
		},
		Components: []Component{
			{Name: "Sensor", ComponentType: ComponentTypeSensor, IsSafetyRelevant: true},
		},
		HasAI: false,
	}

	result := checker.Check(ctx)

	if result.CanExport {
		t.Error("CanExport should be false when not all required gates pass")
	}
	if result.PassedRequired == 0 {
		t.Error("Some required gates should pass (G01, G02, G05, G06, G07, G08)")
	}
	if result.PassedRequired >= result.TotalRequired {
		t.Errorf("PassedRequired (%d) should be less than TotalRequired (%d)", result.PassedRequired, result.TotalRequired)
	}
	// Score should be partial
	if result.Score <= 0 || result.Score >= 95 {
		t.Errorf("Score = %f, expected partial score between 0 and 95", result.Score)
	}
}

func TestCompletenessCheck_G06_AIClassificationGate(t *testing.T) {
	checker := NewCompletenessChecker()

	tests := []struct {
		name            string
		hasAI           bool
		classifications []RegulatoryClassification
		wantG06Passed   bool
	}{
		{
			name:            "no AI present auto-passes G06",
			hasAI:           false,
			classifications: nil,
			wantG06Passed:   true,
		},
		{
			name:            "AI present without classification fails G06",
			hasAI:           true,
			classifications: nil,
			wantG06Passed:   false,
		},
		{
			name:  "AI present with AI Act classification passes G06",
			hasAI: true,
			classifications: []RegulatoryClassification{
				{Regulation: RegulationAIAct},
			},
			wantG06Passed: true,
		},
		{
			name:  "AI present with non-AI classification fails G06",
			hasAI: true,
			classifications: []RegulatoryClassification{
				{Regulation: RegulationCRA},
			},
			wantG06Passed: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &CompletenessContext{
				Project:         &Project{MachineName: "Test"},
				Classifications: tt.classifications,
				HasAI:           tt.hasAI,
			}

			result := checker.Check(ctx)

			for _, g := range result.Gates {
				if g.ID == "G06" {
					if g.Passed != tt.wantG06Passed {
						t.Errorf("G06 Passed = %v, want %v", g.Passed, tt.wantG06Passed)
					}
					return
				}
			}
			t.Error("G06 gate not found in results")
		})
	}
}

func TestCompletenessCheck_G42_AIDocumentsGate(t *testing.T) {
	checker := NewCompletenessChecker()

	tests := []struct {
		name             string
		hasAI            bool
		techFileSections []TechFileSection
		wantG42Passed    bool
	}{
		{
			name:             "no AI auto-passes G42",
			hasAI:            false,
			techFileSections: nil,
			wantG42Passed:    true,
		},
		{
			name:             "AI present without tech files fails G42",
			hasAI:            true,
			techFileSections: nil,
			wantG42Passed:    false,
		},
		{
			name:  "AI present with only intended_purpose fails G42",
			hasAI: true,
			techFileSections: []TechFileSection{
				{SectionType: "ai_intended_purpose"},
			},
			wantG42Passed: false,
		},
		{
			name:  "AI present with only model_description fails G42",
			hasAI: true,
			techFileSections: []TechFileSection{
				{SectionType: "ai_model_description"},
			},
			wantG42Passed: false,
		},
		{
			name:  "AI present with both AI sections passes G42",
			hasAI: true,
			techFileSections: []TechFileSection{
				{SectionType: "ai_intended_purpose"},
				{SectionType: "ai_model_description"},
			},
			wantG42Passed: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &CompletenessContext{
				Project:          &Project{MachineName: "Test"},
				TechFileSections: tt.techFileSections,
				HasAI:            tt.hasAI,
			}

			result := checker.Check(ctx)

			for _, g := range result.Gates {
				if g.ID == "G42" {
					if g.Passed != tt.wantG42Passed {
						t.Errorf("G42 Passed = %v, want %v", g.Passed, tt.wantG42Passed)
					}
					return
				}
			}
			t.Error("G42 gate not found in results")
		})
	}
}

func TestCompletenessCheck_G22_CriticalHighMitigated(t *testing.T) {
	checker := NewCompletenessChecker()

	hazardID := uuid.New()

	tests := []struct {
		name          string
		assessments   []RiskAssessment
		mitigations   []Mitigation
		wantG22Passed bool
	}{
		{
			name:          "no critical/high hazards auto-passes G22",
			assessments:   []RiskAssessment{{HazardID: hazardID, RiskLevel: RiskLevelLow}},
			mitigations:   nil,
			wantG22Passed: true,
		},
		{
			name:          "no assessments at all auto-passes G22 (no critical/high found)",
			assessments:   nil,
			mitigations:   nil,
			wantG22Passed: true,
		},
		{
			name:          "critical hazard without mitigation fails G22",
			assessments:   []RiskAssessment{{HazardID: hazardID, RiskLevel: RiskLevelCritical}},
			mitigations:   nil,
			wantG22Passed: false,
		},
		{
			name:          "high hazard without mitigation fails G22",
			assessments:   []RiskAssessment{{HazardID: hazardID, RiskLevel: RiskLevelHigh}},
			mitigations:   nil,
			wantG22Passed: false,
		},
		{
			name:        "critical hazard with mitigation passes G22",
			assessments: []RiskAssessment{{HazardID: hazardID, RiskLevel: RiskLevelCritical}},
			mitigations: []Mitigation{{HazardID: hazardID, Status: MitigationStatusVerified}},
			wantG22Passed: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &CompletenessContext{
				Project:     &Project{MachineName: "Test"},
				Assessments: tt.assessments,
				Mitigations: tt.mitigations,
			}

			result := checker.Check(ctx)

			for _, g := range result.Gates {
				if g.ID == "G22" {
					if g.Passed != tt.wantG22Passed {
						t.Errorf("G22 Passed = %v, want %v", g.Passed, tt.wantG22Passed)
					}
					return
				}
			}
			t.Error("G22 gate not found in results")
		})
	}
}

func TestCompletenessCheck_G23_MitigationsVerified(t *testing.T) {
	checker := NewCompletenessChecker()

	hazardID := uuid.New()

	tests := []struct {
		name          string
		mitigations   []Mitigation
		wantG23Passed bool
	}{
		{
			name:          "no mitigations passes G23",
			mitigations:   nil,
			wantG23Passed: true,
		},
		{
			name: "all mitigations verified passes G23",
			mitigations: []Mitigation{
				{HazardID: hazardID, Status: MitigationStatusVerified},
				{HazardID: hazardID, Status: MitigationStatusVerified},
			},
			wantG23Passed: true,
		},
		{
			name: "one mitigation still implemented fails G23",
			mitigations: []Mitigation{
				{HazardID: hazardID, Status: MitigationStatusVerified},
				{HazardID: hazardID, Status: MitigationStatusImplemented},
			},
			wantG23Passed: false,
		},
		{
			name: "planned mitigations fail G23 (not yet verified)",
			mitigations: []Mitigation{
				{HazardID: hazardID, Status: MitigationStatusPlanned},
			},
			wantG23Passed: false,
		},
		{
			name: "rejected mitigations pass G23",
			mitigations: []Mitigation{
				{HazardID: hazardID, Status: MitigationStatusRejected},
			},
			wantG23Passed: true,
		},
		{
			name: "mix of verified planned rejected fails G23",
			mitigations: []Mitigation{
				{HazardID: hazardID, Status: MitigationStatusVerified},
				{HazardID: hazardID, Status: MitigationStatusPlanned},
				{HazardID: hazardID, Status: MitigationStatusRejected},
			},
			wantG23Passed: false,
		},
		{
			name: "mix of verified and rejected passes G23",
			mitigations: []Mitigation{
				{HazardID: hazardID, Status: MitigationStatusVerified},
				{HazardID: hazardID, Status: MitigationStatusRejected},
			},
			wantG23Passed: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &CompletenessContext{
				Project:     &Project{MachineName: "Test"},
				Mitigations: tt.mitigations,
			}

			result := checker.Check(ctx)

			for _, g := range result.Gates {
				if g.ID == "G23" {
					if g.Passed != tt.wantG23Passed {
						t.Errorf("G23 Passed = %v, want %v", g.Passed, tt.wantG23Passed)
					}
					return
				}
			}
			t.Error("G23 gate not found in results")
		})
	}
}

func TestCompletenessCheck_G09_PatternMatchingPerformed(t *testing.T) {
	checker := NewCompletenessChecker()

	tests := []struct {
		name          string
		performed     bool
		wantG09Passed bool
	}{
		{
			name:          "pattern matching not performed fails G09",
			performed:     false,
			wantG09Passed: false,
		},
		{
			name:          "pattern matching performed passes G09",
			performed:     true,
			wantG09Passed: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &CompletenessContext{
				Project:                  &Project{MachineName: "Test"},
				PatternMatchingPerformed: tt.performed,
			}

			result := checker.Check(ctx)

			for _, g := range result.Gates {
				if g.ID == "G09" {
					if g.Passed != tt.wantG09Passed {
						t.Errorf("G09 Passed = %v, want %v", g.Passed, tt.wantG09Passed)
					}
					return
				}
			}
			t.Error("G09 gate not found in results")
		})
	}
}

func TestCompletenessCheck_G24_ResidualRiskAccepted(t *testing.T) {
	checker := NewCompletenessChecker()

	hazardID := uuid.New()

	tests := []struct {
		name          string
		assessments   []RiskAssessment
		wantG24Passed bool
	}{
		{
			name:          "no assessments fails G24",
			assessments:   nil,
			wantG24Passed: false,
		},
		{
			name: "all assessments acceptable passes G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: true, RiskLevel: RiskLevelMedium},
				{HazardID: hazardID, IsAcceptable: true, RiskLevel: RiskLevelHigh},
			},
			wantG24Passed: true,
		},
		{
			name: "not acceptable but low risk passes G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: false, RiskLevel: RiskLevelLow},
			},
			wantG24Passed: true,
		},
		{
			name: "not acceptable but negligible risk passes G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: false, RiskLevel: RiskLevelNegligible},
			},
			wantG24Passed: true,
		},
		{
			name: "not acceptable with high risk fails G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: false, RiskLevel: RiskLevelHigh},
			},
			wantG24Passed: false,
		},
		{
			name: "not acceptable with critical risk fails G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: false, RiskLevel: RiskLevelCritical},
			},
			wantG24Passed: false,
		},
		{
			name: "not acceptable with medium risk fails G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: false, RiskLevel: RiskLevelMedium},
			},
			wantG24Passed: false,
		},
		{
			name: "mix acceptable and unacceptable high fails G24",
			assessments: []RiskAssessment{
				{HazardID: hazardID, IsAcceptable: true, RiskLevel: RiskLevelHigh},
				{HazardID: hazardID, IsAcceptable: false, RiskLevel: RiskLevelHigh},
			},
			wantG24Passed: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &CompletenessContext{
				Project:     &Project{MachineName: "Test"},
				Assessments: tt.assessments,
			}

			result := checker.Check(ctx)

			for _, g := range result.Gates {
				if g.ID == "G24" {
					if g.Passed != tt.wantG24Passed {
						t.Errorf("G24 Passed = %v, want %v", g.Passed, tt.wantG24Passed)
					}
					return
				}
			}
			t.Error("G24 gate not found in results")
		})
	}
}

func TestCompletenessCheck_ScoringFormula(t *testing.T) {
	tests := []struct {
		name              string
		passedRequired    int
		totalRequired     int
		passedRecommended int
		totalRecommended  int
		passedOptional    int
		totalOptional     int
		wantScore         float64
	}{
		{
			name:              "all zeros produces zero score",
			passedRequired:    0,
			totalRequired:     0,
			passedRecommended: 0,
			totalRecommended:  0,
			passedOptional:    0,
			totalOptional:     0,
			wantScore:         0,
		},
		{
			name:              "all required passed gives 80",
			passedRequired:    20,
			totalRequired:     20,
			passedRecommended: 0,
			totalRecommended:  1,
			passedOptional:    0,
			totalOptional:     0,
			wantScore:         80,
		},
		{
			name:              "half required passed gives 40",
			passedRequired:    10,
			totalRequired:     20,
			passedRecommended: 0,
			totalRecommended:  1,
			passedOptional:    0,
			totalOptional:     0,
			wantScore:         40,
		},
		{
			name:              "all required and all recommended gives 95",
			passedRequired:    20,
			totalRequired:     20,
			passedRecommended: 1,
			totalRecommended:  1,
			passedOptional:    0,
			totalOptional:     0,
			wantScore:         95,
		},
		{
			name:              "all categories full gives 100",
			passedRequired:    20,
			totalRequired:     20,
			passedRecommended: 1,
			totalRecommended:  1,
			passedOptional:    1,
			totalOptional:     1,
			wantScore:         100,
		},
		{
			name:              "only recommended passed",
			passedRequired:    0,
			totalRequired:     20,
			passedRecommended: 1,
			totalRecommended:  1,
			passedOptional:    0,
			totalOptional:     0,
			wantScore:         15,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			score := calculateWeightedScore(
				tt.passedRequired, tt.totalRequired,
				tt.passedRecommended, tt.totalRecommended,
				tt.passedOptional, tt.totalOptional,
			)

			if math.Abs(score-tt.wantScore) > 0.01 {
				t.Errorf("calculateWeightedScore = %f, want %f", score, tt.wantScore)
			}
		})
	}
}

func TestCompletenessCheck_GateCountsAndCategories(t *testing.T) {
	checker := NewCompletenessChecker()

	ctx := &CompletenessContext{
		Project: &Project{MachineName: "Test"},
	}
	result := checker.Check(ctx)

	// The buildGateDefinitions function returns exactly 22 gates
	// (G01-G08: 8, G09: 1, G10-G13: 4, G20-G24: 5, G30: 1, G40-G42: 3 = 22 total)
	if len(result.Gates) != 22 {
		t.Errorf("Total gates = %d, want 22", len(result.Gates))
	}

	// Count required vs recommended
	requiredCount := 0
	recommendedCount := 0
	for _, g := range result.Gates {
		if g.Required {
			requiredCount++
		}
	}
	// G09 and G30 are recommended gates (Required=false, Recommended=true)
	// All others are required (20 required, 2 recommended)
	if requiredCount != 20 {
		t.Errorf("Required gates count = %d, want 20", requiredCount)
	}

	if result.TotalRequired != 20 {
		t.Errorf("TotalRequired = %d, want 20", result.TotalRequired)
	}

	// TotalRecommended should be 2 (G09, G30)
	if result.TotalRecommended != 2 {
		t.Errorf("TotalRecommended = %d, want 2", result.TotalRecommended)
	}
	_ = recommendedCount

	// Verify expected categories exist
	categories := make(map[string]int)
	for _, g := range result.Gates {
		categories[g.Category]++
	}

	expectedCategories := map[string]int{
		"onboarding":     9, // G01-G08 + G09 (recommended)
		"classification": 4,
		"hazard_risk":    5,
		"evidence":       1,
		"tech_file":      3,
	}
	for cat, expectedCount := range expectedCategories {
		if categories[cat] != expectedCount {
			t.Errorf("Category %q count = %d, want %d", cat, categories[cat], expectedCount)
		}
	}
}

func TestCompletenessCheck_FailedGateHasDetails(t *testing.T) {
	checker := NewCompletenessChecker()

	ctx := &CompletenessContext{
		Project: &Project{}, // empty project, many gates will fail
	}

	result := checker.Check(ctx)

	for _, g := range result.Gates {
		if !g.Passed && g.Details == "" {
			t.Errorf("Gate %s failed but has empty Details", g.ID)
		}
		if g.Passed && g.Details != "" {
			t.Errorf("Gate %s passed but has non-empty Details: %s", g.ID, g.Details)
		}
	}
}
