import json
import os
from typing import Any, Dict

import pytest

from datatypes.epc.schema import (
    RdSapSchema17_0,
    RdSapSchema17_1,
    RdSapSchema18_0,
    RdSapSchema19_0,
    RdSapSchema20_0_0,
    RdSapSchema21_0_0,
    RdSapSchema21_0_1,
)
from datatypes.epc.schema.common import CostAmount, DescriptionV1, Measurement
from datatypes.epc.schema.tests.helpers import from_dict

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


def load(filename: str) -> Dict[str, Any]:
    with open(os.path.join(FIXTURES, filename)) as f:
        return json.load(f)  # type: ignore[no-any-return]


class TestRdSapSchema17_0:

    @pytest.fixture
    def epc(self) -> RdSapSchema17_0:
        return from_dict(RdSapSchema17_0, load("17_0.json"))

    def test_schema_type(self, epc: RdSapSchema17_0) -> None:
        assert epc.schema_type == "RdSAP-Schema-17.0"

    def test_uprn(self, epc: RdSapSchema17_0) -> None:
        assert epc.uprn == 12457

    def test_description_is_localised_object(self, epc: RdSapSchema17_0) -> None:
        assert isinstance(epc.roofs[0].description, DescriptionV1)
        assert epc.roofs[0].description.value == "(another dwelling above)"
        assert epc.roofs[0].description.language == "1"

    def test_dwelling_type_is_localised_object(self, epc: RdSapSchema17_0) -> None:
        assert isinstance(epc.dwelling_type, DescriptionV1)
        assert epc.dwelling_type.value == "Mid-floor flat"

    def test_costs_are_cost_amount_objects(self, epc: RdSapSchema17_0) -> None:
        assert isinstance(epc.heating_cost_current, CostAmount)
        assert epc.heating_cost_current.value == 214
        assert epc.heating_cost_current.currency == "GBP"

    def test_suggested_improvement_saving_is_cost_amount(self, epc: RdSapSchema17_0) -> None:
        assert isinstance(epc.suggested_improvements[0].typical_saving, CostAmount)
        assert epc.suggested_improvements[0].typical_saving.value == 158

    def test_flat_details_populated(self, epc: RdSapSchema17_0) -> None:
        assert epc.sap_flat_details is not None
        assert epc.sap_flat_details.level == 2

    def test_floor_dimensions_use_measurement(self, epc: RdSapSchema17_0) -> None:
        dim = epc.sap_building_parts[0].sap_floor_dimensions[0]
        assert isinstance(dim.room_height, Measurement)
        assert dim.room_height.value == 2.4
        assert dim.room_height.quantity == "metres"

    def test_alternative_improvements_present(self, epc: RdSapSchema17_0) -> None:
        assert epc.alternative_improvements is not None
        assert len(epc.alternative_improvements) == 1
        assert epc.alternative_improvements[0].improvement_type == "J2"

    def test_renewable_heat_incentive(self, epc: RdSapSchema17_0) -> None:
        assert epc.renewable_heat_incentive.water_heating == 4818
        assert epc.renewable_heat_incentive.space_heating_existing_dwelling == 2415
        assert epc.renewable_heat_incentive.impact_of_loft_insulation is None

    def test_glazing_gap_is_string(self, epc: RdSapSchema17_0) -> None:
        assert epc.glazing_gap == "16+"

    def test_lzc_energy_sources(self, epc: RdSapSchema17_0) -> None:
        assert epc.lzc_energy_sources == [11]


