import numpy as np
import pytest
import math
from unittest.mock import MagicMock
from recommendations import recommendation_utils
from datatypes.enums import QuantityUnits
from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases
from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases
from recommendations.tests.test_data.roof_uvalue_test_cases import roof_uvalue_test_cases


class TestRecommendationUtils:
    @pytest.fixture
    def property_mock(self):
        PropertyMock = MagicMock()
        PropertyMock.data = {
            'total_floor_area_group_decile': 'Decile 1',
            'number-habitable-rooms': 3,
            'number-heated-rooms': 2
        }
        return PropertyMock

    def test_r_value_per_mm_to_u_value(self):
        assert recommendation_utils.r_value_per_mm_to_u_value(1, 2) == 0.5
        with pytest.raises(ZeroDivisionError):
            recommendation_utils.r_value_per_mm_to_u_value(0, 2)

    def test_calculate_u_value_uplift(self):
        assert recommendation_utils.calculate_u_value_uplift(1, 2) == (0.33333333333333337, 0.6666666666666666)
        with pytest.raises(ZeroDivisionError):
            recommendation_utils.calculate_u_value_uplift(0, 2)
        with pytest.raises(ZeroDivisionError):
            recommendation_utils.calculate_u_value_uplift(1, 0)

    def test_is_diminishing_returns(self):
        assert not recommendation_utils.is_diminishing_returns([1, 2, 3], 1, 1, 1)
        assert recommendation_utils.is_diminishing_returns([1, 2, 3], 0.5, 1, 1)
        assert not recommendation_utils.is_diminishing_returns([], 1, None, 1)

    def test_update_lowest_selected_u_value(self):
        assert recommendation_utils.update_lowest_selected_u_value(1, 2) == 1
        assert recommendation_utils.update_lowest_selected_u_value(None, 2) == 2
        assert recommendation_utils.update_lowest_selected_u_value(1, 0.5) == 0.5

    def test_get_recommended_part(self):
        part = {'description': "some insulation material"}

        assert recommendation_utils.get_recommended_part(
            part=part, cost_result={"cost_result": 123}, quantity=99, quantity_unit="m2"
        ) == {'description': "some insulation material", 'quantity': 99, 'quantity_unit': QuantityUnits.m2.value,
              "cost_result": 123}

    def test_get_roof_u_value(self):
        # Test case 1: Insulation thickness is known and is_loft is True
        inputs = {
            'insulation_thickness': '50',
            'is_loft': True,
            'is_roof_room': False,
            'is_thatched': False,
            'has_dwelling_above': False,
            'is_flat': False,
            'is_pitched': True,
            'is_at_rafters': False,
        }
        for age_band in ["A", "B", "C", "D"]:
            assert recommendation_utils.get_roof_u_value(**{**inputs, "age_band": age_band}) == 0.68

    def test_get_roof_u_value_case_2(self):
        inputs = {
            'original_description': 'Pitched, 400+ mm insulation at joists',
            'clean_description': 'Pitched, 400+ mm insulation at joists',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': True,
            'is_roof_room': False,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': False,
            'is_at_rafters': False,
            'is_assumed': False,
            'has_dwelling_above': False,
            'is_valid': True,
            'insulation_thickness': '400+',
            'age_band': "J"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 0.16, f"Expected 0.16, but got {u_value}"

    def test_get_roof_u_value_case_3(self):
        inputs = {
            'original_description': 'Room-in-roof, insulated at rafters',
            'clean_description': 'Room-in-roof, insulated at rafters',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': False,
            'is_roof_room': True,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': False,
            'is_at_rafters': True,
            'is_assumed': False,
            'has_dwelling_above': False,
            'is_valid': True,
            'insulation_thickness': 'average',
            'age_band': "J"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 0.4, f"Expected 0.4, but got {u_value}"

    def test_get_roof_u_value_case_4(self):
        inputs = {
            'original_description': 'Pitched, below average insulation',
            'clean_description': 'Pitched, below average insulation',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': True,
            'is_roof_room': False,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': False,
            'is_at_rafters': False,
            'is_assumed': False,
            'has_dwelling_above': False,
            'is_valid': True,
            'insulation_thickness': 'below average',
            'age_band': "E"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 1.5, f"Expected 1.5, but got {u_value}"

    def test_get_roof_u_value_case_5(self):
        # Test case where insulation thickness is exactly specified
        inputs = {
            'original_description': 'Pitched, 100mm insulation',
            'clean_description': 'Pitched, 100mm insulation',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': True,
            'is_roof_room': False,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': False,
            'is_at_rafters': False,
            'is_assumed': False,
            'has_dwelling_above': False,
            'is_valid': True,
            'insulation_thickness': '100',
            'age_band': "G"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 0.40, f"Expected 0.40, but got {u_value}"

    def test_get_roof_u_value_case_6(self):
        # Test case for a thatched roof
        inputs = {
            'original_description': 'Thatched, 75mm insulation',
            'clean_description': 'Thatched, 75mm insulation',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': False,
            'is_roof_room': False,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': True,
            'is_at_rafters': False,
            'is_assumed': False,
            'has_dwelling_above': False,
            'is_valid': True,
            'insulation_thickness': '75',
            'age_band': "H"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 0.35, f"Expected 0.35, but got {u_value}"

    def test_get_roof_u_value_case_7(self):
        # Test case where the roof has a room in it
        inputs = {
            'original_description': 'Pitched, room-in-roof, above average insulation',
            'clean_description': 'Pitched, room-in-roof, above average insulation',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': True,
            'is_roof_room': True,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': False,
            'is_at_rafters': False,
            'is_assumed': False,
            'has_dwelling_above': False,
            'is_valid': True,
            'insulation_thickness': 'above average',
            'age_band': "J"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 0.3, f"Expected 0.3, but got {u_value}"

    def test_get_roof_u_value_case_8(self):
        # Test case where there is a dwelling above the roof, U-value should be 0
        inputs = {
            'original_description': 'Pitched, 100mm insulation',
            'clean_description': 'Pitched, 100mm insulation',
            'thermal_transmittance': None,
            'thermal_transmittance_unit': None,
            'is_pitched': True,
            'is_roof_room': False,
            'is_loft': False,
            'is_flat': False,
            'is_thatched': False,
            'is_at_rafters': False,
            'is_assumed': False,
            'has_dwelling_above': True,
            'is_valid': True,
            'insulation_thickness': '100',
            'age_band': "J"
        }

        u_value = recommendation_utils.get_roof_u_value(**inputs)
        assert u_value == 0.0, f"Expected 0.0, but got {u_value}"

    @pytest.mark.parametrize(
        "test_case",
        roof_uvalue_test_cases
    )
    def test_roof_uvalues(self, test_case):
        expected_uvalue = test_case["uvalue"]
        inputs = test_case.copy()
        del inputs["uvalue"]
        # insulation_thickness = inputs["insulation_thickness"]
        # has_dwelling_above = inputs["has_dwelling_above"]
        # is_loft = inputs["is_loft"]
        # is_roof_room = inputs["is_roof_room"]
        # is_thatched = inputs["is_thatched"]
        # age_band = inputs["age_band"]
        # is_flat = inputs["is_flat"]
        # is_pitched = inputs["is_pitched"]
        # is_at_rafters = inputs["is_at_rafters"]
        uvalue = recommendation_utils.get_roof_u_value(**inputs)
        assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}"

    @pytest.mark.parametrize(
        "test_case",
        wall_uvalue_test_cases
    )
    def test_get_wall_uvalue(self, test_case):
        expected_uvalue = test_case["uvalue"]
        inputs = test_case.copy()
        del inputs["uvalue"]
        uvalue = recommendation_utils.get_wall_u_value(**inputs)
        assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}"

    @pytest.mark.parametrize("test_input", floor_uvalue_test_cases)
    def test_get_floor_u_value(self, test_input):
        if not isinstance(test_input["expected"], float):
            with pytest.raises(test_input["expected"]):
                recommendation_utils.get_floor_u_value(
                    test_input["floor_type"],
                    test_input["area"],
                    test_input["perimeter"],
                    test_input["age_band"],
                    test_input["wall_type"],
                    test_input["insulation_thickness"],
                )
        else:
            result = recommendation_utils.get_floor_u_value(
                floor_type=test_input["floor_type"],
                area=test_input["area"],
                perimeter=test_input["perimeter"],
                age_band=test_input["age_band"],
                wall_type=test_input["wall_type"],
                insulation_thickness=test_input["insulation_thickness"],
            )
            assert result == pytest.approx(test_input["expected"], abs=1e-2)

    # Test with wall_type not in default_wall_thickness
    def test_wall_type_not_in_default_wall_thickness(self):
        # THis previously raised an error but because it largely dicates the thickness, often defaulted to
        # 300, we just use the default instead of raising an error. We see cases of this in the wild, where we
        # estimate EPCs and end up with unusual wall types, so we have fallbacks in place
        assert recommendation_utils.get_floor_u_value(
            floor_type="solid",
            area=100,
            perimeter=40,
            age_band="A",
            wall_type="InvalidWallType",
            insulation_thickness=None,
        ) == 0.6

    # Test with age_band not in s11
    def test_age_band_not_in_s11(self):
        # This previously raised an error but because it largely dicates the thickness, often defaulted to
        # 300, we just use the default instead of raising an error. We see cases of this in the wild, where we
        # might estimate an EPC
        recommendation_utils.get_floor_u_value(
            floor_type="solid",
            area=100,
            perimeter=40,
            age_band="Z",
            wall_type="Cavity",
            insulation_thickness=None,
        )

    def test_age_band_not_in_s11_exposed_floor(self):
        recommendation_utils.get_exposed_floor_uvalue(None, "BadValue")

    def test_convert_thickness_to_numeric(self):

        assert recommendation_utils.convert_thickness_to_numeric("none", True, False) == 0
        assert recommendation_utils.convert_thickness_to_numeric("below average", True, False) == 50
        assert recommendation_utils.convert_thickness_to_numeric("average", True, False) == 100
        assert recommendation_utils.convert_thickness_to_numeric("above average", True, False) == 270

        assert recommendation_utils.convert_thickness_to_numeric("300+", True, False) == 300
        assert recommendation_utils.convert_thickness_to_numeric("400+", True, False) == 400
        assert recommendation_utils.convert_thickness_to_numeric("270", True, False) == 270

        assert recommendation_utils.convert_thickness_to_numeric("none", False, False) == 0
        assert recommendation_utils.convert_thickness_to_numeric("below average", False, False) == 100
        assert recommendation_utils.convert_thickness_to_numeric("average", False, False) == 270
        assert recommendation_utils.convert_thickness_to_numeric("above average", False, False) == 270

        assert recommendation_utils.convert_thickness_to_numeric("none", False, True) == 0
        assert recommendation_utils.convert_thickness_to_numeric("below average", False, True) == 0
        assert recommendation_utils.convert_thickness_to_numeric("average", False, True) == 100
        assert recommendation_utils.convert_thickness_to_numeric("above average", False, True) == 150


def test_estimate_perimeter_regular_inputs():
    assert math.isclose(
        recommendation_utils.estimate_perimeter(100, 5), 40.24922359499622,
        rel_tol=1e-2
    )
    assert math.isclose(
        recommendation_utils.estimate_perimeter(123, 5), 44.63854836349408,
        rel_tol=1e-2
    )


def test_estimate_perimeter_zero_floor_area():
    with pytest.raises(ZeroDivisionError):
        recommendation_utils.estimate_perimeter(0, 5)

    with pytest.raises(ValueError):
        assert recommendation_utils.estimate_perimeter(0, 0) == 0


def test_estimate_perimeter_invalid_inputs():
    with pytest.raises(ValueError):
        recommendation_utils.estimate_perimeter(100, 0)
    with pytest.raises(ValueError):
        recommendation_utils.estimate_perimeter(-100, 5)
    with pytest.raises(ValueError):
        recommendation_utils.estimate_perimeter(100, -5)


def test_solid_floor():
    assert math.isclose(
        recommendation_utils.get_floor_u_value(
            'solid', 100, 40, 'A', 'stone', insulation_thickness="20mm"
        ),
        0.4, rel_tol=1e-2)


def test_suspended_floor():
    assert math.isclose(
        recommendation_utils.get_floor_u_value(
            'suspended', 100, 40, 'A', 'timber frame', insulation_thickness="20mm"
        ),
        0.49, rel_tol=1e-2)


def test_invalid_floor_type():
    with pytest.raises(ValueError):
        recommendation_utils.get_floor_u_value(
            'invalid_type', 100, 40, 'A', 'stone', insulation_thickness="20mm"
        )


def test_park_home():
    assert recommendation_utils.get_floor_u_value(
        'suspended', 100, 40, 'A', 'park home', insulation_thickness="20mm"
    ) == 0


def test_estimate_pitched_roof_area():
    roof_area0 = recommendation_utils.estimate_pitched_roof_area(
        floor_area=80,
    )
    assert np.isclose(roof_area0, 97.65333333333334)

    roof_area1 = recommendation_utils.estimate_pitched_roof_area(
        floor_area=100,
    )

    assert np.isclose(roof_area1, 122.06666666666666)

    roof_area2 = recommendation_utils.estimate_pitched_roof_area(
        floor_area=45,
    )

    assert np.isclose(roof_area2, 54.93)

    roof_area3 = recommendation_utils.estimate_pitched_roof_area(
        floor_area=60,
    )

    assert np.isclose(roof_area3, 73.24)

    zero_roof_area = recommendation_utils.estimate_pitched_roof_area(
        floor_area=0,
    )

    assert zero_roof_area == 0


def test_external_wall_area():
    # Arrange: Define the test cases
    test_cases = [
        (2, 3, 40, 'End-Terrace', 180),  # 3 exposed walls
        (2, 3, 40, 'Mid-Terrace', 120),  # 2 exposed walls
        (2, 3, 40, 'Semi-Detached', 180),  # 3 exposed walls
        (2, 3, 40, 'Detached', 240),  # 4 exposed walls
    ]

    # Act and Assert: Run the test cases
    for num_floors, floor_height, perimeter, built_form, expected in test_cases:
        result = recommendation_utils.estimate_external_wall_area(num_floors, floor_height, perimeter, built_form)
        assert result == expected, f"Test failed for {built_form}: Expected {expected}, got {result}"


def test_estimate_windows():
    # Based on data from an EPR that has 4 windows
    windows_case_1 = recommendation_utils.estimate_windows(
        property_type="Flat",
        built_form="Semi-Detached",
        construction_age_band="England and Wales: 1976-1982",
        floor_area=37,
        number_habitable_rooms=2,
    )

    assert windows_case_1 == 4, f"Expected 4 windows, got {windows_case_1}"

    # Based on data from an EPR that has 7 winows, however two of the windows were very small, having areas of
    # 0.21m^2 and 0.3m^2 respectively. We see 6 as a reasonable estimate for the number of windows
    windows_case_2 = recommendation_utils.estimate_windows(
        property_type="House",
        built_form="Mid-Terrace",
        construction_age_band="England and Wales: 1950-1966",
        floor_area=69,
        number_habitable_rooms=4,
    )

    assert windows_case_2 == 6, f"Expected 6 windows, got {windows_case_2}"

    # Based on data from an EPR on a bungalow, that has 6 windows. Two of the windows are small, both have a 0.4m^2 area
    # and so 5 windows is an acceptable estimate
    windows_case_3 = recommendation_utils.estimate_windows(
        property_type="Bungalow",
        built_form="Mid-Terrace",
        construction_age_band="England and Wales: 1967-1975",
        floor_area=56,
        number_habitable_rooms=3,
    )

    assert windows_case_3 == 5, f"Expected 5 windows, got {windows_case_3}"

    # Based on data from an EPR on a end terrace house that has 8 windows. One of the windows is very small, with an
    # area of 0.25 m^2 and so 7 windows is an acceptable estimate
    windows_case_4 = recommendation_utils.estimate_windows(
        property_type="House",
        built_form="End-Terrace",
        construction_age_band="England and Wales: 1967-1975",
        floor_area=77.28,
        number_habitable_rooms=4,
    )

    assert windows_case_4 == 7, f"Expected 7 windows, got {windows_case_4}"

    # Based on data from an EPR on a Semi-detatched house that has 11 windows based on the associated condition report
    # Right now, we estimate 12 windows for this property
    windows_case_5 = recommendation_utils.estimate_windows(
        property_type="House",
        built_form="Semi-Detached",
        construction_age_band="England and Wales: 1950-1966",
        floor_area=88.4,
        number_habitable_rooms=5,
    )

    assert windows_case_5 == 12, f"Expected 12 windows, got {windows_case_5}"

    # Based on Khalim's flat which has 3 windows. There is no construction age band on the EPC. The windows are large
    # so an estimate of 5 windows is a reasonable estimate
    windows_case_6 = recommendation_utils.estimate_windows(
        property_type="Flat",
        built_form="",
        construction_age_band="",
        floor_area=100,
        number_habitable_rooms=3,
    )

    assert windows_case_6 == 5, f"Expected 5 windows, got {windows_case_6}"

    # Based on an EPR semi detatched house though we don't have the exact number of windows. We estimate 10
    windows_case_7 = recommendation_utils.estimate_windows(
        property_type="House",
        built_form="Semi-Detached",
        construction_age_band="England and Wales: 1967-1975",
        floor_area=85,
        number_habitable_rooms=4,
    )

    assert windows_case_7 == 10, f"Expected 10 windows, got {windows_case_7}"

    # Base on Khalim's parents flat
    windows_case_8 = recommendation_utils.estimate_windows(
        property_type="Flat",
        built_form="End-Terrace",
        construction_age_band="",
        floor_area=50,
        number_habitable_rooms=3,
    )

    assert windows_case_8 == 5, f"Expected 5 windows, got {windows_case_8}"
