from datetime import datetime
import pandas as pd
import pickle
import pytest
from backend.Property import Property
from etl.epc.Record import EPCRecord
from etl.bill_savings.KwhData import KwhData
from recommendations.HeatingRecommender import HeatingRecommender
from recommendations.tests.test_data.heating_recommendations_data import testing_examples
from recommendations.tests.test_data.materials import materials


class TestHeatingRecommendations:

    @pytest.fixture()
    def cleaning_data(self):
        with open("recommendations/tests/test_data/cleaning_data.pkl", "rb") as f:
            data = pickle.load(f)

        return data

    @pytest.fixture()
    def cleaned(self):
        with open("recommendations/tests/test_data/cleaned.pkl", "rb") as f:
            df = pickle.load(f)

        return df

    @pytest.fixture()
    def kwh_client(self):
        client = KwhData(bucket="retrofit-data-dev", read_consumption_data=False)
        # We fix this pricing table for these tests
        client.retail_price_comparison = pd.DataFrame(
            [
                {
                    "Date": datetime.today().strftime("%Y-%m-%d"),
                    'Average standard variable tariff (Large legacy suppliers)': 1
                }
            ]
        )
        client.retail_price_comparison["Date"] = pd.to_datetime(client.retail_price_comparison["Date"])
        return client

    @pytest.mark.parametrize(
        "test_case",
        testing_examples
    )
    def test_recommend(self, test_case, cleaning_data, cleaned, kwh_client):
        """
        With this function, we test out multiple heating descriptions and check which recomendations
        we retrieve alongside them
        :return:
        """

        # We patch an old version of cleaned which is missing some attributes for 'mainheat-description'
        for x in cleaned['mainheat-description']:
            x["has_hot-water-only"] = False
            x["has_mineral_and_wood"] = False
            x["has_dual_fuel_appliance"] = False
            x["has_wood_chips"] = False

        epc_records = {"original_epc": test_case["epc"].copy(), "full_sap_epc": {}, "old_data": []}

        epc_record = EPCRecord(
            epc_records=epc_records,
            run_mode="newdata",
            cleaning_data=cleaning_data
        )

        p = Property(
            id=0,
            postcode=test_case["epc"]["postcode"],
            address=test_case["epc"]["address"],
            epc_record=epc_record,
            energy_assessment={
                "condition": {},
                "energy_assessment_is_newer": False
            }
        )
        p.already_installed = []

        # For these tests, this can be fixed
        kwh_predictions = {
            "heating_kwh_predictions": pd.DataFrame(
                [
                    {"id": p.uprn, "predictions": 12000}
                ]
            ),
            "hotwater_kwh_predictions": pd.DataFrame(
                [
                    {"id": p.uprn, "predictions": 3000}
                ]
            ),
        }

        p.set_features(cleaned=cleaned, kwh_client=kwh_client, kwh_predictions=kwh_predictions)

        recommender = HeatingRecommender(property_instance=p, materials=materials)
        # Check they're empty
        assert not recommender.heating_recommendations

        recommender.recommend(has_cavity_or_loft_recommendations=False)

        assert len(recommender.heating_recommendations) == len(test_case["heating_measure_types"])

        # Check the exact measure types
        assert (
            {x["measure_type"] for x in recommender.heating_recommendations} ==
            set(test_case["heating_measure_types"])
        )


@pytest.mark.parametrize(
    "floor_area, epc_primary, expected_band, expected_model",
    [
        # Case 1 – Typical pre-2000 house, gas heating
        (
            93.75,
            270.19,
            (2.5, 4.6),  # expected rough band (low, high)
            5,  # chosen model
        ),
        # Case 2 – Efficient new-build (low EPC energy)
        (
            93.75,
            142.28,
            (1.4, 2.4),
            3,  # assume 3 or 5 kW model covers this
        ),
    ],
)
def test_estimate_peak_kw_basic(floor_area, epc_primary, expected_band, expected_model):
    """
    Ensure the peak load estimate is within a sensible range and
    that the model selection logic picks the correct bracket.
    """

    load_band = HeatingRecommender.estimate_peak_kw(
        floor_area_m2=floor_area,
        epc_primary_kwh_per_m2_yr=epc_primary,
        primary_to_delivered_factor=1.55,  # electricity
        space_heat_fraction_range=(0.35, 0.60),
        hdd_base_dd=2000.0,
        t_indoor_C=21.0,
        t_design_ext_C=-1.0,
    )

    # Assert range sanity
    assert expected_band[0] * 0.8 <= load_band[0] <= expected_band[1] * 1.2
    assert expected_band[0] <= load_band[1] <= expected_band[1] * 1.2

    # Pick model
    model = HeatingRecommender.pick_model(load_band, models_kw=(3, 5, 6, 8.5, 11.2))
    assert model == expected_model


def test_estimate_peak_kw_with_hlp():
    """
    Test direct HLP input path (best-quality data).
    """
    hlp = 1.5  # W/m²K typical for semi-detached
    floor_area = 100
    load_band = HeatingRecommender.estimate_peak_kw(
        floor_area_m2=floor_area,
        heat_loss_parameter_W_per_m2K=hlp,
        t_indoor_C=21,
        t_design_ext_C=-2,
    )
    # Should return identical low/high values since it's direct
    assert isinstance(load_band, tuple)
    assert abs(load_band[0] - load_band[1]) < 1e-6
    # Expected peak = 1.5 * 100 * 23 / 1000 = 3.45 kW
    assert pytest.approx(load_band[0], rel=0.05) == 3.45


def test_estimate_peak_kw_with_space_heat_demand():
    """
    Test the space-heating-demand path.
    """
    floor_area = 120
    space_heat_kwh_m2 = 100
    load_band = HeatingRecommender.estimate_peak_kw(
        floor_area_m2=floor_area,
        space_heat_kwh_per_m2_yr=space_heat_kwh_m2,
        hdd_base_dd=2100,
        t_indoor_C=21,
        t_design_ext_C=-3,
    )
    # Rough expected peak ~ (100*120*1000)/(2100*24) * 24 /1000 = 5.4 kW
    assert 4.5 < load_band[0] < 6.0
    assert abs(load_band[0] - load_band[1]) < 1e-6


def test_pick_model_boundaries():
    """
    Ensure pick_model correctly selects the smallest model covering the upper band.
    """
    assert HeatingRecommender.pick_model((2.0, 4.9), models_kw=(3, 5, 6, 8.5)) == 5
    assert HeatingRecommender.pick_model((5.0, 5.0), models_kw=(3, 5, 6, 8.5)) == 5
    assert HeatingRecommender.pick_model((5.0, 6.1), models_kw=(3, 5, 6, 8.5)) == 8.5
    assert HeatingRecommender.pick_model((8.6, 9.0), models_kw=(3, 5, 6, 8.5, 11.2)) == 11.2
    assert HeatingRecommender.pick_model((20, 25), models_kw=(3, 5, 6, 8.5, 11.2)) == 11.2  # largest model


def test_parameter_validation_and_defaults():
    """
    Validate that the function handles missing or minimal parameters properly.
    """
    # Minimal path using primary energy only
    load_band = HeatingRecommender.estimate_peak_kw(
        floor_area_m2=80,
        epc_primary_kwh_per_m2_yr=250,
    )
    assert isinstance(load_band, tuple)
    assert load_band[0] < load_band[1]
