import sys
from pathlib import Path
from unittest.mock import Mock, patch

import pytest
from loguru import logger

from akkudoktoreos.core.ems import get_ems
from akkudoktoreos.prediction.prediction import get_prediction
from akkudoktoreos.prediction.pvforecastakkudoktor import (
    AkkudoktorForecastHorizon,
    AkkudoktorForecastMeta,
    AkkudoktorForecastValue,
    PVForecastAkkudoktor,
    PVForecastAkkudoktorDataRecord,
)
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration

DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")

FILE_TESTDATA_PV_FORECAST_INPUT_1 = DIR_TESTDATA.joinpath("pv_forecast_input_1.json")
FILE_TESTDATA_PV_FORECAST_INPUT_SINGLE_PLANE = DIR_TESTDATA.joinpath(
    "pv_forecast_input_single_plane.json"
)
FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1.txt")


@pytest.fixture
def sample_settings(config_eos):
    """Fixture that adds settings data to the global config."""
    settings = {
        "general": {
            "latitude": 52.52,
            "longitude": 13.405,
        },
        "prediction": {
            "hours": 48,
            "historic_hours": 24,
        },
        "pvforecast": {
            "provider": "PVForecastAkkudoktor",
            "planes": [
                {
                    "peakpower": 5.0,
                    "surface_azimuth": 170,
                    "surface_tilt": 7,
                    "userhorizon": [20, 27, 22, 20],
                    "inverter_paco": 10000,
                },
                {
                    "peakpower": 4.8,
                    "surface_azimuth": 90,
                    "surface_tilt": 7,
                    "userhorizon": [30, 30, 30, 50],
                    "inverter_paco": 10000,
                },
                {
                    "peakpower": 1.4,
                    "surface_azimuth": 140,
                    "surface_tilt": 60,
                    "userhorizon": [60, 30, 0, 30],
                    "inverter_paco": 2000,
                },
                {
                    "peakpower": 1.6,
                    "surface_azimuth": 185,
                    "surface_tilt": 45,
                    "userhorizon": [45, 25, 30, 60],
                    "inverter_paco": 1400,
                },
            ],
        },
    }

    # Merge settings to config
    config_eos.merge_settings_from_dict(settings)
    assert config_eos.pvforecast.provider == "PVForecastAkkudoktor"
    return config_eos


@pytest.fixture
def sample_forecast_data():
    """Fixture that returns sample forecast data converted to pydantic model."""
    with FILE_TESTDATA_PV_FORECAST_INPUT_1.open("r", encoding="utf-8", newline=None) as f_in:
        input_data = f_in.read()
    return PVForecastAkkudoktor._validate_data(input_data)


@pytest.fixture
def sample_forecast_data_raw():
    """Fixture that returns raw sample forecast data."""
    with FILE_TESTDATA_PV_FORECAST_INPUT_1.open("r", encoding="utf-8", newline=None) as f_in:
        input_data = f_in.read()
    return input_data


@pytest.fixture
def sample_forecast_data_single_plane_raw():
    """Fixture that returns raw sample forecast data."""
    with FILE_TESTDATA_PV_FORECAST_INPUT_SINGLE_PLANE.open(
        "r", encoding="utf-8", newline=None
    ) as f_in:
        input_data = f_in.read()
    return input_data


@pytest.fixture
def sample_forecast_report():
    """Fixture that returns sample forecast data report."""
    with FILE_TESTDATA_PV_FORECAST_RESULT_1.open("r", encoding="utf-8", newline=None) as f_res:
        input_data = f_res.read()
    return input_data


@pytest.fixture
def sample_forecast_start(sample_forecast_data):
    """Fixture that returns the start date of the sample forecast data."""
    forecast_start = to_datetime(sample_forecast_data.values[0][0].datetime)
    expected_datetime = to_datetime("2024-10-06T00:00:00.000+02:00")
    assert compare_datetimes(to_datetime(forecast_start), expected_datetime).equal

    timezone_name = sample_forecast_data.meta.timezone
    assert timezone_name == "Europe/Berlin"
    return forecast_start


