import pytest
import pandas as pd
from backend.Funding import Funding, EligibilityCaveats
from backend.tests.test_data.innovation_measure_fixtures import innovation_scenarios
from backend.tests.test_data.pre_heating_scenarios import pre_main_heating_scenarios
from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes
from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes


@pytest.fixture
def mock_project_scores_matrix():
    data = []
    floor_segments = ["0-72", "73-97", "98-199", "200"]
    bands = [
        "Low_G", "High_G", "Low_F", "High_F", "Low_E", "High_E", "Low_D", "High_D", "Low_C", "High_C", "Low_B",
        "High_B", "Low_A", "High_A"
    ]

    cost = 50.0
    for floor in floor_segments:
        for start in bands:
            for finish in bands:
                if start != finish:  # skip identical start/finish (no SAP movement)
                    data.append({
                        "Floor Area Segment": floor,
                        "Starting Band": start,
                        "Finishing Band": finish,
                        "Cost Savings": cost
                    })
                    cost += 5.0  # increment to create variety

    return pd.DataFrame(data)


@pytest.fixture
def mock_partial_scores_matrix():
    df = pd.read_csv("backend/tests/test_data/ECO4_Partial_Project_Scores_Matrix_v6.csv")
    df.columns = ['Measure category', 'Measure_Type', 'Pre_Main_Heating_Source',
                  'Post_Main_Heating_Source', 'Total Floor Area Band', 'Starting Band',
                  'Average Treatable Factor', 'Cost Savings', 'SAP Savings']
    return df


@pytest.fixture
def mock_whlg_postcodes():
    return pd.DataFrame([{"Postcode": "ab12cd"}])


@pytest.fixture
def mock_mainheating():
    return {
        'original_description': 'Electric storage heaters', 'has_radiators': False,
        'has_fan_coil_units': False,
        'has_pipes_in_screed_above_insulation': False,
        'has_pipes_in_insulated_timber_floor': False,
        'has_pipes_in_concrete_slab': False, 'has_boiler': False,
        'has_air_source_heat_pump': False,
        'has_room_heaters': False, 'has_electric_storage_heaters': True, 'has_warm_air': False,
        'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
        'has_community_scheme': False,
        'has_ground_source_heat_pump': False, 'has_no_system_present': False,
        'has_portable_electric_heaters': False,
        'has_water_source_heat_pump': False, 'has_electric': True, 'has_mains_gas': False,
        'has_wood_logs': False,
        'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite':
            False,
        'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False,
        'has_assumed': False,
        'has_electricaire': False, 'has_assumed_for_most_rooms': False,
        'has_underfloor_heating': False,
        "has_electric_heat_pumps": False,
        "has_micro-cogeneration": False,
        'has_mineral_and_wood': False,
        "has_dual_fuel_appliance": False
    }


@pytest.fixture
def mock_main_fuel():
    return {
        'original_description': 'Electricity: electricity, unspecified tariff', 'fuel_type':
            'electricity',
        'tariff_type': 'unspecified tariff', 'is_community': False,
        'no_individual_heating_or_community_network': False,
        'complex_fuel_type': None,
    }


@pytest.fixture
def mock_mainheat_energy_eff():
    return "Average"


### -------------------------
### PRIVATE RENTED SECTOR (PRS)
### -------------------------

