import json
import os
from datetime import date

import pytest

from backend.documents_parser.extractor import PasHubRdSapSiteNotesExtractor
from datatypes.epc.surveys.pashub_rdsap_site_notes import (
    BuildingConstruction,
    BuildingMeasurements,
    Conservatories,
    CustomerResponse,
    ExtensionConstruction,
    ExtensionMeasurements,
    ExtensionRoofSpace,
    FloorConstruction,
    FloorMeasurement,
    General,
    HeatingAndHotWater,
    InspectionMetadata,
    MainBuildingConstruction,
    MainBuildingMeasurements,
    MainHeating,
    Renewables,
    RoomCountElements,
    RoofSpace,
    RoofSpaceDetail,
    SecondaryHeating,
    Shower,
    SurveyAddendum,
    Ventilation,
    WaterHeating,
    WaterUse,
)

FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")


def load_text_fixture() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_1_text.json")) as f:
        return json.load(f)


def load_text_fixture_2() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_2_text.json")) as f:
        return json.load(f)


def load_text_fixture_3() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_3_text.json")) as f:
        return json.load(f)


def load_text_fixture_4() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_4_text.json")) as f:
        return json.load(f)


def load_text_fixture_5() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_5_text.json")) as f:
        return json.load(f)


def load_text_fixture_6() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_6_text.json")) as f:
        return json.load(f)


def load_text_fixture_7() -> list[str]:
    with open(os.path.join(FIXTURES, "pashub_site_notes_7_text.json")) as f:
        return json.load(f)