class TestRdSapSchema17_1:

    @pytest.fixture
    def epc(self) -> RdSapSchema17_1:
        return from_dict(RdSapSchema17_1, load("17_1.json"))

    def test_schema_type(self, epc: RdSapSchema17_1) -> None:
        assert epc.schema_type == "RdSAP-Schema-17.1"

    def test_description_is_localised_object(self, epc: RdSapSchema17_1) -> None:
        assert isinstance(epc.roofs[0].description, DescriptionV1)
        assert epc.roofs[0].description.value == "Pitched, 100 mm loft insulation"

    def test_address_line_2(self, epc: RdSapSchema17_1) -> None:
        assert epc.address_line_2 == "Lower Moria"

    def test_cylinder_thermostat(self, epc: RdSapSchema17_1) -> None:
        assert epc.sap_heating.cylinder_thermostat == "Y"

    def test_cylinder_insulation_thickness(self, epc: RdSapSchema17_1) -> None:
        assert epc.sap_heating.cylinder_insulation_thickness == 12

    def test_boiler_flue_type_in_heating_detail(self, epc: RdSapSchema17_1) -> None:
        detail = epc.sap_heating.main_heating_details[0]
        assert detail.boiler_flue_type == 1
        assert detail.main_heating_index_number == 9049

    def test_extension_party_wall_length_is_int(self, epc: RdSapSchema17_1) -> None:
        # Extension floor dimension has party_wall_length as a plain int (0)
        extension = epc.sap_building_parts[1]
        assert extension.identifier == "Extension 1"
        dim = extension.sap_floor_dimensions[0]
        assert dim.party_wall_length == 0

    def test_extension_roof_insulation_thickness_is_int(self, epc: RdSapSchema17_1) -> None:
        extension = epc.sap_building_parts[1]
        assert extension.roof_insulation_thickness == 0

    def test_renewable_heat_incentive_has_loft_impact(self, epc: RdSapSchema17_1) -> None:
        assert epc.renewable_heat_incentive.impact_of_loft_insulation == -565

    def test_multiple_roofs(self, epc: RdSapSchema17_1) -> None:
        assert len(epc.roofs) == 2


class TestRdSapSchema18_0:

    @pytest.fixture
    def epc(self) -> RdSapSchema18_0:
        return from_dict(RdSapSchema18_0, load("18_0.json"))

    def test_schema_type(self, epc: RdSapSchema18_0) -> None:
        assert epc.schema_type == "RdSAP-Schema-18.0"

    def test_glazing_gap_is_integer(self, epc: RdSapSchema18_0) -> None:
        assert epc.glazing_gap == 12
        assert isinstance(epc.glazing_gap, int)

    def test_room_in_roof_present(self, epc: RdSapSchema18_0) -> None:
        main = epc.sap_building_parts[0]
        assert main.sap_room_in_roof is not None
        assert main.sap_room_in_roof.insulation == "AB"
        assert main.sap_room_in_roof.construction_age_band == "G"

    def test_room_in_roof_floor_area_is_measurement(self, epc: RdSapSchema18_0) -> None:
        room_in_roof = epc.sap_building_parts[0].sap_room_in_roof
        assert room_in_roof is not None
        assert isinstance(room_in_roof.floor_area, Measurement)
        assert room_in_roof.floor_area.value == 9.0
        assert room_in_roof.floor_area.quantity == "square metres"

    def test_flat_roof_insulation_on_extension(self, epc: RdSapSchema18_0) -> None:
        extension = epc.sap_building_parts[1]
        assert extension.flat_roof_insulation_thickness == "NI"

    def test_description_is_localised_object(self, epc: RdSapSchema18_0) -> None:
        assert isinstance(epc.walls[0].description, DescriptionV1)

    def test_three_roofs(self, epc: RdSapSchema18_0) -> None:
        assert len(epc.roofs) == 3

    def test_renewable_heat_incentive_has_solid_wall_impact(self, epc: RdSapSchema18_0) -> None:
        assert epc.renewable_heat_incentive.impact_of_solid_wall_insulation == -1864


class TestRdSapSchema19_0:

    @pytest.fixture
    def epc(self) -> RdSapSchema19_0:
        return from_dict(RdSapSchema19_0, load("19_0.json"))

    def test_schema_type(self, epc: RdSapSchema19_0) -> None:
        assert epc.schema_type == "RdSAP-Schema-19.0"

    def test_windows_transmission_details(self, epc: RdSapSchema19_0) -> None:
        wtd = epc.windows_transmission_details
        assert wtd.u_value == 3.1
        assert wtd.data_source == 2
        assert wtd.solar_transmittance == 0.76

    def test_description_is_localised_object(self, epc: RdSapSchema19_0) -> None:
        assert isinstance(epc.roofs[0].description, DescriptionV1)
        assert epc.roofs[0].description.value == "Pitched, 150 mm loft insulation"

    def test_glazing_gap_absent(self, epc: RdSapSchema19_0) -> None:
        assert epc.glazing_gap is None

    def test_secondary_heating_type(self, epc: RdSapSchema19_0) -> None:
        assert epc.sap_heating.secondary_heating_type == 631

    def test_multiple_lzc_sources(self, epc: RdSapSchema19_0) -> None:
        assert epc.lzc_energy_sources == [11, 9]

    def test_floor_insulation_thickness_on_building_part(self, epc: RdSapSchema19_0) -> None:
        assert epc.sap_building_parts[0].floor_insulation_thickness == "NI"


