import pytest
import numpy as np
from unittest.mock import Mock, MagicMock
from recommendations.WallRecommendations import WallRecommendations
from backend.Property import Property
from recommendations.recommendation_utils import is_diminishing_returns
from recommendations.tests.test_data.materials import materials
from etl.epc.Record import EPCRecord


class TestWallRecommendations:

    @pytest.fixture
    def mock_wall_rec_instance(self):
        # Creating a mock instance of WallRecommendations with the necessary attributes
        property_mock = Mock()
        epc_record = EPCRecord()
        epc_record.construction_age_band = "1950"
        epc_record.county = "Derbyshire"
        epc_record.lodgement_date = "2000-01-01"
        property_mock.epc_record = epc_record
        property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"}

        mock_wall_rec_instance = WallRecommendations(
            property_mock, materials=materials
        )
        return mock_wall_rec_instance

    def test_init(self):
        p = Mock(
            id=1,
            insulation_wall_area=100,
            walls={
                'original_description': 'Average thermal transmittance 0.16 W/m-¦K', 'thermal_transmittance': 0.16,
                'thermal_transmittance_unit': 'w/m-¦k', 'is_cavity_wall': False, 'is_filled_cavity': False,
                'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False,
                'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': False, 'is_assumed': False,
                'is_sandstone_or_limestone': False, 'insulation_thickness': None, 'external_insulation': False,
                'internal_insulation': False
            },
            already_installed=[],
            data={"county": "Greater London Authority"}
        )

        obj = WallRecommendations(
            property_instance=p,
            materials=materials
        )
        assert obj
        assert obj.property

    def test_uvalue_0_16(self):
        """
        This tests the wall description Average thermal transmittance 0.16 W/m-¦K
        The important data for this recommendation is:
        - u value of 0.16
        - property built in 2014
        Since epc built after 1990 are typically built with insulation and this property
        already has really good insulation, we do NOT recommend any measures for this property
        """

        p = Mock(
            id=1,
            insulation_wall_area=100,
            year_built=2014,
            in_conservation_area=None,
            restricted_measure=False,
            walls={
                'original_description': 'Average thermal transmittance 0.16 W/m-¦K',
                'clean_description': 'Average thermal transmittance 0.16 W/m-¦K',
                'thermal_transmittance': 0.16,
                'thermal_transmittance_unit': 'w/m-¦k', 'is_cavity_wall': False, 'is_filled_cavity': False,
                'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False,
                'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': False, 'is_assumed': False,
                'is_sandstone_or_limestone': False, 'insulation_thickness': None, 'external_insulation': False,
                'internal_insulation': False
            },
            already_installed=[],
            data={"county": "Greater London Authority", 'transaction-type': 'new dwelling'}
        )

        recommender = WallRecommendations(
            property_instance=p,
            materials=materials
        )
        assert recommender.property.walls["original_description"] == "Average thermal transmittance 0.16 W/m-¦K"
        recommender.recommend()
        # This should be empty
        assert recommender.recommendations == []

    def test_solid_brick_no_insulation(self):
        """
        This tests a property with a wall description of Solid brick, as built, no insulation (assumed)
        The property was built in 1930, right on the threshold for when cavity walls were introduced
        However, we're told this property is solid brik so we assume no cavity.
        We're also told that it has no insulation, so we will recommend internal/external wall insulation

        This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
        """

        epc_record = EPCRecord()
        epc_record.county = "Greater London Authority"
        epc_record.property_type = "Flat"
        epc_record.walls_energy_eff = "Very Poor"

        p = Mock(
            id=2,
            year_built=1930,
            insulation_wall_area=100,
            age_band="A",
            restricted_measures=False,
            already_installed=[],
            construction_age_band="England and Wales: 1930-1949",
            in_conservation_area="not_in_conservation_area",
            non_invasive_recommendations=[],
            walls={
                'original_description': 'Solid brick, as built, no insulation (assumed)',
                "clean_description": "Solid brick, as built, no insulation",
                'thermal_transmittance': None,
                'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False,
                'is_solid_brick': True, 'is_system_built': False, 'is_timber_frame': False,
                'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_assumed': True,
                'is_sandstone_or_limestone': False, 'insulation_thickness': 'none', 'external_insulation': False,
                'internal_insulation': False, 'is_park_home': False
            },
            epc_record=epc_record,
        )

        recommender = WallRecommendations(
            property_instance=p,
            materials=materials
        )
        assert not recommender.ewi_valid()

        recommender.recommend(phase=0)
        # This should result in some recommendations, all of which should be internal insulation
        assert recommender.recommendations

        rec_types = {part["type"] for rec in recommender.recommendations for part in rec["parts"]}
        assert rec_types == {"internal_wall_insulation"}

        # Check the recommendations provide a u value below the minimum
        assert all(
            rec["new_u_value"] < WallRecommendations.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE for rec in
            recommender.recommendations
        )

    def test_solid_brick_insulation(self):
        """
        This tests a property with a wall description of Solid brick, as built, insulation (assumed)
        The property was built in 1991, after cavity walls were introduced
        However, we're told this property is solid brik so we assume no cavity.
        We're also told that it has some insulation already

        Since the walls are already insulated, we don't recommend further measures

        This property is not in a conservation area, however it's a flat so we don't recommend external wall insulation
        """

        epc_record = EPCRecord()
        epc_record.county = "Greater London Authority"
        epc_record.property_type = "Flat"

        p = Mock(
            id=3,
            year_built=1991,
            restricted_measures=False,
            insulation_wall_area=100,
            already_installed=[],
            in_conservation_area="not_in_conservation_area",
            walls={
                'original_description': 'Solid brick, as built, insulated (assumed)',
                'clean_description': 'Solid brick, as built, insulated',
                'thermal_transmittance': None,
                'thermal_transmittance_unit': None, 'is_cavity_wall': False, 'is_filled_cavity': False,
                'is_solid_brick': True, 'is_system_built': False, 'is_timber_frame': False,
                'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'is_assumed': True,
                'is_sandstone_or_limestone': False, 'insulation_thickness': 'average', 'external_insulation': False,
                'internal_insulation': False
            },
            epc_record=epc_record
        )

        recommender = WallRecommendations(
            property_instance=p,
            materials=materials
        )

        assert recommender.property.walls["original_description"] == "Solid brick, as built, insulated (assumed)"
        assert not recommender.ewi_valid()
        assert recommender.estimated_u_value is None

        recommender.recommend()

        assert not recommender.recommendations
        assert not recommender.estimated_u_value

    def test_is_diminishing_returns_no_recommendations(self):
        # We have no recommendations but the new u value is below the diminishing returns threshold
        # however since there are no recommendaions yet, we should include it and say
        # it is NOT diminishing

        new_u_value = 0.24
        assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE

        assert not is_diminishing_returns([], new_u_value, None, WallRecommendations.DIMINISHING_RETURNS_U_VALUE)

    def test_is_diminishing_returns_diminishing(self):
        # We have a recommendation already and the new u value falls below the diminishing returns
        # threshold and is also lower than the lowest selected u value, so we should say this IS
        # diminishing returns

        new_u_value = 0.2
        lowest_selected_u_value = 0.23
        assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE

        assert is_diminishing_returns([Mock()], new_u_value, lowest_selected_u_value,
                                      WallRecommendations.DIMINISHING_RETURNS_U_VALUE)

    def test_is_diminishing_returns_is_diminishing(self):
        # We have a recommendation already and the new u value falls below the diminishing returns
        # threshold and it's lower than the lowest selected u value, so we should say this IS DIMINISHIN

        new_u_value = 0.24
        lowest_selected_u_value = 0.26

        assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE

        assert is_diminishing_returns([Mock()], new_u_value, lowest_selected_u_value,
                                      WallRecommendations.DIMINISHING_RETURNS_U_VALUE)

    def test_is_diminishing_returns_not_diminishing(self):
        # We have a recommendation already and the new u value falls below the diminishing returns
        # threshold however it's higher than the lowest selected u value, so we should say this is NOT
        # diminishing returns

        new_u_value = 0.24
        lowest_selected_u_value = 0.22

        assert new_u_value < WallRecommendations.DIMINISHING_RETURNS_U_VALUE

        assert not is_diminishing_returns([Mock()], new_u_value, lowest_selected_u_value,
                                          WallRecommendations.DIMINISHING_RETURNS_U_VALUE)

    def test_is_diminishing_returns_error_in_recommendations(self, mock_wall_rec_instance):
        # Testing case where there's an error in recommendations
        mock_wall_rec_instance.recommendations = []
        with pytest.raises(ValueError):
            is_diminishing_returns([], 0.2, 0.24, 0.25)