class TestInspectionMetadata:
    def test_full_inspection_metadata(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_inspection_metadata()
        assert result == InspectionMetadata(
            inspection_surveyor="Benjamin Burke",
            email_address="ben@mbsolutionsgroup.co.uk",
            report_reference="6EA2A86D-94CE-4792-8D49-AB495C744EDD",
            created_on="2025-11-10",
            date_of_inspection=date(2025, 9, 25),
            property_address="40, Abbey Place, Crewe, Cheshire, CW1 4JR",
            property_photo=True,
        )


class TestGeneral:
    @pytest.fixture
    def general(self) -> General:
        return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_general()

    def test_epc_checked_before_assessment(self, general: General) -> None:
        assert general.epc_checked_before_assessment is True

    def test_epc_exists_at_point_of_assessment(self, general: General) -> None:
        assert general.epc_exists_at_point_of_assessment is False

    def test_inspection_date(self, general: General) -> None:
        assert general.inspection_date == date(2025, 9, 25)

    def test_transaction_type(self, general: General) -> None:
        assert general.transaction_type == "Grant-Scheme (ECO, RHI, etc.)"

    def test_tenure(self, general: General) -> None:
        assert general.tenure == "Rented Social"

    def test_property_type(self, general: General) -> None:
        assert general.property_type == "House"

    def test_detachment_type(self, general: General) -> None:
        assert general.detachment_type == "Mid-terrace"

    def test_number_of_storeys(self, general: General) -> None:
        assert general.number_of_storeys == 2

    def test_number_of_extensions(self, general: General) -> None:
        assert general.number_of_extensions == 1

    def test_electricity_smart_meter(self, general: General) -> None:
        assert general.electricity_smart_meter is True

    def test_mains_gas_available(self, general: General) -> None:
        assert general.mains_gas_available is True

    def test_measurements_location(self, general: General) -> None:
        assert general.measurements_location == "Internal"

    def test_full_general(self, general: General) -> None:
        assert general == General(
            epc_checked_before_assessment=True,
            epc_exists_at_point_of_assessment=False,
            inspection_date=date(2025, 9, 25),
            transaction_type="Grant-Scheme (ECO, RHI, etc.)",
            tenure="Rented Social",
            property_type="House",
            detachment_type="Mid-terrace",
            number_of_storeys=2,
            terrain_type="Suburban",
            number_of_extensions=1,
            electricity_smart_meter=True,
            electric_meter_type="Single",
            dwelling_export_capable=True,
            mains_gas_available=True,
            gas_smart_meter=True,
            gas_meter_accessible=True,
            measurements_location="Internal",
        )


class TestGeneralNoExtensions:
    @pytest.fixture
    def general(self) -> General:
        return PasHubRdSapSiteNotesExtractor(load_text_fixture_2()).extract_general()

    def test_number_of_extensions_when_no_extensions(self, general: General) -> None:
        assert general.number_of_extensions == 0


class TestBuildingConstruction:
    @pytest.fixture
    def construction(self) -> BuildingConstruction:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_building_construction()

    def test_main_building_wall_u_value_known_is_false(
        self, construction: BuildingConstruction
    ) -> None:
        assert construction.main_building.wall_u_value_known is False

    def test_main_building_wall_thickness_mm(
        self, construction: BuildingConstruction
    ) -> None:
        assert construction.main_building.wall_thickness_mm == 310

    def test_main_building_filled_cavity_indicators_present(
        self, construction: BuildingConstruction
    ) -> None:
        assert (
            construction.main_building.filled_cavity_indicators
            == "evidence of cavity fill drill holes"
        )

    def test_extension_filled_cavity_indicators_absent(
        self, construction: BuildingConstruction
    ) -> None:
        assert construction.extensions is not None
        assert construction.extensions[0].filled_cavity_indicators is None

    def test_one_extension(self, construction: BuildingConstruction) -> None:
        assert construction.extensions is not None
        assert len(construction.extensions) == 1

    def test_extension_id(self, construction: BuildingConstruction) -> None:
        assert construction.extensions is not None
        assert construction.extensions[0].id == 1

    def test_full_building_construction(
        self, construction: BuildingConstruction
    ) -> None:
        assert construction == BuildingConstruction(
            main_building=MainBuildingConstruction(
                age_range="1950-1966",
                age_indicators="local knowledge, enquiries of owner",
                walls_construction_type="Cavity",
                cavity_construction_indicators="wall thickness over 270 mm",
                walls_insulation_type="Filled Cavity",
                filled_cavity_indicators="evidence of cavity fill drill holes",
                thermal_conductivity_of_wall_insulation="Unknown",
                wall_u_value_known=False,
                wall_thickness_mm=310,
                party_wall_construction_type="Cavity Masonry, Filled",
            ),
            floor=FloorConstruction(
                floor_type="Ground Floor",
                floor_construction="Solid",
                floor_insulation_type="As Built",
                floor_u_value_known=False,
            ),
            extensions=[
                ExtensionConstruction(
                    id=1,
                    age_range="2003-2006",
                    age_indicators="local knowledge, enquiries of owner",
                    walls_construction_type="Cavity",
                    cavity_construction_indicators="wall thickness over 270 mm",
                    walls_insulation_type="As built",
                    thermal_conductivity_of_wall_insulation="Unknown",
                    wall_u_value_known=False,
                    wall_thickness_mm=310,
                    party_wall_construction_type="Cavity Masonry, Filled",
                    filled_cavity_indicators=None,
                )
            ],
        )


class TestBuildingMeasurements:
    @pytest.fixture
    def measurements(self) -> BuildingMeasurements:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_building_measurements()

    def test_main_building_has_two_floors(
        self, measurements: BuildingMeasurements
    ) -> None:
        assert len(measurements.main_building.floors) == 2

    def test_main_building_floor_area(
        self, measurements: BuildingMeasurements
    ) -> None:
        assert measurements.main_building.floors[0].area_m2 == 35.68

    def test_integer_token_parses_to_float(
        self, measurements: BuildingMeasurements
    ) -> None:
        # "11" in the PDF (no decimal) should parse to 11.0
        assert measurements.main_building.floors[1].heat_loss_perimeter_m == 11.0

    def test_extension_measurements_present(
        self, measurements: BuildingMeasurements
    ) -> None:
        assert measurements.extensions is not None
        assert len(measurements.extensions) == 1

    def test_extension_id(self, measurements: BuildingMeasurements) -> None:
        assert measurements.extensions is not None
        assert measurements.extensions[0].id == 1

    def test_full_building_measurements(
        self, measurements: BuildingMeasurements
    ) -> None:
        assert measurements == BuildingMeasurements(
            main_building=MainBuildingMeasurements(
                floors=[
                    FloorMeasurement(
                        name="Floor 1",
                        area_m2=35.68,
                        height_m=2.19,
                        heat_loss_perimeter_m=13.44,
                        pwl_m=10.62,
                    ),
                    FloorMeasurement(
                        name="Floor 0",
                        area_m2=35.68,
                        height_m=2.17,
                        heat_loss_perimeter_m=11.0,
                        pwl_m=10.62,
                    ),
                ]
            ),
            extensions=[
                ExtensionMeasurements(
                    id=1,
                    floors=[
                        FloorMeasurement(
                            name="Floor 0",
                            area_m2=3.8,
                            height_m=2.0,
                            heat_loss_perimeter_m=5.7,
                            pwl_m=0.0,
                        )
                    ],
                )
            ],
        )


class TestRoofSpace:
    @pytest.fixture
    def roof_space(self) -> RoofSpace:
        return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_roof_space()

    def test_main_building_insulation_thickness_mm(
        self, roof_space: RoofSpace
    ) -> None:
        assert roof_space.main_building.insulation_thickness_mm == 100

    def test_main_building_insulation_thickness_string_absent(
        self, roof_space: RoofSpace
    ) -> None:
        assert roof_space.main_building.insulation_thickness is None

    def test_main_building_rooms_in_roof(self, roof_space: RoofSpace) -> None:
        assert roof_space.main_building.rooms_in_roof is False

    def test_main_building_roof_u_value_known(self, roof_space: RoofSpace) -> None:
        assert roof_space.main_building.roof_u_value_known is False

    def test_extension_uses_string_thickness(self, roof_space: RoofSpace) -> None:
        assert roof_space.extensions is not None
        assert roof_space.extensions[0].insulation_thickness == "As built"
        assert roof_space.extensions[0].insulation_thickness_mm is None

    def test_full_roof_space(self, roof_space: RoofSpace) -> None:
        assert roof_space == RoofSpace(
            main_building=RoofSpaceDetail(
                construction_type="Pitched roof (Slates or tiles), Access to loft",
                insulation_at="Joists",
                roof_u_value_known=False,
                cavity_wall_construction_indicators="cavity visible in roof space",
                rooms_in_roof=False,
                insulation_thickness_mm=100,
                insulation_thickness=None,
            ),
            extensions=[
                ExtensionRoofSpace(
                    id=1,
                    construction_type="Pitched roof, Sloping ceiling",
                    insulation_at="Sloping ceiling insulation",
                    roof_u_value_known=False,
                    cavity_wall_construction_indicators="No indicator of construction visible",
                    rooms_in_roof=False,
                    insulation_thickness_mm=None,
                    insulation_thickness="As built",
                )
            ],
        )


class TestWindows:
    @pytest.fixture
    def windows(self) -> list:
        return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_windows()

    def test_window_count(self, windows: list) -> None:
        assert len(windows) == 8

    def test_ids_are_sequential(self, windows: list) -> None:
        assert [w.id for w in windows] == list(range(1, 9))

    def test_first_window_location(self, windows: list) -> None:
        assert windows[0].location == "Main Building"

    def test_extension_window_location(self, windows: list) -> None:
        assert windows[3].location == "Extension 1"

    def test_height_parses_to_float(self, windows: list) -> None:
        assert windows[0].height_m == 1.2

    def test_draught_proofed_true(self, windows: list) -> None:
        assert windows[0].draught_proofed is True

    def test_permanent_shutters_false(self, windows: list) -> None:
        assert windows[0].permanent_shutters is False

    def test_first_window_full(self, windows: list) -> None:
        from datatypes.epc.surveys.pashub_rdsap_site_notes import Window
        assert windows[0] == Window(
            id=1,
            location="Main Building",
            wall_type="External wall",
            glazing_type="Double glazing, Unknown install date",
            window_type="Window",
            frame_type="Wooden or PVC",
            glazing_gap="16 mm or more",
            draught_proofed=True,
            permanent_shutters=False,
            height_m=1.2,
            width_m=2.3,
            orientation="North West",
        )


class TestWaterHeatingCylinderThickness:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_2()
        ).extract_heating_and_hot_water()

    @pytest.fixture
    def hhw_no_cylinder(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_heating_and_hot_water()

    def test_cylinder_insulation_thickness_mm(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.insulation_thickness_mm == 38

    def test_cylinder_insulation_thickness_mm_absent(self, hhw_no_cylinder: HeatingAndHotWater) -> None:
        assert hhw_no_cylinder.water_heating.insulation_thickness_mm is None

    def test_cylinder_size(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.cylinder_size == "Normal (90-130 litres)"


class TestImmersionType:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_3()
        ).extract_heating_and_hot_water()

    def test_immersion_type(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.immersion_type == "Dual"


class TestCylinderThermostat:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_3()
        ).extract_heating_and_hot_water()

    def test_has_thermostat_true(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.has_thermostat is True


class TestSecondaryHeating:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_2()
        ).extract_heating_and_hot_water()

    @pytest.fixture
    def hhw_no_secondary(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_heating_and_hot_water()

    def test_secondary_system(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.secondary_heating.secondary_system == "Open fire in grate"

    def test_secondary_system_absent(self, hhw_no_secondary: HeatingAndHotWater) -> None:
        assert hhw_no_secondary.secondary_heating.secondary_system is None


class TestHeatingAndHotWater:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_heating_and_hot_water()

    def test_product_id_parses_to_int(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.main_heating.product_id == 16839

    def test_summer_efficiency_parses_to_float(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.main_heating.summer_efficiency == 0.0

    def test_condensing_true(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.main_heating.condensing is True

    def test_fghrs_false(self, hhw: HeatingAndHotWater) -> None:
        # multi-line label
        assert hhw.main_heating.flue_gas_heat_recovery_system is False

    def test_secondary_fuel(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.secondary_heating.secondary_fuel == "No Secondary Heating"

    def test_water_heating_no_cylinder(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.cylinder_size == "No Cylinder"
        assert hhw.water_heating.insulation_type is None
        assert hhw.water_heating.has_thermostat is None

    def test_full_heating_and_hot_water(self, hhw: HeatingAndHotWater) -> None:
        assert hhw == HeatingAndHotWater(
            main_heating=MainHeating(
                selection_method="PCDF Search",
                system_type="Boiler with radiators or underfloor heating",
                product_id=16839,
                manufacturer="Vaillant",
                model="ecoTEC pro 28",
                orig_manufacturer="Vaillant",
                fuel="Mains gas",
                summer_efficiency=0.0,
                type="Combi",
                condensing=True,
                year="2005 - 2015",
                mount="Wall",
                open_flue="Room-sealed",
                fan_assist=True,
                status="Normal status for an actual product",
                central_heating_pump_age="Unknown",
                controls="Programmer, room thermostat and TRVs",
                flue_gas_heat_recovery_system=False,
                weather_compensator=False,
                emitter="Radiators",
                emitter_temperature="Unknown",
            ),
            secondary_heating=SecondaryHeating(
                secondary_fuel="No Secondary Heating",
            ),
            water_heating=WaterHeating(
                type="Regular",
                system="From main heating 1",
                cylinder_size="No Cylinder",
                cylinder_measured_heat_loss=None,
                insulation_type=None,
                insulation_thickness_mm=None,
                has_thermostat=None,
            ),
        )


class TestVentilation:
    @pytest.fixture
    def ventilation(self) -> Ventilation:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_ventilation()

    def test_ventilation_type(self, ventilation: Ventilation) -> None:
        assert ventilation.ventilation_type == "Mechanical Extract - Decentralised"

    def test_number_of_open_flues(self, ventilation: Ventilation) -> None:
        assert ventilation.number_of_open_flues == 0

    def test_ventilation_in_pcdf_database(self, ventilation: Ventilation) -> None:
        assert ventilation.ventilation_in_pcdf_database is False

    def test_full_ventilation(self, ventilation: Ventilation) -> None:
        assert ventilation == Ventilation(
            ventilation_type="Mechanical Extract - Decentralised",
            has_fixed_air_conditioning=False,
            number_of_open_flues=0,
            number_of_closed_flues=0,
            number_of_boiler_flues=0,
            number_of_other_flues=0,
            number_of_extract_fans=0,
            number_of_passive_vents=0,
            number_of_flueless_gas_fires=0,
            pressure_test="No test",
            draught_lobby=False,
            ventilation_in_pcdf_database=False,
        )


class TestConservatories:
    def test_full_conservatories(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_conservatories()
        assert result == Conservatories(has_conservatory=False)


class TestRenewables:
    def test_number_of_pv_batteries_none_string_becomes_zero(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_renewables()
        assert result.number_of_pv_batteries == 0

    def test_full_renewables(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_renewables()
        assert result == Renewables(
            wind_turbines=False,
            solar_hot_water=False,
            photovoltaic_array=False,
            number_of_pv_batteries=0,
            hydro=False,
        )


class TestRenewablesPvConnection:
    @pytest.fixture
    def renewables(self) -> Renewables:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_3()
        ).extract_renewables()

    def test_pv_connection(self, renewables: Renewables) -> None:
        assert renewables.pv_connection == "Connected to dwellings electricity meter"

    def test_percent_roof_covered_pv(self, renewables: Renewables) -> None:
        assert renewables.percent_roof_covered_pv == 45


class TestRoomCountElements:
    @pytest.fixture
    def rce(self) -> RoomCountElements:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_room_count_elements()

    def test_habitable_rooms(self, rce: RoomCountElements) -> None:
        assert rce.number_of_habitable_rooms == 3

    def test_heated_rooms_null(self, rce: RoomCountElements) -> None:
        assert rce.number_of_heated_rooms is None

    def test_full_room_count_elements(self, rce: RoomCountElements) -> None:
        assert rce == RoomCountElements(
            number_of_habitable_rooms=3,
            any_unheated_rooms=False,
            number_of_heated_rooms=None,
            number_of_external_doors=2,
            number_of_insulated_external_doors=0,
            number_of_draughtproofed_external_doors=2,
            number_of_open_chimneys=0,
            number_of_blocked_chimneys=0,
            number_of_fixed_incandescent_bulbs=4,
            exact_led_cfl_count_known=True,
            number_of_fixed_led_bulbs=0,
            number_of_fixed_cfl_bulbs=1,
            waste_water_heat_recovery="None",
        )


class TestWaterUse:
    def test_full_water_use(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_water_use()
        assert result == WaterUse(
            number_of_baths=1,
            number_of_special_features=0,
            showers=[Shower(id=1, outlet_type="Non-Electric Shower")],
        )


class TestCustomerResponse:
    def test_full_customer_response(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(
            load_text_fixture()
        ).extract_customer_response()
        assert result == CustomerResponse(
            customer_present=True,
            willing_to_answer_satisfaction_survey=False,
        )


class TestExtract:
    def test_full_extract(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract()
        assert result.inspection_metadata.inspection_surveyor == "Benjamin Burke"
        assert result.general.inspection_date == date(2025, 9, 25)
        assert result.building_construction.main_building.wall_thickness_mm == 310
        assert result.building_measurements.main_building.floors[0].area_m2 == 35.68
        assert result.roof_space.main_building.insulation_thickness_mm == 100
        assert len(result.windows) == 8
        assert result.heating_and_hot_water.main_heating.product_id == 16839
        assert result.ventilation.ventilation_type == "Mechanical Extract - Decentralised"
        assert result.conservatories.has_conservatory is False
        assert result.renewables.number_of_pv_batteries == 0
        assert result.room_count_elements.number_of_habitable_rooms == 3
        assert result.water_use.number_of_baths == 1
        assert result.customer_response.customer_present is True
        assert result.addendum.addendum == "None"


class TestSurveyAddendum:
    def test_hard_to_treat_flags(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_addendum()
        assert result.hard_to_treat_cavity_access_issues is False
        assert result.hard_to_treat_cavity_high_exposure is False
        assert result.hard_to_treat_cavity_narrow_cavities is False

    def test_full_addendum(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_addendum()
        assert result == SurveyAddendum(
            addendum="None",
            related_party_disclosure="No related party",
            hard_to_treat_cavity_access_issues=False,
            hard_to_treat_cavity_high_exposure=False,
            hard_to_treat_cavity_narrow_cavities=False,
        )


# --- fixture 4: heat pump, factory-fitted cylinder, blocked loft ---


class TestCylinderInsulationType:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_4()
        ).extract_heating_and_hot_water()

    def test_insulation_type_extracted(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.insulation_type == "Factory fitted"

    def test_insulation_thickness_mm(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.insulation_thickness_mm == 50

    def test_cylinder_size(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.water_heating.cylinder_size == "Medium (131-170 litres)"


class TestHeatPumpFuelExtraction:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_4()
        ).extract_heating_and_hot_water()

    def test_fuel_raw_value(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.main_heating.fuel == "Electricity, any tariff"

    def test_system_type(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.main_heating.system_type == "Heat pump with radiators or underfloor heating"


class TestRoofSpaceUnknownInsulation:
    @pytest.fixture
    def roof_space(self) -> RoofSpace:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_4()
        ).extract_roof_space()

    def test_insulation_at_unknown(self, roof_space: RoofSpace) -> None:
        assert roof_space.main_building.insulation_at == "Unknown"

    def test_insulation_thickness_mm_none(self, roof_space: RoofSpace) -> None:
        assert roof_space.main_building.insulation_thickness_mm is None

    def test_insulation_thickness_str_none(self, roof_space: RoofSpace) -> None:
        assert roof_space.main_building.insulation_thickness is None


class TestCflBulbCount:
    @pytest.fixture
    def rce(self) -> RoomCountElements:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_5()
        ).extract_room_count_elements()

    def test_cfl_count(self, rce: RoomCountElements) -> None:
        assert rce.number_of_fixed_cfl_bulbs == 2

    def test_led_count(self, rce: RoomCountElements) -> None:
        assert rce.number_of_fixed_led_bulbs == 7

    def test_incandescent_count(self, rce: RoomCountElements) -> None:
        assert rce.number_of_fixed_incandescent_bulbs == 1


class TestSecondaryHeatingPanel:
    @pytest.fixture
    def hhw(self) -> HeatingAndHotWater:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_5()
        ).extract_heating_and_hot_water()

    def test_secondary_system(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.secondary_heating.secondary_system == "Panel, convector or radiant heaters"

    def test_secondary_fuel(self, hhw: HeatingAndHotWater) -> None:
        assert hhw.secondary_heating.secondary_fuel == "Electricity"


class TestElectricShowerExtraction:
    @pytest.fixture
    def wu(self) -> WaterUse:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_5()
        ).extract_water_use()

    def test_shower_outlet_type(self, wu: WaterUse) -> None:
        assert wu.showers[0].outlet_type == "Electric Shower"


# --- fixture 7: maisonette, 2 extensions, no property photo ---


class TestExtractNoPropertyPhoto:
    def test_address_extracted_when_no_property_photo(self) -> None:
        result = PasHubRdSapSiteNotesExtractor(load_text_fixture_7()).extract()
        assert result.inspection_metadata.property_address == "Flat 3, 29 Watcombe Circus, NOTTINGHAM, NG5 2DU"
        assert result.inspection_metadata.property_photo is False
        assert result.general.property_type == "Maisonette"
        assert result.general.number_of_extensions == 2


class TestWallThicknessExtraction:
    def _extractor(self) -> PasHubRdSapSiteNotesExtractor:
        return PasHubRdSapSiteNotesExtractor([])

    def test_numeric_value_returns_int(self) -> None:
        assert self._extractor()._wall_thickness_in(["Wall thickness:", "310 mm"]) == 310

    def test_unmeasurable_returns_none(self) -> None:
        assert self._extractor()._wall_thickness_in(["Wall thickness:", "Unmeasurable"]) is None

    def test_unmeasurable_lowercase_returns_none(self) -> None:
        assert self._extractor()._wall_thickness_in(["Wall thickness:", "unmeasurable"]) is None

    def test_unmeasurable_uppercase_returns_none(self) -> None:
        assert self._extractor()._wall_thickness_in(["Wall thickness:", "UNMEASURABLE"]) is None

    def test_missing_field_returns_none(self) -> None:
        assert self._extractor()._wall_thickness_in([]) is None


class TestSolidMasonryPartyWall:
    @pytest.fixture
    def bc(self) -> BuildingConstruction:
        return PasHubRdSapSiteNotesExtractor(
            load_text_fixture_6()
        ).extract_building_construction()

    def test_party_wall_construction_type(self, bc: BuildingConstruction) -> None:
        assert (
            bc.main_building.party_wall_construction_type
            == "Solid Masonry, Timber Frame, or System Built"
        )
