import os
import pytest
import pickle
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


# with open(
#     os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb"
# ) as f:
#     input_properties = pickle.load(f)


class TestWallRecommendations:

    @pytest.fixture
    def input_properties(self):
        with open(
            os.path.abspath(os.path.dirname(__file__)) + "/test_data/input_properties.pkl", "rb"
        ) as f:
            return pickle.load(f)

    @pytest.fixture
    def mock_wall_rec_instance(self):
        # Creating a mock instance of WallRecommendations with the necessary attributes
        property_mock = Mock()
        property_mock.full_sap_epc = {"lodgement-date": "2000-01-01"}  # or any date you want
        property_mock.data = {"construction-age-band": "1950",
                              "county": "Derbyshire"}  # or any other data that fits your tests

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

    def test_init(self, input_properties):
        input_properties[0].insulation_wall_area = 100

        obj = WallRecommendations(
            property_instance=input_properties[0],
            materials=materials
        )
        assert obj
        assert obj.property

    def test_uvalue_0_16(self, input_properties):
        """
        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
        """
        input_properties[0].year_built = 2014
        input_properties[0].in_conservation_area = None
        input_properties[0].restricted_measures = False
        input_properties[0].insulation_wall_area = 100

        recommender = WallRecommendations(
            property_instance=input_properties[0],
            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, input_properties):
        """
        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
        """
        input_properties[1].year_built = 1930
        input_properties[1].insulation_wall_area = 100
        input_properties[1].walls["clean_description"] = "Solid brick, as built, no insulation"
        input_properties[1].walls["is_sandstone_or_limestone"] = False
        input_properties[1].age_band = "A"
        input_properties[1].restricted_measures = False

        recommender = WallRecommendations(
            property_instance=input_properties[1],
            materials=materials
        )
        assert recommender.property.walls["original_description"] == "Solid brick, as built, no insulation (assumed)"
        assert not recommender.ewi_valid
        assert recommender.property.in_conservation_area == "not_in_conservation_area"
        assert recommender.property.data["property-type"] == "Flat"

        recommender.recommend()
        # 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, input_properties):
        """
        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
        """

        input_properties[6].year_built = 1991
        input_properties[6].restricted_measures = False
        input_properties[6].insulation_wall_area = 100

        recommender = WallRecommendations(
            property_instance=input_properties[6],
            materials=materials
        )

        assert recommender.property.walls["original_description"] == "Solid brick, as built, insulated (assumed)"
        assert not recommender.ewi_valid
        assert recommender.property.in_conservation_area == "not_in_conservation_area"
        assert recommender.property.data["property-type"] == "Flat"
        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
        property_mock.data = {"county": "Derbyshire"}
        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.data = {"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
        wall_recommendations_instance.property.data = {"property-type": "house"}
        assert wall_recommendations_instance.ewi_valid is True


class TestCavityWallRecommensations:

    def test_fill_empty_cavity(self):
        input_property = Property(id=1, postcode="F4k3", address1="123 fake street", epc_client=Mock())
        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.data = {"county": "Derbyshire"}

        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"], 1668.6600000000003)

        assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.35)
        assert np.isclose(recommender.recommendations[1]["total"], 2004.6600000000003)

    def test_fill_partial_filled_cavity(self):
        input_property = Property(id=1, postcode="F4k3", address1="123 fake street", epc_client=Mock())
        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.data = {"county": "County Durham"}

        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"], 1663.9350000000002)

        assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.41)
        assert np.isclose(recommender.recommendations[1]["total"], 1999.9350000000002)

    def test_system_built_wall(self):
        input_property2 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
        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.data = {"property-type": "House", "county": "Derbyshire", "built-form": "Detached"}

        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) == 9
        assert recommender2.estimated_u_value == 1
        assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.19)
        assert np.isclose(recommender2.recommendations[0]["total"], 15899.9616)
        assert recommender2.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender2.recommendations[0]["parts"][0]["depth"] == 100

        assert np.isclose(recommender2.recommendations[8]["new_u_value"], 0.23)
        assert np.isclose(recommender2.recommendations[8]["total"], 10916.3424)
        assert recommender2.recommendations[8]["parts"][0]["type"] == "internal_wall_insulation"
        assert recommender2.recommendations[8]["parts"][0]["depth"] == 72.5

        assert np.isclose(recommender2.recommendations[6]["new_u_value"], 0.29)
        assert np.isclose(recommender2.recommendations[6]["total"], 10621.934399999998)
        assert recommender2.recommendations[6]["parts"][0]["type"] == "internal_wall_insulation"
        assert recommender2.recommendations[6]["parts"][0]["depth"] == 52.5

    def test_timber_frame_wall(self):
        input_property3 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
        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 = "B"
        input_property3.insulation_wall_area = 99
        input_property3.restricted_measures = False
        input_property3.data = {"property-type": "House", "county": "Derbyshire", "built-form": "Semi-Detached"}

        assert input_property3.walls["is_timber_frame"]

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

        assert not recommender3.recommendations

        recommender3.recommend()

        assert recommender3.recommendations
        assert len(recommender3.recommendations) == 6
        assert recommender3.estimated_u_value == 1.9
        assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.2)
        assert np.isclose(recommender3.recommendations[0]["total"], 13117.46832)
        assert recommender3.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender3.recommendations[0]["parts"][0]["depth"] == 100.0

        assert np.isclose(recommender3.recommendations[1]["new_u_value"], 0.23)
        assert np.isclose(recommender3.recommendations[1]["total"], 34070.50944)
        assert recommender3.recommendations[1]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender3.recommendations[1]["parts"][0]["depth"] == 150.0

    def test_granite_or_whinstone_wall(self):
        input_property4 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
        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 = "A"
        input_property4.insulation_wall_area = 223
        input_property4.restricted_measures = False
        input_property4.data = {"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached"}

        assert input_property4.walls["is_granite_or_whinstone"]

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

        assert not recommender4.recommendations

        recommender4.recommend()

        assert recommender4.recommendations
        assert len(recommender4.recommendations) == 6
        assert recommender4.estimated_u_value == 2.3
        assert np.isclose(recommender4.recommendations[0]["new_u_value"], 0.21)
        assert np.isclose(recommender4.recommendations[0]["total"], 28562.514352)
        assert recommender4.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender4.recommendations[0]["parts"][0]["depth"] == 100

        assert np.isclose(recommender4.recommendations[1]["new_u_value"], 0.23)
        assert np.isclose(recommender4.recommendations[1]["total"], 74186.52678400002)
        assert recommender4.recommendations[1]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender4.recommendations[1]["parts"][0]["depth"] == 150

    def test_cob_wall(self):
        input_property5 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock())
        input_property5.walls = {
            'original_description': 'Cob, as built',
            'clean_description': 'Cob, as built',
            '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': False, 'is_cob': True, 'is_assumed': False,
            'is_sandstone_or_limestone': False, 'is_park_home': False,
            'insulation_thickness': 'none', 'external_insulation': False,
            'internal_insulation': False
        }
        input_property5.age_band = "E"
        input_property5.insulation_wall_area = 77
        input_property5.restricted_measures = False
        input_property5.data = {"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached"}

        assert input_property5.walls["is_cob"]

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

        assert not recommender5.recommendations

        recommender5.recommend()

        assert recommender5.recommendations
        assert len(recommender5.recommendations) == 5
        assert recommender5.estimated_u_value == 0.8
        assert np.isclose(recommender5.recommendations[0]["new_u_value"], 0.29)
        assert np.isclose(recommender5.recommendations[0]["total"], 8665.040384000002)
        assert recommender5.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender5.recommendations[0]["parts"][0]["depth"] == 50

        assert np.isclose(recommender5.recommendations[3]["new_u_value"], 0.26)
        assert np.isclose(recommender5.recommendations[3]["total"], 20078.742992)
        assert recommender5.recommendations[3]["parts"][0]["type"] == "internal_wall_insulation"
        assert recommender5.recommendations[3]["parts"][0]["depth"] == 100

    def test_sandstone_or_limestone_wall(self):
        input_property6 = Property(id=1, postcode="F4k3 6", address1="623 fake street", epc_client=Mock())
        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': False, 'is_cob': False, 'is_assumed': False,
            '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 = 350
        input_property6.restricted_measures = False
        input_property6.data = {"property-type": "House", "county": "Derbyshire", "built-form": "Mid-Terrace"}

        assert input_property6.walls["is_sandstone_or_limestone"]

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

        assert not recommender6.recommendations

        recommender6.recommend()

        assert recommender6.recommendations
        assert len(recommender6.recommendations) == 9
        assert recommender6.estimated_u_value == 1
        assert np.isclose(recommender6.recommendations[0]["new_u_value"], 0.19)
        assert np.isclose(recommender6.recommendations[0]["total"], 44829.0584)
        assert recommender6.recommendations[0]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender6.recommendations[0]["parts"][0]["depth"] == 100

        assert np.isclose(recommender6.recommendations[2]["new_u_value"], 0.21)
        assert np.isclose(recommender6.recommendations[2]["total"], 116436.25280000002)
        assert recommender6.recommendations[2]["parts"][0]["type"] == "external_wall_insulation"
        assert recommender6.recommendations[2]["parts"][0]["depth"] == 150

        assert np.isclose(recommender6.recommendations[4]["new_u_value"], 0.28)
        assert np.isclose(recommender6.recommendations[4]["total"], 91267.0136)
        assert recommender6.recommendations[4]["parts"][0]["type"] == "internal_wall_insulation"
        assert recommender6.recommendations[4]["parts"][0]["depth"] == 100
