from __future__ import annotations import pytest from domain.fuel_rates.fuel import Fuel from domain.billing.bill import BillSection, EnergyBreakdown from domain.sap10_calculator.calculator import SapResult def _sap_result( *, main_heating_fuel_kwh_per_yr: float = 0.0, main_heating_fuel_code: int | None = None, main_2_heating_fuel_kwh_per_yr: float = 0.0, main_2_heating_fuel_code: int | None = None, secondary_heating_fuel_kwh_per_yr: float = 0.0, secondary_heating_fuel_code: int | None = None, hot_water_kwh_per_yr: float = 0.0, hot_water_fuel_code: int | None = None, space_cooling_fuel_kwh_per_yr: float = 0.0, pumps_fans_kwh_per_yr: float = 0.0, lighting_kwh_per_yr: float = 0.0, appliances_kwh_per_yr: float = 0.0, cooking_kwh_per_yr: float = 0.0, pv_exported_kwh_per_yr: float = 0.0, ) -> SapResult: return SapResult( sap_score=72, sap_score_continuous=72.0, ecf=0.0, total_fuel_cost_gbp=0.0, co2_kg_per_yr=0.0, space_heating_kwh_per_yr=0.0, space_cooling_kwh_per_yr=0.0, fabric_energy_efficiency_kwh_per_m2_yr=0.0, main_heating_fuel_kwh_per_yr=main_heating_fuel_kwh_per_yr, main_2_heating_fuel_kwh_per_yr=main_2_heating_fuel_kwh_per_yr, secondary_heating_fuel_kwh_per_yr=secondary_heating_fuel_kwh_per_yr, space_cooling_fuel_kwh_per_yr=space_cooling_fuel_kwh_per_yr, hot_water_kwh_per_yr=hot_water_kwh_per_yr, pumps_fans_kwh_per_yr=pumps_fans_kwh_per_yr, lighting_kwh_per_yr=lighting_kwh_per_yr, appliances_kwh_per_yr=appliances_kwh_per_yr, cooking_kwh_per_yr=cooking_kwh_per_yr, main_heating_fuel_code=main_heating_fuel_code, main_2_heating_fuel_code=main_2_heating_fuel_code, secondary_heating_fuel_code=secondary_heating_fuel_code, hot_water_fuel_code=hot_water_fuel_code, pv_exported_kwh_per_yr=pv_exported_kwh_per_yr, primary_energy_kwh_per_yr=0.0, primary_energy_kwh_per_m2=0.0, monthly=(), intermediate={}, ) def test_each_positive_end_use_becomes_a_line_at_its_fuel() -> None: # Arrange — a gas-boiler home with an electric secondary heater: HEATING # carries two lines on different fuels; HW is gas; the rest are electricity. result = _sap_result( main_heating_fuel_kwh_per_yr=8000.0, main_heating_fuel_code=1, # mains gas secondary_heating_fuel_kwh_per_yr=300.0, secondary_heating_fuel_code=30, # electricity hot_water_kwh_per_yr=2500.0, hot_water_fuel_code=1, # mains gas lighting_kwh_per_yr=400.0, appliances_kwh_per_yr=1900.0, cooking_kwh_per_yr=300.0, ) # Act breakdown = EnergyBreakdown.from_sap_result(result) # Assert lines = {(line.section, line.fuel): line.kwh for line in breakdown.lines} assert lines == { (BillSection.HEATING, Fuel.MAINS_GAS): 8000.0, (BillSection.HEATING, Fuel.ELECTRICITY): 300.0, (BillSection.HOT_WATER, Fuel.MAINS_GAS): 2500.0, (BillSection.LIGHTING, Fuel.ELECTRICITY): 400.0, (BillSection.APPLIANCES, Fuel.ELECTRICITY): 1900.0, (BillSection.COOKING, Fuel.ELECTRICITY): 300.0, } def test_zero_kwh_end_uses_emit_no_line() -> None: # Arrange — only lighting has energy; everything else is zero. result = _sap_result(lighting_kwh_per_yr=350.0) # Act breakdown = EnergyBreakdown.from_sap_result(result) # Assert — exactly one line, no empty HEATING / HOT_WATER / COOLING entries. assert len(breakdown.lines) == 1 assert breakdown.lines[0].section == BillSection.LIGHTING def test_cooling_is_billed_as_electricity() -> None: # Arrange — a home with fixed cooling. result = _sap_result(space_cooling_fuel_kwh_per_yr=450.0) # Act breakdown = EnergyBreakdown.from_sap_result(result) # Assert assert len(breakdown.lines) == 1 line = breakdown.lines[0] assert (line.section, line.fuel, line.kwh) == ( BillSection.COOLING, Fuel.ELECTRICITY, 450.0, ) def test_pv_export_carries_to_exported_kwh() -> None: # Arrange result = _sap_result(lighting_kwh_per_yr=400.0, pv_exported_kwh_per_yr=1200.0) # Act breakdown = EnergyBreakdown.from_sap_result(result) # Assert assert breakdown.exported_kwh == 1200.0 def test_raw_api_fuel_code_is_normalized_to_its_billing_fuel() -> None: # Arrange — the calculator can carry a raw gov-API fuel code (26 = mains gas). result = _sap_result( main_heating_fuel_kwh_per_yr=9000.0, main_heating_fuel_code=26 ) # Act breakdown = EnergyBreakdown.from_sap_result(result) # Assert assert breakdown.lines[0].fuel == Fuel.MAINS_GAS def test_positive_heating_kwh_with_no_fuel_code_raises() -> None: # Arrange — energy with no resolvable fuel is a data gap, not a default. result = _sap_result( main_heating_fuel_kwh_per_yr=8000.0, main_heating_fuel_code=None ) # Act / Assert with pytest.raises(ValueError, match="no fuel code"): EnergyBreakdown.from_sap_result(result)