package params import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLoadFromFile(t *testing.T) { p, err := LoadFromFile("../params.yaml") require.NoError(t, err, "Failed to load params.yaml") // Test time configuration assert.Equal(t, []int{1, 2, 3}, p.Time.Years, "Years should be [1,2,3]") // Test adoption parameters assert.Equal(t, 500, p.Adoption.TotalOrgs.GetYear(1), "Year 1 total orgs should be 500") assert.Equal(t, 2000, p.Adoption.TotalOrgs.GetYear(2), "Year 2 total orgs should be 2000") assert.Equal(t, 5000, p.Adoption.TotalOrgs.GetYear(3), "Year 3 total orgs should be 5000") assert.Equal(t, 0.30, p.Adoption.PayingShare.GetYear(1), "Year 1 paying share should be 0.30") // Test pricing assert.Equal(t, 35.0, p.Pricing.Basic, "Basic tier price should be 35") assert.Equal(t, 120.0, p.Pricing.Business, "Business tier price should be 120") assert.Equal(t, 400.0, p.Pricing.Enterprise, "Enterprise tier price should be 400") } // Test LoadFromFile with non-existent file func TestLoadFromFile_FileNotFound(t *testing.T) { _, err := LoadFromFile("non_existent_file.yaml") assert.Error(t, err, "Should return error for non-existent file") assert.Contains(t, err.Error(), "non_existent_file.yaml", "Error should mention the filename") } func TestValidate(t *testing.T) { tests := []struct { name string params *Params wantError bool errorMsg string }{ { name: "valid params", params: createValidParams(), wantError: false, }, { name: "no years", params: &Params{ Time: TimeParams{Years: []int{}}, }, wantError: true, errorMsg: "must have at least one year", }, { name: "missing total orgs", params: &Params{ Time: TimeParams{Years: []int{1}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{}, // empty }, }, wantError: true, errorMsg: "missing value for year 1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.params.Validate() if tt.wantError { assert.Error(t, err, "Expected validation error") assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected text") } else { assert.NoError(t, err, "Expected no validation error") } }) } } func TestYearlyInt_GetYear(t *testing.T) { yi := YearlyInt{ "1": 100, "2": 200, "3": 300, } assert.Equal(t, 100, yi.GetYear(1), "Year 1 should return 100") assert.Equal(t, 200, yi.GetYear(2), "Year 2 should return 200") assert.Equal(t, 300, yi.GetYear(3), "Year 3 should return 300") assert.Equal(t, 0, yi.GetYear(4), "Non-existent year should return 0") } func TestYearlyFloat_GetYear(t *testing.T) { yf := YearlyFloat{ "1": 1.5, "2": 2.5, "3": 3.5, } assert.Equal(t, 1.5, yf.GetYear(1), "Year 1 should return 1.5") assert.Equal(t, 2.5, yf.GetYear(2), "Year 2 should return 2.5") assert.Equal(t, 3.5, yf.GetYear(3), "Year 3 should return 3.5") assert.Equal(t, 0.0, yf.GetYear(4), "Non-existent year should return 0.0") } func TestYearlyTierMix_GetYear(t *testing.T) { ytm := YearlyTierMix{ "1": TierMix{Basic: 0.6, Business: 0.3, Enterprise: 0.1}, "2": TierMix{Basic: 0.5, Business: 0.35, Enterprise: 0.15}, } tier1 := ytm.GetYear(1) assert.Equal(t, 0.6, tier1.Basic, "Year 1 Basic should be 0.6") assert.Equal(t, 0.3, tier1.Business, "Year 1 Business should be 0.3") assert.Equal(t, 0.1, tier1.Enterprise, "Year 1 Enterprise should be 0.1") tier2 := ytm.GetYear(2) assert.Equal(t, 0.5, tier2.Basic, "Year 2 Basic should be 0.5") assert.Equal(t, 0.35, tier2.Business, "Year 2 Business should be 0.35") // Test empty case emptyTier := ytm.GetYear(3) assert.Equal(t, TierMix{}, emptyTier, "Non-existent year should return empty TierMix") } func createValidParams() *Params { return &Params{ Time: TimeParams{Years: []int{1, 2, 3}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000}, PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3}, }, Pricing: PricingParams{ Basic: 35.0, Business: 120.0, Enterprise: 400.0, TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, }, Transactions: TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600}, IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40}, ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000}, ServiceCommission: 0.15, GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000}, GroupCommission: 0.04, }, Municipal: MunicipalParams{ Cities: YearlyInt{"1": 1, "2": 2, "3": 4}, AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000}, DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000}, }, ImplServices: ImplementationParams{ MatchesPerOrg: 0.5, PaidShare: 0.25, AvgFee: 5000.0, }, Impact: ImpactParams{ HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000}, GridFactor: 0.3, HXEff: 0.9, Utilization: 0.7, WaterPerOrg: 25000.0, WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30}, WastePerOrg: 100.0, WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35}, }, Costs: CostParams{ Engineers: YearlyInt{"1": 8, "2": 12, "3": 15}, EngineerSalary: 100000.0, Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000}, MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000}, Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000}, }, Market: MarketParams{ TAM: 500000000000.0, AddressableDigital: 3000000000.0, PilotCityEconomicBenefit: 4000000.0, ScalabilityPotential: 400000000.0, EUIndustrialFacilities: 2100000, EnergyWastePotential: 0.45, ResourceCostReduction: 0.25, ViableExchangeRate: 0.15, PlatformCaptureRate: 0.50, SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000}, }, } } // Test LoadFromJSON function func TestLoadFromJSON(t *testing.T) { jsonData := `{ "time": {"years": [1, 2]}, "adoption": { "total_orgs": {"1": 100, "2": 200}, "paying_share": {"1": 0.5, "2": 0.6} }, "pricing": { "basic": 30.0, "business": 100.0, "enterprise": 300.0 } }` p, err := LoadFromJSON([]byte(jsonData)) require.NoError(t, err, "Should load JSON successfully") assert.Equal(t, []int{1, 2}, p.Time.Years, "Years should be loaded from JSON") assert.Equal(t, 100, p.Adoption.TotalOrgs.GetYear(1), "Total orgs should be loaded from JSON") assert.Equal(t, 0.5, p.Adoption.PayingShare.GetYear(1), "Paying share should be loaded from JSON") assert.Equal(t, 30.0, p.Pricing.Basic, "Basic price should be loaded from JSON") } // Test LoadFromYAML function func TestLoadFromYAML(t *testing.T) { yamlData := ` time: years: [1, 2] adoption: total_orgs: "1": 150 "2": 250 paying_share: "1": 0.4 "2": 0.5 pricing: basic: 40.0 business: 130.0 enterprise: 350.0 ` p, err := LoadFromYAML([]byte(yamlData)) require.NoError(t, err, "Should load YAML successfully") assert.Equal(t, []int{1, 2}, p.Time.Years, "Years should be loaded from YAML") assert.Equal(t, 150, p.Adoption.TotalOrgs.GetYear(1), "Total orgs should be loaded from YAML") assert.Equal(t, 0.4, p.Adoption.PayingShare.GetYear(1), "Paying share should be loaded from YAML") assert.Equal(t, 40.0, p.Pricing.Basic, "Basic price should be loaded from YAML") } // Test ErrInvalidParams error formatting func TestErrInvalidParams_Error(t *testing.T) { err := ErrInvalidParams{ Field: "test.field", Message: "test message", } errorMsg := err.Error() assert.Contains(t, errorMsg, "invalid params", "Should contain error prefix") assert.Contains(t, errorMsg, "test.field", "Should contain field name") assert.Contains(t, errorMsg, "test message", "Should contain message") } // Test yearKey function func TestYearKey(t *testing.T) { assert.Equal(t, "1", yearKey(1), "Year 1 should convert to string '1'") assert.Equal(t, "42", yearKey(42), "Year 42 should convert to string '42'") assert.Equal(t, "0", yearKey(0), "Year 0 should convert to string '0'") } // Test GetYear methods with missing keys to ensure 100% coverage func TestGetYear_Methods_MissingKeys(t *testing.T) { // Test YearlyInt.GetYear with missing key yi := YearlyInt{"1": 100, "2": 200} assert.Equal(t, 100, yi.GetYear(1), "Should return value for existing key") assert.Equal(t, 200, yi.GetYear(2), "Should return value for existing key") assert.Equal(t, 0, yi.GetYear(3), "Should return 0 for missing key") // Test YearlyFloat.GetYear with missing key yf := YearlyFloat{"1": 100.5, "2": 200.5} assert.Equal(t, 100.5, yf.GetYear(1), "Should return value for existing key") assert.Equal(t, 200.5, yf.GetYear(2), "Should return value for existing key") assert.Equal(t, 0.0, yf.GetYear(3), "Should return 0.0 for missing key") // Test YearlyTierMix.GetYear with missing key ytm := YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.5, 0.3, 0.2}} tier1 := ytm.GetYear(1) assert.Equal(t, 0.6, tier1.Basic, "Should return value for existing key") assert.Equal(t, 0.3, tier1.Business, "Should return value for existing key") assert.Equal(t, 0.1, tier1.Enterprise, "Should return value for existing key") tier2 := ytm.GetYear(2) assert.Equal(t, 0.5, tier2.Basic, "Should return value for existing key") assert.Equal(t, 0.3, tier2.Business, "Should return value for existing key") assert.Equal(t, 0.2, tier2.Enterprise, "Should return value for existing key") tier3 := ytm.GetYear(3) assert.Equal(t, TierMix{}, tier3, "Should return empty TierMix for missing key") } // Test Params.Validate method thoroughly func TestParams_Validate(t *testing.T) { tests := []struct { name string params *Params expectError bool errorField string }{ { name: "valid params", params: createValidParams(), expectError: false, }, { name: "no years", params: &Params{ Time: TimeParams{Years: []int{}}, }, expectError: true, errorField: "time.years", }, { name: "missing total orgs for year 1", params: &Params{ Time: TimeParams{Years: []int{1, 2, 3}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{"2": 2000, "3": 5000}, // Missing year 1 PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3}, }, }, expectError: true, errorField: "adoption.total_orgs", }, { name: "missing paying share for year 2", params: &Params{ Time: TimeParams{Years: []int{1, 2, 3}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000}, PayingShare: YearlyFloat{"1": 0.3, "3": 0.3}, // Missing year 2 }, Pricing: PricingParams{ Basic: 35.0, Business: 120.0, Enterprise: 400.0, TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, }, Transactions: TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600}, IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40}, ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000}, ServiceCommission: 0.15, GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000}, GroupCommission: 0.04, }, Municipal: MunicipalParams{ Cities: YearlyInt{"1": 1, "2": 2, "3": 4}, AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000}, DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000}, }, ImplServices: ImplementationParams{ MatchesPerOrg: 0.5, PaidShare: 0.25, AvgFee: 5000.0, }, Impact: ImpactParams{ HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000}, GridFactor: 0.3, HXEff: 0.9, Utilization: 0.7, WaterPerOrg: 25000.0, WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30}, WastePerOrg: 100.0, WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35}, }, Costs: CostParams{ Engineers: YearlyInt{"1": 8, "2": 12, "3": 15}, EngineerSalary: 100000.0, Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000}, MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000}, Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000}, }, Market: MarketParams{ TAM: 500000000000.0, AddressableDigital: 3000000000.0, PilotCityEconomicBenefit: 4000000.0, ScalabilityPotential: 400000000.0, EUIndustrialFacilities: 2100000, EnergyWastePotential: 0.45, ResourceCostReduction: 0.25, ViableExchangeRate: 0.15, PlatformCaptureRate: 0.50, SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000}, }, }, expectError: true, errorField: "adoption.paying_share", }, { name: "missing pricing tier mix for year 1", params: &Params{ Time: TimeParams{Years: []int{1, 2, 3}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000}, PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3}, }, Pricing: PricingParams{ Basic: 35.0, Business: 120.0, Enterprise: 400.0, TierMix: YearlyTierMix{"2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, // Missing year 1 }, Transactions: TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600}, IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40}, ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000}, ServiceCommission: 0.15, GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000}, GroupCommission: 0.04, }, Municipal: MunicipalParams{ Cities: YearlyInt{"1": 1, "2": 2, "3": 4}, AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000}, DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000}, }, ImplServices: ImplementationParams{ MatchesPerOrg: 0.5, PaidShare: 0.25, AvgFee: 5000.0, }, Impact: ImpactParams{ HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000}, GridFactor: 0.3, HXEff: 0.9, Utilization: 0.7, WaterPerOrg: 25000.0, WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30}, WastePerOrg: 100.0, WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35}, }, Costs: CostParams{ Engineers: YearlyInt{"1": 8, "2": 12, "3": 15}, EngineerSalary: 100000.0, Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000}, MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000}, Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000}, }, Market: MarketParams{ TAM: 500000000000.0, AddressableDigital: 3000000000.0, PilotCityEconomicBenefit: 4000000.0, ScalabilityPotential: 400000000.0, EUIndustrialFacilities: 2100000, EnergyWastePotential: 0.45, ResourceCostReduction: 0.25, ViableExchangeRate: 0.15, PlatformCaptureRate: 0.50, SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000}, }, }, expectError: true, errorField: "pricing.tier_mix", }, { name: "missing transaction intros for year 1", params: &Params{ Time: TimeParams{Years: []int{1, 2, 3}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000}, PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3}, }, Pricing: PricingParams{ Basic: 35.0, Business: 120.0, Enterprise: 400.0, TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, }, Transactions: TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: YearlyInt{"2": 400, "3": 600}, // Missing year 1 IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40}, ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000}, ServiceCommission: 0.15, GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000}, GroupCommission: 0.04, }, Impact: ImpactParams{ HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000}, GridFactor: 0.3, HXEff: 0.9, Utilization: 0.7, WaterPerOrg: 25000.0, WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30}, WastePerOrg: 100.0, WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35}, }, Costs: CostParams{ Engineers: YearlyInt{"1": 8, "2": 12, "3": 15}, EngineerSalary: 100000.0, Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000}, MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000}, Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000}, }, Market: MarketParams{ TAM: 500000000000.0, AddressableDigital: 3000000000.0, PilotCityEconomicBenefit: 4000000.0, ScalabilityPotential: 400000000.0, EUIndustrialFacilities: 2100000, EnergyWastePotential: 0.45, ResourceCostReduction: 0.25, ViableExchangeRate: 0.15, PlatformCaptureRate: 0.50, SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000}, }, }, expectError: true, errorField: "transactions.intros_per_year", }, { name: "missing municipal cities for year 2", params: &Params{ Time: TimeParams{Years: []int{1, 2, 3}}, Adoption: AdoptionParams{ TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000}, PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3}, }, Pricing: PricingParams{ Basic: 35.0, Business: 120.0, Enterprise: 400.0, TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, }, Transactions: TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600}, IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40}, ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000}, ServiceCommission: 0.15, GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000}, GroupCommission: 0.04, }, Municipal: MunicipalParams{ Cities: YearlyInt{"1": 1, "3": 4}, // Missing year 2 AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000}, DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000}, }, ImplServices: ImplementationParams{ MatchesPerOrg: 0.5, PaidShare: 0.25, AvgFee: 5000.0, }, Impact: ImpactParams{ HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000}, GridFactor: 0.3, HXEff: 0.9, Utilization: 0.7, WaterPerOrg: 25000.0, WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30}, WastePerOrg: 100.0, WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35}, }, Costs: CostParams{ Engineers: YearlyInt{"1": 8, "2": 12, "3": 15}, EngineerSalary: 100000.0, Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000}, MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000}, Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000}, }, Market: MarketParams{ TAM: 500000000000.0, AddressableDigital: 3000000000.0, PilotCityEconomicBenefit: 4000000.0, ScalabilityPotential: 400000000.0, EUIndustrialFacilities: 2100000, EnergyWastePotential: 0.45, ResourceCostReduction: 0.25, ViableExchangeRate: 0.15, PlatformCaptureRate: 0.50, SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000}, }, }, expectError: true, errorField: "municipal.cities", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.params.Validate() if tt.expectError { assert.Error(t, err, "Should return validation error") assert.IsType(t, ErrInvalidParams{}, err, "Should be ErrInvalidParams") errInvalid, ok := err.(ErrInvalidParams) if !ok { t.Fatalf("expected ErrInvalidParams, got %T", err) } assert.Contains(t, errInvalid.Field, tt.errorField, "Error field should match") } else { assert.NoError(t, err, "Should not return validation error") } }) } }