class TestRdSapSchema20_0_0:

    @pytest.fixture
    def epc(self) -> RdSapSchema20_0_0:
        return from_dict(RdSapSchema20_0_0, load("20_0_0.json"))

    def test_schema_type(self, epc: RdSapSchema20_0_0) -> None:
        assert epc.schema_type == "RdSAP-Schema-20.0.0"

    def test_description_is_plain_string(self, epc: RdSapSchema20_0_0) -> None:
        assert isinstance(epc.roofs[0].description, str)
        assert epc.roofs[0].description == "Pitched, 25 mm loft insulation"

    def test_dwelling_type_is_plain_string(self, epc: RdSapSchema20_0_0) -> None:
        assert isinstance(epc.dwelling_type, str)
        assert epc.dwelling_type == "Mid-terrace house"

    def test_costs_are_floats(self, epc: RdSapSchema20_0_0) -> None:
        assert isinstance(epc.heating_cost_current, float)
        assert epc.heating_cost_current == pytest.approx(365.98)

    def test_suggested_improvement_saving_is_numeric(self, epc: RdSapSchema20_0_0) -> None:
        assert epc.suggested_improvements[0].typical_saving == 360

    def test_indicative_cost_can_be_int(self, epc: RdSapSchema20_0_0) -> None:
        # Second improvement has indicative_cost as a plain integer
        assert epc.suggested_improvements[1].indicative_cost == 2000

    def test_improvement_details_with_texts(self, epc: RdSapSchema20_0_0) -> None:
        # Third improvement uses improvement_texts instead of improvement_number
        third = epc.suggested_improvements[2]
        assert third.improvement_details.improvement_number is None
        assert third.improvement_details.improvement_texts is not None
        assert third.improvement_details.improvement_texts.improvement_summary == "An improvement summary"

    def test_sap_windows_present(self, epc: RdSapSchema20_0_0) -> None:
        assert len(epc.sap_windows) == 2
        assert epc.sap_windows[0].window_area == pytest.approx(200.1)
        assert epc.sap_windows[0].glazing_type == 1

    def test_addendum(self, epc: RdSapSchema20_0_0) -> None:
        assert epc.addendum is not None
        assert epc.addendum.stone_walls == "true"
        assert epc.addendum.addendum_numbers == [1, 8]

    def test_flat_details_has_storey_count(self, epc: RdSapSchema20_0_0) -> None:
        assert epc.sap_flat_details is not None
        assert epc.sap_flat_details.storey_count == 3
        assert epc.sap_flat_details.unheated_corridor_length == 10

    def test_room_in_roof_floor_area_is_plain_number(self, epc: RdSapSchema20_0_0) -> None:
        room_in_roof = epc.sap_building_parts[0].sap_room_in_roof
        assert room_in_roof is not None
        assert room_in_roof.floor_area == 100
        assert not isinstance(room_in_roof.floor_area, Measurement)

    def test_renewable_heat_incentive_has_cavity_impact(self, epc: RdSapSchema20_0_0) -> None:
        assert epc.renewable_heat_incentive.impact_of_cavity_insulation == -122

    def test_multiple_glazed_proportion_nr(self, epc: RdSapSchema20_0_0) -> None:
        assert epc.multiple_glazed_proportion_nr == "NR"


