"""The one-property console entrypoint for interactive sense-checking.""" from __future__ import annotations import dataclasses import pytest from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.modelling.contingencies import contingency_rate from harness.console import DEFAULT_CATALOGUE, run_modelling, run_one from repositories.product.product_json_repository import ProductJsonRepository from tests.domain.modelling._elmhurst_recommendation import ( parse_recommendation_summary, ) from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import ( build_epc as _build_uninsulated_cavity_and_floor_epc, ) # Every Measure Type the fabric generators can emit; the harness catalogue # must price all of them or an offline run raises mid-pipeline. _GENERATOR_MEASURE_TYPES = ( "cavity_wall_insulation", "external_wall_insulation", "internal_wall_insulation", "loft_insulation", "sloping_ceiling_insulation", "flat_roof_insulation", "suspended_floor_insulation", "solid_floor_insulation", "mechanical_ventilation", "double_glazing", "secondary_glazing", ) def _uninsulated_lodged_epc() -> EpcPropertyData: epc = _build_uninsulated_cavity_and_floor_epc() return dataclasses.replace( epc, energy_rating_current=57, current_energy_efficiency_band=Epc.D, co2_emissions_current=3.0, energy_consumption_current=300, ) def test_run_one_returns_a_plan_and_prints_the_table( capsys: pytest.CaptureFixture[str], ) -> None: # Arrange epc: EpcPropertyData = _uninsulated_lodged_epc() # Act — run one property end-to-end with no database, against the default # sample catalogue. plan = run_one(epc, goal_band="C") # Assert — a multi-measure Plan came back, and its sense-check table printed. assert len(plan.measures) >= 1 printed: str = capsys.readouterr().out assert "Plan SAP" in printed assert "cavity_wall_insulation" in printed def test_run_modelling_inspects_a_plan_without_baseline_or_lodged_performance() -> None: # Arrange — the RAW 000490 fixture, with NO lodged recorded-performance, so # the Baseline stage could not run on it. Modelling re-scores the EPC itself. epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc() # Act — Modelling only, no Ingestion / Baseline, no database. plan = run_modelling(epc, goal_band="C", print_table=False) # Assert — a multi-measure Plan came straight out of Modelling. assert len(plan.measures) >= 1 def test_run_modelling_recommends_solid_wall_insulation_for_solid_brick() -> None: # Arrange — an uninsulated solid-brick dwelling (cert 001431 before), # which has no cavity to fill, so any wall measure must be solid-wall. epc: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) # Act — Modelling only, no database. plan = run_modelling(epc, goal_band="C", print_table=False) # Assert — the solid-wall generator is wired into the candidate pool, so a # solid-wall Option reaches the optimised package. measure_types = {measure.measure_type for measure in plan.measures} assert measure_types & {"external_wall_insulation", "internal_wall_insulation"} def test_run_modelling_listed_building_yields_no_wall_insulation() -> None: # Arrange — the same uninsulated solid-brick dwelling that gets IWI when # unrestricted; listing it protects the fabric, blocking both EWI and IWI. epc: EpcPropertyData = parse_recommendation_summary( "solid_brick_ewi_001431_before.pdf" ) # Act — thread a listed-building restriction through to the generator. plan = run_modelling( epc, goal_band="C", print_table=False, planning_restrictions=PlanningRestrictions(is_listed=True), ) # Assert — no wall-insulation measure survives the restriction. measure_types = {measure.measure_type for measure in plan.measures} assert not ( measure_types & {"external_wall_insulation", "internal_wall_insulation"} ) def _single_glazed_epc() -> EpcPropertyData: """The cavity/floor dwelling with all windows single-glazed — the glazing generator's trigger, sized so the upgrade reaches the optimised package.""" epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc() for window in epc.sap_windows: window.glazing_type = 1 # SAP10.2 Table U2 code 1 = single. return epc def test_run_modelling_recommends_double_glazing_for_single_glazed_windows() -> None: # Arrange — a single-glazed dwelling; the glazing generator is wired into # the candidate pool. epc: EpcPropertyData = _single_glazed_epc() # Act — Modelling only, no database, unrestricted. plan = run_modelling(epc, goal_band="C", print_table=False) # Assert — double glazing reaches the optimised package. measure_types = {measure.measure_type for measure in plan.measures} assert "double_glazing" in measure_types def test_run_modelling_protected_dwelling_yields_secondary_glazing() -> None: # Arrange — the same single-glazed dwelling, listed (blocks external work). epc: EpcPropertyData = _single_glazed_epc() # Act — thread a listed-building restriction through to the generator. plan = run_modelling( epc, goal_band="C", print_table=False, planning_restrictions=PlanningRestrictions(is_listed=True), ) # Assert — the picked glazing Measure is secondary, never double. measure_types = {measure.measure_type for measure in plan.measures} assert "secondary_glazing" in measure_types assert "double_glazing" not in measure_types def test_sample_catalogue_prices_every_generator_measure_type() -> None: # Arrange — the default offline catalogue. products: ProductJsonRepository = ProductJsonRepository(DEFAULT_CATALOGUE) # Act / Assert — get() and contingency_rate() each raise on a missing # Measure Type, so an offline run over arbitrary EPCs never dies on a # missing catalogue or contingency entry. for measure_type in _GENERATOR_MEASURE_TYPES: products.get(measure_type) contingency_rate(measure_type) def test_run_one_threads_a_current_market_value_onto_the_plan() -> None: # Arrange epc: EpcPropertyData = _uninsulated_lodged_epc() # Act — supply a Property Valuation so the Plan can value the uplift. plan = run_one( epc, goal_band="C", current_market_value=250_000.0, print_table=False ) # Assert — the value reached the Plan, which derives its Valuation Uplift # from it (the £ amount is 0 here as 000490 stays within band D). assert plan.current_market_value == 250_000.0 assert plan.valuation.average_value is not None