class TestWallRecommendationsBase:
    @pytest.fixture
    def property_mock(self):
        property_mock = MagicMock(spec=Property)
        property_mock.full_sap_epc = {"lodgement-date": "1999-12-31"}
        property_mock.in_conservation_area = "not_in_conservation_area"
        property_mock.restricted_measures = False
        property_mock.insulation_wall_area = 100
        epc_record = EPCRecord(county="Derbyshire", property_type="House")
        property_mock.epc_record = epc_record
        property_mock.walls = {
            "is_cob": False,
            "is_sandstone_or_limestone": False,
            "is_cavity_wall": False
        }
        return property_mock

    @pytest.fixture
    def wall_recommendations_instance(self, property_mock):
        wall_recommendations_instance = WallRecommendations(
            property_mock, materials=materials
        )
        return wall_recommendations_instance

    def test_ewi_valid_in_conservation_area(self, wall_recommendations_instance):
        wall_recommendations_instance.property.in_conservation_area = "in_conversation_area"
        wall_recommendations_instance.property.restricted_measures = True
        assert wall_recommendations_instance.ewi_valid() is False

    def test_ewi_valid_is_flat(self, wall_recommendations_instance):
        wall_recommendations_instance.property.epc_record.property_type = "Flat"
        assert wall_recommendations_instance.ewi_valid() is False

    def test_ewi_valid_not_in_conservation_area_and_not_flat(self, wall_recommendations_instance):
        wall_recommendations_instance.property.in_conservation_area = "not_in_conversation_area"
        wall_recommendations_instance.property.restricted_measures = False
        # Set property_type on the EPCRecord directly
        wall_recommendations_instance.property.epc_record.property_type = "House"
        assert wall_recommendations_instance.ewi_valid() is True