class TestRdSapSchema21_0_0:

    @pytest.fixture
    def epc(self) -> RdSapSchema21_0_0:
        return from_dict(RdSapSchema21_0_0, load("21_0_0.json"))

    def test_schema_type(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.schema_type == "RdSAP-Schema-21.0.0"

    def test_description_is_plain_string(self, epc: RdSapSchema21_0_0) -> None:
        assert isinstance(epc.roofs[0].description, str)

    def test_pressure_test(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.pressure_test == 6

    def test_wet_rooms_count(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.wet_rooms_count == 0

    def test_instantaneous_wwhrs_uses_index_numbers(self, epc: RdSapSchema21_0_0) -> None:
        wwhrs = epc.sap_heating.instantaneous_wwhrs
        assert wwhrs.wwhrs_index_number1 == 1
        assert wwhrs.wwhrs_index_number2 == 2

    def test_shower_outlets(self, epc: RdSapSchema21_0_0) -> None:
        outlets = epc.sap_heating.shower_outlets
        assert outlets is not None
        assert outlets.shower_outlet.shower_wwhrs == 1
        assert outlets.shower_outlet.shower_outlet_type == 1

    def test_boiler_ignition_type(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.sap_heating.main_heating_details[0].boiler_ignition_type == 1

    def test_smart_meters(self, epc: RdSapSchema21_0_0) -> None:
        src = epc.sap_energy_source
        assert src.electricity_smart_meter_present == "true"
        assert src.gas_smart_meter_present == "false"

    def test_pv_batteries(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.sap_energy_source.pv_batteries is not None
        assert epc.sap_energy_source.pv_batteries.pv_battery.battery_capacity == 5

    def test_alternative_walls(self, epc: RdSapSchema21_0_0) -> None:
        part = epc.sap_building_parts[0]
        assert part.sap_alternative_wall_1 is not None
        assert part.sap_alternative_wall_1.wall_area == pytest.approx(10.4)
        assert part.sap_alternative_wall_2 is not None
        assert part.sap_alternative_wall_2.wall_area == pytest.approx(10.8)

    def test_detailed_sap_windows(self, epc: RdSapSchema21_0_0) -> None:
        win = epc.sap_windows[0]
        assert win.glazing_gap == 6
        assert win.window_width == pytest.approx(1.2)
        assert win.window_transmission_details.u_value == pytest.approx(1.0)

    def test_mechanical_vent_fields(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.mechanical_vent_duct_type == 3
        assert epc.mechanical_ventilation_index_number == 12

    def test_lighting_bulb_counts(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.led_fixed_lighting_bulbs_count == 10
        assert epc.cfl_fixed_lighting_bulbs_count == 5
        assert epc.incandescent_fixed_lighting_bulbs_count == 5

    def test_open_chimneys_count(self, epc: RdSapSchema21_0_0) -> None:
        assert epc.open_chimneys_count == 1

    def test_room_in_roof_has_no_insulation_field(self, epc: RdSapSchema21_0_0) -> None:
        room_in_roof = epc.sap_building_parts[0].sap_room_in_roof
        assert room_in_roof is not None
        assert room_in_roof.construction_age_band == "B"
        assert not hasattr(room_in_roof, "insulation")


class TestRdSapSchema21_0_1:

    @pytest.fixture
    def epc(self) -> RdSapSchema21_0_1:
        return from_dict(RdSapSchema21_0_1, load("21_0_1.json"))

    def test_schema_type(self, epc: RdSapSchema21_0_1) -> None:
        assert epc.schema_type == "RdSAP-Schema-21.0.1"

    def test_description_reverts_to_localised_object(self, epc: RdSapSchema21_0_1) -> None:
        # Descriptions on energy elements revert to DescriptionV1 in 21.0.1
        assert isinstance(epc.roofs[0].description, DescriptionV1)
        assert epc.roofs[0].description.value == "Pitched, 25 mm loft insulation"

    def test_main_heating_description_is_localised_object(self, epc: RdSapSchema21_0_1) -> None:
        assert isinstance(epc.main_heating[0].description, DescriptionV1)
        assert epc.main_heating[0].description.value == "Boiler and radiators, anthracite"

    def test_dwelling_type_remains_plain_string(self, epc: RdSapSchema21_0_1) -> None:
        # dwelling_type does NOT revert — stays as plain str in 21.0.1
        assert isinstance(epc.dwelling_type, str)
        assert epc.dwelling_type == "Mid-terrace house"

    def test_unheated_corridor_length_is_measurement(self, epc: RdSapSchema21_0_1) -> None:
        # Changed from plain int (21.0.0) to Measurement object in 21.0.1
        assert epc.sap_flat_details is not None
        corridor_length = epc.sap_flat_details.unheated_corridor_length
        assert isinstance(corridor_length, Measurement)
        assert corridor_length.value == 10
        assert corridor_length.quantity == "metres"

    def test_completion_date(self, epc: RdSapSchema21_0_1) -> None:
        assert epc.completion_date == "2025-04-04"

    def test_addendum_numbers(self, epc: RdSapSchema21_0_1) -> None:
        assert epc.addendum is not None
        assert epc.addendum.addendum_numbers == [1, 13]

    def test_pv_battery_capacity(self, epc: RdSapSchema21_0_1) -> None:
        assert epc.sap_energy_source.pv_batteries is not None
        assert epc.sap_energy_source.pv_batteries.pv_battery.battery_capacity == 5

    def test_incandescent_bulb_count(self, epc: RdSapSchema21_0_1) -> None:
        assert epc.incandescent_fixed_lighting_bulbs_count == 0
