"""Behaviour of the glazing Recommendation Generator: detecting single-glazed windows and emitting one planning-picked "Windows" Recommendation (ADR-0022). Unrestricted dwellings get `double_glazing`; a planning-protected dwelling gets `secondary_glazing` instead — the measure is hard-picked by the Property's `PlanningRestrictions`, not offered as competing Options. All single-glazed windows are upgraded together in one overlay; the overlay writes each window's lodged U-value and g-value (not just `glazing_type`) because the calculator consumes those per-window values directly. Detection + pricing only, no scores (ADR-0016). The before/after cascade pins live in `test_elmhurst_cascade_pins`. """ import copy from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.modelling.generators.glazing_recommendation import recommend_glazing from domain.modelling.product import Product from domain.modelling.recommendation import Recommendation from domain.modelling.simulation import WindowOverlay from repositories.product.product_repository import ProductRepository from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import ( build_epc, ) # SAP10.2 Table U2 code 1 = single glazing (the generator's trigger). _SINGLE_GLAZED = 1 class _StubProducts(ProductRepository): """In-memory ProductRepository returning a fixed per-window glazing price. `unit_cost_per_m2` carries the catalogue row's fully-loaded total, reused as a flat average price per window (ADR-0022).""" def get(self, measure_type: str) -> Product: return Product( measure_type=measure_type, unit_cost_per_m2=600.0, contingency_rate=0.2, id=42, ) def _with_single_glazed(epc: EpcPropertyData, *indices: int) -> EpcPropertyData: """Return a copy of `epc` with the windows at `indices` set single-glazed.""" clone: EpcPropertyData = copy.deepcopy(epc) for index in indices: clone.sap_windows[index].glazing_type = _SINGLE_GLAZED return clone def test_single_glazed_dwelling_yields_a_double_glazing_recommendation() -> None: # Arrange — 000490 is all double-glazed; make windows 0 and 2 single-glazed. baseline: EpcPropertyData = _with_single_glazed(build_epc(), 0, 2) # Act recommendation: Recommendation | None = recommend_glazing( baseline, _StubProducts() ) # Assert — one double-glazing Option whose overlay rewrites each single- # glazed window to the pinned double target (gt=5, U=1.40, g=0.72). assert recommendation is not None assert recommendation.surface == "Windows" assert len(recommendation.options) == 1 option = recommendation.options[0] assert option.measure_type == "double_glazing" assert dict(option.overlay.windows) == { 0: WindowOverlay(glazing_type=5, u_value=1.40, solar_transmittance=0.72), 2: WindowOverlay(glazing_type=5, u_value=1.40, solar_transmittance=0.72), } def test_fully_glazed_dwelling_yields_no_recommendation() -> None: # Arrange — 000490 is entirely double-glazed (gt=2); nothing to upgrade. baseline: EpcPropertyData = build_epc() # Act recommendation: Recommendation | None = recommend_glazing( baseline, _StubProducts() ) # Assert assert recommendation is None def test_recommendation_prices_a_flat_average_per_single_glazed_window() -> None: # Arrange — three single-glazed windows at £600 each (a flat per-window # average reused from `unit_cost_per_m2`, ADR-0022). baseline: EpcPropertyData = _with_single_glazed(build_epc(), 0, 2, 4) # Act recommendation: Recommendation | None = recommend_glazing( baseline, _StubProducts() ) # Assert assert recommendation is not None cost = recommendation.options[0].cost assert cost is not None assert cost.total == 3 * 600.0 assert cost.contingency_rate == 0.2 assert recommendation.options[0].material_id == 42 def test_planning_protection_picks_secondary_glazing_over_double() -> None: # Arrange — a conservation area blocks external work, so the external units # can't be replaced; an internal secondary pane is the picked Measure. baseline: EpcPropertyData = _with_single_glazed(build_epc(), 0, 2) restrictions = PlanningRestrictions(in_conservation_area=True) # Act recommendation: Recommendation | None = recommend_glazing( baseline, _StubProducts(), restrictions ) # Assert — one secondary-glazing Option; each single window upgraded to the # pinned secondary target (gt=11, U=2.90, g unchanged at 0.85). assert recommendation is not None option = recommendation.options[0] assert option.measure_type == "secondary_glazing" assert dict(option.overlay.windows) == { 0: WindowOverlay(glazing_type=11, u_value=2.90, solar_transmittance=0.85), 2: WindowOverlay(glazing_type=11, u_value=2.90, solar_transmittance=0.85), }