class TestCavityWallRecommensations:

    def test_fill_empty_cavity(self):
        epc_record = EPCRecord(county="Derbyshire", walls_energy_eff="Very Poor", property_type="House")
        input_property = Property(id=1, postcode="F4k3", address="123 fake street", epc_record=epc_record)
        input_property.walls = {
            'original_description': 'Cavity wall, as built, no insulation (assumed)',
            'clean_description': 'Cavity wall, as built, no insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
            'is_as_built': True, 'is_cob': False, 'is_assumed': True,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property.age_band = "C"
        input_property.insulation_wall_area = 50
        input_property.construction_age_band = "England and Wales: 1930-1949"
        input_property.already_installed = []

        recommender = WallRecommendations(
            property_instance=input_property,
            materials=materials
        )

        assert not recommender.recommendations

        recommender.recommend()

        assert recommender.recommendations
        assert recommender.estimated_u_value == 1.5
        assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.35)
        assert np.isclose(recommender.recommendations[0]["total"], 925)

    def test_fill_partial_filled_cavity(self):
        epc_record = EPCRecord(county="County Durham", walls_energy_eff="Poor", property_type="House")
        input_property = Property(id=1, postcode="F4k3", address="123 fake street", epc_record=epc_record)
        input_property.walls = {
            'original_description': 'Cavity wall, as built, partial insulation (assumed)',
            'clean_description': 'Cavity wall, as built, partial insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': True, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
            'is_as_built': True, 'is_cob': False, 'is_assumed': True,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'below average', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property.age_band = "C"
        input_property.insulation_wall_area = 50
        input_property.construction_age_band = "England and Wales: 1930-1949"
        input_property.already_installed = []

        recommender = WallRecommendations(
            property_instance=input_property,
            materials=materials
        )

        assert not recommender.recommendations

        recommender.recommend()

        assert recommender.recommendations
        assert recommender.estimated_u_value == 1.3
        assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.41)
        assert np.isclose(recommender.recommendations[0]["total"], 925.0)

    def test_system_built_wall(self):
        epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
                               walls_energy_eff="Very Poor")
        input_property2 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record)
        input_property2.walls = {
            'original_description': 'System built, as built, no insulation (assumed)',
            'clean_description': 'System built, as built, no insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': True, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
            'is_as_built': True, 'is_cob': False, 'is_assumed': True,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property2.age_band = "F"
        input_property2.insulation_wall_area = 120
        input_property2.restricted_measures = False
        input_property2.construction_age_band = "England and Wales: 1976-1982"
        input_property2.already_installed = []

        assert input_property2.walls["is_system_built"]

        recommender2 = WallRecommendations(
            property_instance=input_property2,
            materials=materials
        )

        assert not recommender2.recommendations

        recommender2.recommend()

        assert recommender2.recommendations
        assert len(recommender2.recommendations) == 2
        assert recommender2.estimated_u_value == 1
        assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.21)
        assert np.isclose(recommender2.recommendations[0]["total"], 35802.0)

    def test_timber_frame_wall(self):
        epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
                               walls_energy_eff="Very Poor")
        input_property3 = Property(id=1, postcode="F4k3 3", address="323 fake street", epc_record=epc_record)
        input_property3.walls = {
            'original_description': 'Timber frame, as built, no insulation (assumed)',
            'clean_description': 'Timber frame, as built, no insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': False, 'is_timber_frame': True, 'is_granite_or_whinstone': False,
            'is_as_built': True, 'is_cob': False, 'is_assumed': True,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property3.age_band = "F"
        input_property3.insulation_wall_area = 120
        input_property3.restricted_measures = False
        input_property3.construction_age_band = "England and Wales: 1976-1982"
        input_property3.already_installed = []

        recommender3 = WallRecommendations(
            property_instance=input_property3,
            materials=materials
        )

        assert not recommender3.recommendations

        recommender3.recommend()

        assert recommender3.recommendations
        assert recommender3.estimated_u_value == 0.45
        assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.17)
        assert np.isclose(recommender3.recommendations[0]["total"], 35802.0)

    def test_granite_or_whinstone_wall(self):
        epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
                               walls_energy_eff="Very Poor")
        input_property4 = Property(id=1, postcode="F4k3 4", address="423 fake street", epc_record=epc_record)
        input_property4.walls = {
            'original_description': 'Granite or whinstone, as built, no insulation (assumed)',
            'clean_description': 'Granite or whinstone, as built, no insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': True,
            'is_as_built': True, 'is_cob': False, 'is_assumed': True,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property4.age_band = "F"
        input_property4.insulation_wall_area = 120
        input_property4.restricted_measures = False
        input_property4.construction_age_band = "England and Wales: 1976-1982"
        input_property4.already_installed = []

        recommender4 = WallRecommendations(
            property_instance=input_property4,
            materials=materials
        )

        assert not recommender4.recommendations

        recommender4.recommend()

        assert not recommender4.recommendations

    def test_cob_wall(self):
        epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
                               walls_energy_eff="Very Poor")
        input_property5 = Property(id=1, postcode="F4k3 5", address="523 fake street", epc_record=epc_record)
        input_property5.walls = {
            'original_description': 'Cob, as built, no insulation (assumed)',
            'clean_description': 'Cob, as built, no insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
            'is_as_built': True, 'is_cob': True, 'is_assumed': True,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property5.age_band = "F"
        input_property5.insulation_wall_area = 120
        input_property5.restricted_measures = False
        input_property5.construction_age_band = "England and Wales: 1976-1982"
        input_property5.already_installed = []

        recommender5 = WallRecommendations(
            property_instance=input_property5,
            materials=materials
        )

        assert not recommender5.recommendations

        recommender5.recommend()

        assert not recommender5.recommendations

    def test_sandstone_or_limestone_wall(self):
        epc_record = EPCRecord(property_type="House", county="Derbyshire", built_form="Detached",
                               walls_energy_eff="Very Poor")
        input_property6 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record)
        input_property6.walls = {
            'original_description': 'Sandstone or limestone, as built, no insulation (assumed)',
            'clean_description': 'Sandstone or limestone, as built, no insulation',
            'thermal_transmittance': None, 'thermal_transmittance_unit': None,
            'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False,
            'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False,
            'is_as_built': True, 'is_cob': False, 'is_assumed': True,
            'is_sandstone_or_limestone': True, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property6.age_band = "F"
        input_property6.insulation_wall_area = 120
        input_property6.restricted_measures = False
        input_property6.construction_age_band = "England and Wales: 1976-1982"
        input_property6.already_installed = []

        recommender6 = WallRecommendations(
            property_instance=input_property6,
            materials=materials
        )

        assert not recommender6.recommendations

        recommender6.recommend()

        assert not recommender6.recommendations