@pytest.fixture
def provider():
    """Fixture that returns the PVForecastAkkudoktor instance from the prediction."""
    prediction = get_prediction()
    provider = prediction.provider_by_id("PVForecastAkkudoktor")
    assert isinstance(provider, PVForecastAkkudoktor)
    return provider


@pytest.fixture
def provider_empty_instance():
    """Fixture that returns an empty instance of PVForecast."""
    empty_instance = PVForecastAkkudoktor()
    empty_instance.clear()
    assert len(empty_instance) == 0
    return empty_instance


# Sample data for testing
sample_horizon = AkkudoktorForecastHorizon(altitude=30, azimuthFrom=90, azimuthTo=180)
sample_meta = AkkudoktorForecastMeta(
    lat=52.52,
    lon=13.405,
    power=[5000],
    azimuth=[180],
    tilt=[30],
    timezone="Europe/Berlin",
    albedo=0.25,
    past_days=5,
    inverterEfficiency=0.8,
    powerInverter=[10000],
    cellCoEff=-0.36,
    range=True,
    horizont=[[sample_horizon]],
    horizontString=["sample_horizon"],
)
sample_value = AkkudoktorForecastValue(
    datetime="2024-11-09T12:00:00",
    dcPower=500.0,
    power=480.0,
    sunTilt=30.0,
    sunAzimuth=180.0,
    temperature=15.0,
    relativehumidity_2m=50.0,
    windspeed_10m=10.0,
)
sample_config_data = {
    "general": {
        "latitude": 52.52,
        "longitude": 13.405,
    },
    "prediction": {
        "hours": 48,
        "historic_hours": 24,
    },
    "pvforecast": {
        "provider": "PVForecastAkkudoktor",
        "planes": [
            {
                "peakpower": 5.0,
                "surface_azimuth": 180,
                "surface_tilt": 30,
                "inverter_paco": 10000,
            }
        ],
    },
}


# Tests for AkkudoktorForecastHorizon
def test_akkudoktor_forecast_horizon():
    horizon = AkkudoktorForecastHorizon(altitude=30, azimuthFrom=90, azimuthTo=180)
    assert horizon.altitude == 30
    assert horizon.azimuthFrom == 90
    assert horizon.azimuthTo == 180


# Tests for AkkudoktorForecastMeta
def test_akkudoktor_forecast_meta():
    meta = sample_meta
    assert meta.lat == 52.52
    assert meta.lon == 13.405
    assert meta.power == [5000]
    assert meta.tilt == [30]
    assert meta.timezone == "Europe/Berlin"


# Tests for AkkudoktorForecastValue
def test_akkudoktor_forecast_value():
    value = sample_value
    assert value.dcPower == 500.0
    assert value.power == 480.0
    assert value.temperature == 15.0
    assert value.windspeed_10m == 10.0


# Tests for PVForecastAkkudoktorDataRecord
def test_pvforecast_akkudoktor_data_record():
    record = PVForecastAkkudoktorDataRecord(
        pvforecastakkudoktor_ac_power_measured=1000.0,
        pvforecastakkudoktor_wind_speed_10m=10.0,
        pvforecastakkudoktor_temp_air=15.0,
    )
    assert record.pvforecastakkudoktor_ac_power_measured == 1000.0
    assert record.pvforecastakkudoktor_wind_speed_10m == 10.0
    assert record.pvforecastakkudoktor_temp_air == 15.0
    assert (
        record.pvforecastakkudoktor_ac_power_any == 1000.0
    )  # Assuming AC power measured is preferred


def test_pvforecast_akkudoktor_validate_data(provider_empty_instance, sample_forecast_data_raw):
    """Test validation of PV forecast data on sample data."""
    logger.info("The following errors are intentional and part of the test.")
    with pytest.raises(
        ValueError,
        match="Field: meta\nError: Field required\nType: missing\nField: values\nError: Field required\nType: missing\n",
    ):
        ret = provider_empty_instance._validate_data("{}")
    data = provider_empty_instance._validate_data(sample_forecast_data_raw)
    # everything worked


