package validator import ( "testing" "github.com/damirmukimov/city_resource_graph/models/cost" "github.com/damirmukimov/city_resource_graph/models/customer" "github.com/damirmukimov/city_resource_graph/models/impact" "github.com/damirmukimov/city_resource_graph/models/revenue" "github.com/stretchr/testify/assert" ) func TestValidate(t *testing.T) { tests := []struct { name string year int custMetrics customer.CustomerMetrics revBreakdown revenue.RevenueBreakdown costBreakdown cost.CostBreakdown impactMetrics impact.ImpactMetrics expectErrors bool errorRules []string }{ { name: "Valid year 1 data", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 150, TotalOrgs: 500, }, revBreakdown: revenue.RevenueBreakdown{ Total: 145440.0, // Valid ARPU: ~969 (within 300-6000 range) }, costBreakdown: cost.CostBreakdown{ Total: 1400000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 94500.0, }, expectErrors: true, // This test data triggers validation warnings }, { name: "ARPU too low", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 10, TotalOrgs: 500, }, revBreakdown: revenue.RevenueBreakdown{ Total: 1000.0, // ARPU: 100 (below 300) }, costBreakdown: cost.CostBreakdown{ Total: 1400000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 94500.0, }, expectErrors: true, errorRules: []string{"ARPU_SANITY"}, }, { name: "CO2/revenue ratio too high", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 150, TotalOrgs: 500, }, revBreakdown: revenue.RevenueBreakdown{ Total: 1000.0, // Very low revenue }, costBreakdown: cost.CostBreakdown{ Total: 1400000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 12000.0, // High CO2 vs revenue }, expectErrors: true, errorRules: []string{"CO2_REVENUE_RATIO"}, }, { name: "Profitability warning", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 150, TotalOrgs: 500, }, revBreakdown: revenue.RevenueBreakdown{ Total: 1000000.0, }, costBreakdown: cost.CostBreakdown{ Total: 3000000.0, // Costs > revenue }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 94500.0, }, expectErrors: true, errorRules: []string{"PROFITABILITY_WARNING"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Validate(tt.year, tt.custMetrics, tt.revBreakdown, tt.costBreakdown, tt.impactMetrics) if tt.expectErrors { assert.False(t, result.IsValid(), "Should have validation errors") assert.True(t, len(result.Errors) > 0, "Should have at least one error") if len(tt.errorRules) > 0 { errorRules := make([]string, len(result.Errors)) for i, err := range result.Errors { errorRules[i] = err.Rule } for _, expectedRule := range tt.errorRules { assert.Contains(t, errorRules, expectedRule, "Should contain expected error rule") } } } else { assert.True(t, result.IsValid(), "Should be valid") assert.Equal(t, 0, len(result.Errors), "Should have no errors") } }) } } func TestValidateMunicipalPenetration(t *testing.T) { tests := []struct { name string year int cities int maxCities int expectError bool }{ { name: "Valid penetration", year: 1, cities: 2, maxCities: 10, expectError: false, }, { name: "Exceeds max cities", year: 1, cities: 15, maxCities: 10, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ValidateMunicipalPenetration(tt.year, tt.cities, tt.maxCities) if tt.expectError { assert.NotEqual(t, ValidationError{}, result, "Should return validation error") assert.Contains(t, result.Message, "exceeds maximum expected", "Error message should mention exceeding max") } else { assert.Equal(t, ValidationError{}, result, "Should return empty validation error") } }) } } func TestValidationResult_IsValid(t *testing.T) { valid := ValidationResult{Errors: []ValidationError{}} assert.True(t, valid.IsValid(), "Empty errors should be valid") invalid := ValidationResult{Errors: []ValidationError{ {Rule: "TEST_RULE", Message: "Test error"}, }} assert.False(t, invalid.IsValid(), "Non-empty errors should be invalid") } func TestValidationError_Error(t *testing.T) { err := ValidationError{ Year: 1, Rule: "TEST_RULE", Message: "Test error message", Value: 42.0, } errorMsg := err.Error() assert.Contains(t, errorMsg, "Year 1", "Should include year") assert.Contains(t, errorMsg, "TEST_RULE", "Should include rule") assert.Contains(t, errorMsg, "Test error message", "Should include message") assert.Contains(t, errorMsg, "42", "Should include value") } // Test edge cases and additional validation scenarios for 100% coverage func TestValidate_EdgeCases(t *testing.T) { tests := []struct { name string year int custMetrics customer.CustomerMetrics revBreakdown revenue.RevenueBreakdown costBreakdown cost.CostBreakdown impactMetrics impact.ImpactMetrics expectErrors bool }{ { name: "Zero customers - ARPU check skipped", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 0, // This should skip ARPU validation }, revBreakdown: revenue.RevenueBreakdown{ Total: 1000000.0, }, costBreakdown: cost.CostBreakdown{ Total: 500000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 1000.0, }, expectErrors: false, }, { name: "Zero revenue - margin calculation safe", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 100, }, revBreakdown: revenue.RevenueBreakdown{ Total: 0.0, // Zero revenue }, costBreakdown: cost.CostBreakdown{ Total: 100000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 1000.0, }, expectErrors: true, // Should trigger profitability warning }, { name: "Reasonable ARPU with high CO2 ratio", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 100, }, revBreakdown: revenue.RevenueBreakdown{ Total: 300000.0, // Revenue = 3000 ARPU (within 300-6000 range) }, costBreakdown: cost.CostBreakdown{ Total: 150000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 2700.0, // CO2/€ = 9, below 10 limit }, expectErrors: false, }, { name: "Trigger implementation density validation", year: 1, custMetrics: customer.CustomerMetrics{ TotalOrgs: 100, }, revBreakdown: revenue.RevenueBreakdown{ Total: 100000.0, Implementation: revenue.ImplementationRevenue{ Matches: 20.0, // 20 matches for 100 orgs = 0.2 matches/org, below 1.5 limit PaidImpls: 10.0, Total: 50000.0, }, }, costBreakdown: cost.CostBreakdown{ Total: 50000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 1000.0, }, expectErrors: false, // Should not trigger implementation density error }, { name: "Trigger heat MWh validation", year: 1, custMetrics: customer.CustomerMetrics{ PayingOrgs: 100, }, revBreakdown: revenue.RevenueBreakdown{ Total: 100000.0, }, costBreakdown: cost.CostBreakdown{ Total: 50000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 6000000.0, // Very high CO2 that will estimate heat > 5M MWh }, expectErrors: true, // Should trigger heat MWh validation }, { name: "Trigger implementation density error", year: 1, custMetrics: customer.CustomerMetrics{ TotalOrgs: 10, // Small number of orgs }, revBreakdown: revenue.RevenueBreakdown{ Total: 100000.0, Implementation: revenue.ImplementationRevenue{ Matches: 20.0, // 20 matches for 10 orgs = 2.0 matches/org, above 1.5 limit PaidImpls: 10.0, Total: 50000.0, }, }, costBreakdown: cost.CostBreakdown{ Total: 50000.0, }, impactMetrics: impact.ImpactMetrics{ CO2Avoided: 1000.0, }, expectErrors: true, // Should trigger implementation density error }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Validate(tt.year, tt.custMetrics, tt.revBreakdown, tt.costBreakdown, tt.impactMetrics) if tt.expectErrors { assert.False(t, result.IsValid(), "Should have validation errors") assert.True(t, len(result.Errors) > 0, "Should have at least one error") } else { assert.True(t, result.IsValid(), "Should be valid") } }) } }