def test_eco4_prs_eligible_with_swi(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    # The property is:
    # 1) private,
    # 2) EPC E
    # 3) is getting a solid was measure
    # so it's eligible for ECO4

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
    funding.check_funding(
        measures=measures,
        starting_sap=50,  # EPC E
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        is_partial=False,
        existing_li_thickness=0,
        current_wall_uvalue=2,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.eco4_eligible
    assert EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME in funding.eco4_eligibility_caveats


def test_eco4_prs_not_eligible_high_epc(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """Should NOT be eligible if EPC is too high (C or above)."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
    funding.check_funding(
        measures=measures,
        starting_sap=72,  # EPC C (too high)
        ending_sap=75,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        is_partial=False,
        existing_li_thickness=0,
        current_wall_uvalue=2,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible


def test_gbis_prs_general_eligibility(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """PRS EPC D–G & council tax band A–D should trigger GBIS general route."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
    funding.check_funding(
        measures=measures,
        starting_sap=65,  # EPC D
        ending_sap=70,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="A",
        is_partial=False,
        existing_li_thickness=0,
        current_wall_uvalue=2,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.gbis_eligible


def test_gbis_prs_low_income_caveat(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """PRS EPC D–G should flag low-income caveat when low-income route is used."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
    funding.check_funding(
        measures=measures,
        starting_sap=60,  # EPC D
        ending_sap=70,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        is_partial=False,
        existing_li_thickness=0,
        current_wall_uvalue=2,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.gbis_eligible
    assert EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME in funding.gbis_eligibility_caveats


### -------------------------
### SOCIAL HOUSING
### -------------------------

def test_eco4_sh_epc_e_eligible(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC E social housing should be ECO4 eligible without innovation."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
    funding.check_funding(
        measures=measures,
        starting_sap=50,  # EPC E
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.eco4_eligible


def test_eco4_sh_epc_d_requires_innovation(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC D social housing should require an innovation measure."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]
    funding.check_funding(
        measures=measures,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats

    # Test with an innovation measure
    funding2 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )
    measures2 = [{"type": "internal_wall_insulation", "is_innovation": True, "innovation_uplift": 0.25}]
    funding2.check_funding(
        measures=measures2,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding2.eco4_eligible
    assert not funding2.eco4_eligibility_caveats

    # Test with innovation solar. If the measure is solar, we need to have an eligible heating system.
    # If we don't have an eligible heating system in place, we need to have one as part of the measure
    # package
    # THIS SHOULD NOT BE ELIGIBLE
    funding3 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )
    measures3 = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}]
    funding3.check_funding(
        measures=measures3,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding3.eco4_eligible
    assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding3.eco4_eligibility_caveats

    # Test with innovation solar and ASHP. This should be eligible
    funding4 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures4 = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, ]
    funding4.check_funding(
        measures=measures4,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Air source heat pump, radiators",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding4.eco4_eligible
    assert not funding4.eco4_eligibility_caveats

    # Test with innovation solar, a non-eligible heating system but a heating upgrade
    funding5 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures5 = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "high_heat_retention_storage_heaters", "is_innovation": False, "innovation_uplift": 0}
    ]
    funding5.check_funding(
        measures=measures5,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding5.eco4_eligible
    assert EligibilityCaveats.INNOVATION_REQUIRED in funding5.eco4_eligibility_caveats

    # Test with innovation solar, an eligible heating system but a package that excludes the required
    # fabric upgrades
    funding6 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures6 = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
    ]
    funding6.check_funding(
        measures=measures6,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="controls for high heat retention storage heaters",
        is_cavity=True,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=False,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding6.eco4_eligible
    assert EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET in funding6.eco4_eligibility_caveats

    # Test with innovation solar, an eligible heating system but a package that includes the required
    # fabric upgrades
    funding7 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )
    measures7 = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}
    ]
    funding7.check_funding(
        measures=measures7,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Air source heat pump, radiators",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )
    assert funding7.eco4_eligible
    assert not funding7.eco4_eligibility_caveats


def test_eco4_sh_solar_pv_requires_heating(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """Solar PV as innovation measure requires ASHP or HHRSH."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}]
    funding.check_funding(
        measures=measures,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats


def test_eco4_sh_solar_pv_with_heating_is_ok(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """Solar PV innovation with ASHP should pass EPC D innovation rule."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social",
    )

    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
    ]
    funding.check_funding(
        measures=measures,
        starting_sap=60,  # EPC D
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats


def test_eco4_upgrade_requirement_e_to_c_pass(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC E upgraded to C should pass ECO4 upgrade rule."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]

    # E (SAP 50) → C (SAP 70) meets upgrade rule
    funding.check_funding(
        measures=measures,
        starting_sap=50,
        ending_sap=70,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.eco4_eligible


def test_eco4_upgrade_requirement_e_to_d_fail(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC E upgraded to D should FAIL ECO4 upgrade rule (needs to hit C)."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]

    # E (SAP 50) → D (SAP 65) does NOT meet ECO4 upgrade rule
    funding.check_funding(
        measures=measures,
        starting_sap=50,
        ending_sap=65,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible


def test_eco4_upgrade_requirement_f_to_d_pass(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC F upgraded to D should pass ECO4 upgrade rule."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]

    # F (SAP 35) → D (SAP 60) is OK for ECO4
    funding.check_funding(
        measures=measures,
        starting_sap=35,
        ending_sap=60,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.eco4_eligible


def test_eco4_upgrade_requirement_f_to_e_fail(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC F upgraded only to E should FAIL ECO4 upgrade rule (needs to hit at least D)."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}]

    # F (SAP 35) → E (SAP 50) does NOT meet ECO4 rule
    funding.check_funding(
        measures=measures,
        starting_sap=35,
        ending_sap=50,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        council_tax_band="B",
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible


### -------------------------
### INNOVATION PRODUCTS
### -------------------------
def test_epc_d_social_no_innovation_no_heating(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=61,
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        has_wall_insulation_recommendation=False,
        has_roof_insulation_recommendation=False,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats


def test_epc_d_social_with_heating_and_insulation(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    # Should NOT be eligible as the ASHP is not an innovation measure
    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=61,
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats


def test_epc_d_social_solar_with_only_minimum_insulation_should_fail(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    # Solar PV innovation with insulation, but no heating system upgrade => not eligible
    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=61,
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats


def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    # Solar PV innovation with heating, but no insulation when insulation is recommended => not eligible
    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=61,
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET in funding.eco4_eligibility_caveats


def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass(
    mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    # Innovation solar + insulation measures + eligible heating upgrade = not valid because the heat pump isn;t
    # an innovation measure
    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=61,
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats

    funding2 = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    # Innovation solar + insulation measures + eligible heating upgrade = should be valid because the
    # heat pump is an innovation measure
    measures2 = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "air_source_heat_pump", "is_innovation": True, "innovation_uplift": 0.25}
    ]

    funding2.check_funding(
        measures=measures2,
        starting_sap=61,
        ending_sap=69,
        floor_area=80,
        mainheat_description="Electric storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding2.eco4_eligible
    assert not funding2.eco4_eligibility_caveats


@pytest.mark.parametrize("scenario", innovation_scenarios)
def test_custom_eco4_scenarios(
    scenario,
    mock_project_scores_matrix,
    mock_partial_scores_matrix,
    mock_whlg_postcodes,
    mock_mainheating,
    mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    funding.check_funding(
        measures=scenario["measures"],
        starting_sap=scenario["starting_sap"],
        ending_sap=69,
        floor_area=80,
        mainheat_description=scenario["mainheat_description"],
        heating_control_description=scenario["heating_control_description"],
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        has_wall_insulation_recommendation=scenario.get("has_wall_insulation_recommendation", False),
        has_roof_insulation_recommendation=scenario.get("has_roof_insulation_recommendation", False),
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff
    )

    assert funding.eco4_eligible == scenario["expected_eligibility"], f"Failed: {scenario['description']}"
    for caveat in scenario.get("expected_caveats", []):
        assert caveat in funding.eco4_eligibility_caveats, f"Missing caveat in: {scenario['description']}"
    for caveat in funding.eco4_eligibility_caveats:
        assert caveat in scenario.get("expected_caveats", []), f"Unexpected caveat in: {scenario['description']}"


### -------------------------
### Innovation uplift scenarios
### -------------------------

def test_uplift(
    mock_project_scores_matrix,
    mock_partial_scores_matrix,
    mock_whlg_postcodes,
    mock_mainheating,
    mock_main_fuel,
    mock_mainheat_energy_eff
):
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Social"
    )

    # # TODO: Add a scenario with multiple measures, where some are innovation, some are not and we have
    # TODO: Make sure private works too
    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0},
        {"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25},
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=33,
        ending_sap=69,
        floor_area=71,
        mainheat_description="Electic storage heaters",
        heating_control_description="Manual charge control",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff,
    )

    assert funding.eco4_funding == 5302.3949999999995
    assert funding.full_project_abs == 280  # Doesn't include the eco4 uplift
    assert funding.eco4_uplift == 112.77


def _dummy_funding():
    # Matrices/whlg are unused by _map_to_pre_main_heating; pass harmless placeholders
    return Funding(
        tenure="Social",
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        project_scores_matrix=None,
        partial_project_scores_matrix=None,
        whlg_eligible_postcodes=set(),
    )


@pytest.mark.parametrize("scenario", pre_main_heating_scenarios)
def test_map_to_pre_main_heating(scenario):
    funding = _dummy_funding()

    # Build normalized mainheating / main_fuel using your attribute processors
    h = MainHeatAttributes(description=scenario["MAINHEAT_DESCRIPTION"]).process()
    f = MainFuelAttributes(description=scenario["MAIN_FUEL"]).process()

    result = funding._map_to_pre_main_heating(
        mainheating=h,
        main_fuel=f,
        mainheat_energy_eff=scenario["MAINHEAT_ENERGY_EFF"],
    )

    assert result == scenario[
        "expected"], f"Failed: {scenario['description']} -> {result} (expected {scenario['expected']})"


# Large scale testing for various measures
# measures = [
#     {"type": "solar_pv", "is_innovation": True, "uplift": 0.45},
#     {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0},
#     {"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0},
#     {"type": "external_wall_insulation", "is_innovation": False, "uplift": 0},
#     {"type": "loft_insulation", "is_innovation": False, "uplift": 0},
#     {"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0},
#     {"type": "double_glazing", "is_innovation": False, "uplift": 0},
#     {"type": "cavity_wall_insulation", "is_innovation": True, "uplift": 0.25},
#     {"type": "high_heat_retention_storage_heaters", "is_innovation": False, "uplift": 0},
# ]


### -------------------------
### PRIVATE (PRS/Owner) — Innovation uplift behaviour
### -------------------------

def test_private_epc_e_solar_needs_heating(
    mock_project_scores_matrix,
    mock_partial_scores_matrix,
    mock_whlg_postcodes,
    mock_mainheating,
    mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC D private: Solar PV as innovation requires eligible low-carbon heating."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}]
    funding.check_funding(
        measures=measures,
        starting_sap=54,  # EPC E - eligible for private on EPC
        ending_sap=69,
        floor_area=80,
        mainheat_description="Boiler and radiators, mains gas",  # not eligible for solar innovation
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        has_wall_insulation_recommendation=False,
        has_roof_insulation_recommendation=False,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff,
        council_tax_band="B",
    )

    assert not funding.eco4_eligible
    assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats


def test_private_epc_e_solar_with_heating_and_minimum_insulation_produces_uplift(
    mock_project_scores_matrix,
    mock_partial_scores_matrix,
    mock_whlg_postcodes,
    mock_mainheating,
    mock_main_fuel,
    mock_mainheat_energy_eff
):
    """EPC E private: Solar PV innovation + eligible heating + required insulation -> eligible and uplift > 0."""
    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    measures = [
        {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45},
        {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0},
        {"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0},
        {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0},
    ]

    funding.check_funding(
        measures=measures,
        starting_sap=54,  # EPC E
        ending_sap=69,
        floor_area=80,
        mainheat_description="Air source heat pump, radiators",  # eligible low-carbon heating present
        heating_control_description="Programmer, room thermostat and TRVs",
        is_cavity=True,
        current_wall_uvalue=2,
        is_partial=False,
        existing_li_thickness=0,
        has_wall_insulation_recommendation=True,
        has_roof_insulation_recommendation=True,
        mainheating=mock_mainheating,
        main_fuel=mock_main_fuel,
        mainheat_energy_eff=mock_mainheat_energy_eff,
        council_tax_band="B",
    )

    assert funding.eco4_eligible
    assert EligibilityCaveats.INNOVATION_REQUIRED not in funding.eco4_eligibility_caveats
    assert EligibilityCaveats.SOLAR_NEEDS_HEATING not in funding.eco4_eligibility_caveats
    # We don't pin an exact numeric value (depends on score matrices),
    # but innovation uplift should be positive when solar PV has an uplift.
    assert funding.eco4_uplift and funding.eco4_uplift > 0
    # And total funding should include that uplift
    assert funding.eco4_funding and funding.eco4_funding > 0


def test_existing_gshp_to_ashp(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
    r = {'phase': 3, 'parts': [], 'type': 'heating', 'measure_type': 'air_source_heat_pump',
         'description': 'Install a 5KW air source heat pump, and upgrade heating controls to Smart Thermostats, '
                        'room sensors and smart radiator valves (time & temperature zone control). Ensure you have a '
                        'single tariff',
         'starting_u_value': None, 'new_u_value': None, 'sap_points': 7.7, 'already_installed': False,
         'simulation_config': {'mainheat_energy_eff_ending': 'Good', 'hot_water_energy_eff_ending': 'Average',
                               'has_air_source_heat_pump_ending': True, 'has_ground_source_heat_pump_ending': False,
                               'extra_features_ending': None,
                               'thermostatic_control_ending': 'time and temperature zone control',
                               'switch_system_ending': None, 'multiple_room_thermostats_ending': False,
                               'mainheatc_energy_eff_ending': 'Very Good'},
         'description_simulation': {'mainheat-description': 'Air source heat pump, radiators, electric',
                                    'mainheat-energy-eff': 'Good', 'hot-water-energy-eff': 'Average',
                                    'hotwater-description': 'From main system',
                                    'mainheatcont-description': 'Time and temperature zone control',
                                    'mainheatc-energy-eff': 'Very Good'}, 'total': 13188.996000000001,
         'contingency': 3145.8150000000005, 'contingency_rate': 0.35, 'vat': 2080.666, 'labour_hours': 44.7,
         'labour_days': 6.0, 'innovation_rate': 0, 'recommendation_id': '6_phase=3',
         'efficiency': 13188.996000000001, 'co2_equivalent_savings': 0.4999999999999998,
         'heat_demand': 53.20000000000002, 'kwh_savings': 801.5000000000005,
         'energy_cost_savings': 327.31316785714296
         }

    funding = Funding(
        project_scores_matrix=mock_project_scores_matrix,
        partial_project_scores_matrix=mock_partial_scores_matrix,
        whlg_eligible_postcodes=mock_whlg_postcodes,
        eco4_social_cavity_abs_rate=13.5,
        eco4_social_solid_abs_rate=17,
        eco4_private_cavity_abs_rate=13.5,
        eco4_private_solid_abs_rate=17,
        gbis_social_cavity_abs_rate=21,
        gbis_social_solid_abs_rate=25,
        gbis_private_cavity_abs_rate=22,
        gbis_private_solid_abs_rate=28,
        tenure="Private",
    )

    (
        pps, ppf, iu, ups
    ) = funding.get_innovation_uplift(
        measure=r,
        starting_sap=62,
        floor_area=69,
        is_cavity=True,
        current_wall_uvalue=0.7,
        is_partial=False,
        existing_li_thickness=200,
        mainheating={
            'original_description': 'Ground source heat pump, radiators, electric',
            'clean_description': 'Ground source heat pump, radiators, electric', 'has_radiators': True,
            'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False,
            'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False,
            'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False,
            'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
            'has_community_scheme': False, 'has_ground_source_heat_pump': True, 'has_no_system_present': False,
            'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False,
            'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False,
            'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_hot-water-only': False,
            'has_electric': True, 'has_mains_gas': False, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False,
            'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False,
            'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_mineral_and_wood': False,
            'has_dual_fuel_appliance': False, 'has_assumed': False, 'has_electricaire': False,
            'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False
        },
        main_fuel={
            'original_description': 'electricity (not community)',
            'clean_description': 'Electricity not community', 'fuel_type': 'electricity', 'tariff_type': None,
            'is_community': False, 'no_individual_heating_or_community_network': False,
            'complex_fuel_type': None
        },
        mainheat_energy_eff="Poor",
    )

    # All should be zero
    assert pps == 0
    assert ppf == 0
    assert iu == 0
    assert ups == 0