def test_pvforecast_akkudoktor_validate_data_single_plane(
    provider_empty_instance, sample_forecast_data_single_plane_raw
):
    """Test validation of PV forecast data on sample data with a single plane."""
    logger.info("The following errors are intentional and part of the test.")
    with pytest.raises(
        ValueError,
        match="Field: meta\nError: Field required\nType: missing\nField: values\nError: Field required\nType: missing\n",
    ):
        ret = provider_empty_instance._validate_data("{}")
    data = provider_empty_instance._validate_data(sample_forecast_data_single_plane_raw)
    # everything worked


@patch("requests.get")
def test_pvforecast_akkudoktor_update_with_sample_forecast(
    mock_get, sample_settings, sample_forecast_data_raw, sample_forecast_start, provider
):
    """Test data processing using sample forecast data."""
    # Mock response object
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.content = sample_forecast_data_raw
    mock_get.return_value = mock_response

    # Test that update properly inserts data records
    ems_eos = get_ems()
    ems_eos.set_start_datetime(sample_forecast_start)
    provider.update_data(force_enable=True, force_update=True)
    assert compare_datetimes(provider.start_datetime, sample_forecast_start).equal
    assert compare_datetimes(provider[0].date_time, to_datetime(sample_forecast_start)).equal


# Report Generation Test
def test_report_ac_power_and_measurement(provider, config_eos):
    # Set the configuration
    config_eos.merge_settings_from_dict(sample_config_data)

    record = PVForecastAkkudoktorDataRecord(
        pvforecastakkudoktor_ac_power_measured=900.0,
        pvforecast_dc_power=450.0,
        pvforecast_ac_power=400.0,
    )
    provider.append(record)

    report = provider.report_ac_power_and_measurement()
    assert "DC: 450.0" in report
    assert "AC: 400.0" in report
    assert "AC sampled: 900.0" in report


@pytest.mark.skipif(
    sys.platform.startswith("win"), reason="'other_timezone' fixture not supported on Windows"
)
@patch("requests.get")
def test_timezone_behaviour(
    mock_get,
    sample_settings,
    sample_forecast_data_raw,
    sample_forecast_start,
    provider,
    set_other_timezone,
):
    """Test PVForecast in another timezone."""
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.content = sample_forecast_data_raw
    mock_get.return_value = mock_response

    # sample forecast start in other timezone
    other_timezone = set_other_timezone()
    other_start_datetime = to_datetime(sample_forecast_start, in_timezone=other_timezone)
    assert compare_datetimes(other_start_datetime, sample_forecast_start).equal
    expected_datetime = to_datetime("2024-10-06T00:00:00+0200", in_timezone=other_timezone)
    assert compare_datetimes(other_start_datetime, expected_datetime).equal

    provider.clear()
    assert len(provider) == 0
    ems_eos = get_ems()
    ems_eos.set_start_datetime(other_start_datetime)
    provider.update_data(force_update=True)
    assert compare_datetimes(provider.start_datetime, other_start_datetime).equal
    # Check wether first record starts at requested sample start time
    assert compare_datetimes(provider[0].date_time, sample_forecast_start).equal

    # Test updating AC power measurement for a specific date.
    provider.update_value(sample_forecast_start, "pvforecastakkudoktor_ac_power_measured", 1000)
    # Check wether first record was filled with ac power measurement
    assert provider[0].pvforecastakkudoktor_ac_power_measured == 1000

    # Test fetching temperature forecast for a specific date.
    other_end_datetime = other_start_datetime + to_duration("24 hours")
    expected_end_datetime = to_datetime("2024-10-07T00:00:00+0200", in_timezone=other_timezone)
    assert compare_datetimes(other_end_datetime, expected_end_datetime).equal
    forecast_temps = provider.key_to_series(
        "pvforecastakkudoktor_temp_air", other_start_datetime, other_end_datetime
    )
    assert len(forecast_temps) == 23  # 24-1, first temperature is null
    assert forecast_temps.iloc[0] == 6.5
    assert forecast_temps.iloc[1] == 6.0

    # Test fetching AC power forecast
    other_end_datetime = other_start_datetime + to_duration("48 hours")
    forecast_measured = provider.key_to_series(
        "pvforecastakkudoktor_ac_power_measured", other_start_datetime, other_end_datetime
    )
    assert len(forecast_measured) == 1
    assert forecast_measured.iloc[0] == 1000.0  # changed before
