use chrono::TimeDelta;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;

use super::test;
use crate::{
    assert_approx_eq, currency,
    duration::{test::FromHms as _, ToHoursDecimal as _},
    generate::{self, ChargingPeriod, Dimension, DimensionType, PartialCdr},
    json::{self, FromJson as _},
    price, tariff, Ampere, Kw, Kwh, Money, Price,
};

const DATE: &str = "2025-11-10";

fn config_ten_mins() -> generate::Config {
    generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:02:12"),
        end_date_time: test::datetime_utc(DATE, "15:12:12"),
        max_power_supply_kw: Decimal::from(24),
        requested_kwh: Decimal::from(80),
        max_current_supply_amp: Decimal::from(4),
    }
}

fn config_two_mins() -> generate::Config {
    generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:02:12"),
        end_date_time: test::datetime_utc(DATE, "15:04:12"),
        max_power_supply_kw: dec!(276),
        requested_kwh: dec!(0.2),
        max_current_supply_amp: Decimal::from(300),
    }
}

fn config_six_hours() -> generate::Config {
    generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:02:12"),
        end_date_time: test::datetime_utc(DATE, "19:02:12"),
        max_power_supply_kw: Decimal::from(24),
        requested_kwh: Decimal::from(80),
        max_current_supply_amp: Decimal::from(4),
    }
}

#[track_caller]
fn periods(tariff_json: &str, config: &generate::Config) -> Vec<price::Period> {
    let tariff = tariff::build_versioned(
        tariff::infer_version(json::parse_object(tariff_json).unwrap()).unwrap_certain(),
    )
    .ignore_warnings();
    let (metrics, _tz) = generate::metrics(&tariff, config).unwrap().unwrap();
    let tariff = tariff::v221::Tariff::from_json(tariff.as_element())
        .unwrap()
        .unwrap();
    let timeline = super::timeline(chrono_tz::Tz::Europe__Amsterdam, &metrics, &tariff);
    super::charge_periods(&metrics, timeline)
}

#[test]
fn should_partially_charge_battery() {
    const TARIFF_JSON: &str = r#"{
    "country_code": "DE",
    "party_id": "ALL",
    "id": "1",
    "currency": "EUR",
    "type": "REGULAR",
    "elements": [
        {
            "price_components": [{
                  "type": "ENERGY",
                  "price": 0.50,
                  "vat": 20.0,
                  "step_size": 1
            }]
        }
    ],
    "last_updated": "2018-12-05T12:01:09Z"
}
"#;

    crate::test::setup();

    let config = config_ten_mins();
    let periods = periods(TARIFF_JSON, &config);
    let [period] = periods
        .try_into()
        .expect("There are no restrictions so there should be one big period");

    let price::Period {
        start_date_time,
        consumed,
    } = period;

    assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));

    let price::Consumed {
        duration_charging,
        duration_idle,
        energy,
        current_max,
        current_min,
        power_max,
        power_min,
    } = consumed;

    assert_eq!(
        duration_charging,
        Some(TimeDelta::minutes(10)),
        "The battery is charged for 10 mins and the plug is pulled"
    );
    let max_power_kw_input = config.max_power_supply_kw;
    let avg_power = Decimal::from(energy.unwrap())
        .checked_div(duration_charging.unwrap().to_hours_dec())
        .unwrap();
    assert!(avg_power <= max_power_kw_input);

    assert_eq!(duration_idle, None, "The battery never fully charges");
    assert_approx_eq!(
            energy,
            Some(Kwh::from(4)),
            "The energy supplied is 24 Kwh from a session duration of 10 Mins (0.1666 Hours), so 4 Kwh should be consumed"
        );
    assert_approx_eq!(
        current_max,
        None,
        "There is no `min_current` or `max_current` restriction defined"
    );
    assert_approx_eq!(
        current_min,
        None,
        "There is no `min_current` or `max_current` defined"
    );
    assert_approx_eq!(
        power_max,
        None,
        "There is no `min_power` or `max_power` defined"
    );
    assert_approx_eq!(
        power_min,
        None,
        "There is no `min_power` or `max_power` defined"
    );
}

#[test]
fn should_fully_charge_then_park() {
    const TARIFF_JSON: &str = r#"{
    "country_code": "DE",
    "party_id": "ALL",
    "id": "1",
    "currency": "EUR",
    "type": "REGULAR",
    "elements": [
        {
            "price_components": [{
                  "type": "ENERGY",
                  "price": 0.50,
                  "vat": 20.0,
                  "step_size": 1
            }]
        }
    ],
    "last_updated": "2018-12-05T12:01:09Z"
}
"#;

    crate::test::setup();

    let config = config_six_hours();
    let periods = periods(TARIFF_JSON, &config);
    let [period_charging, period_parking] = periods.try_into().expect(
        "There are no restrictions so there should be period for charging and a second for idle",
    );

    {
        let price::Period {
            start_date_time,
            consumed,
        } = period_charging;

        assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));

        let price::Consumed {
            duration_charging,
            duration_idle,
            energy,
            current_max,
            current_min,
            power_max,
            power_min,
        } = consumed;

        assert_eq!(
            duration_charging,
            Some(TimeDelta::from_hms("03:20:00")),
            "The battery is charged fully"
        );
        assert_eq!(duration_idle, None);

        assert_approx_eq!(energy, Some(Kwh::from(80)));
        let max_power_kw_input = config.max_power_supply_kw;
        let avg_power = Decimal::from(energy.unwrap())
            .checked_div(duration_charging.unwrap().to_hours_dec())
            .unwrap();
        assert!(avg_power <= max_power_kw_input);

        assert_approx_eq!(
            current_max,
            None,
            "There is no `min_current` or `max_current` restriction defined"
        );
        assert_approx_eq!(
            current_min,
            None,
            "There is no `min_current` or `max_current` defined"
        );
        assert_approx_eq!(
            power_max,
            None,
            "There is no `min_power` or `max_power` defined"
        );
        assert_approx_eq!(
            power_min,
            None,
            "There is no `min_power` or `max_power` defined"
        );
    }

    {
        let price::Period {
            start_date_time,
            consumed,
        } = period_parking;

        assert_eq!(start_date_time, test::datetime_utc(DATE, "18:22:12"));

        let price::Consumed {
            duration_charging,
            duration_idle,
            energy,
            current_max,
            current_min,
            power_max,
            power_min,
        } = consumed;

        assert_eq!(
            duration_charging, None,
            "The battery is charged fully then idle begins"
        );
        assert_eq!(
            duration_idle,
            Some(TimeDelta::from_hms("00:40:00")),
            "The battery is charged fully then idle begins"
        );
        assert_approx_eq!(energy, None);
        assert_approx_eq!(
            current_max,
            None,
            "There is no `min_current` or `max_current` restriction defined"
        );
        assert_approx_eq!(
            current_min,
            None,
            "There is no `min_current` or `max_current` defined"
        );
        assert_approx_eq!(
            power_max,
            None,
            "There is no `min_power` or `max_power` defined"
        );
        assert_approx_eq!(
            power_min,
            None,
            "There is no `min_power` or `max_power` defined"
        );
    }
}

#[test]
#[expect(clippy::cognitive_complexity, reason = "The test is simply complex")]
fn should_generate_power() {
    const TARIFF_JSON: &str = r#"{
    "country_code": "DE",
    "party_id": "ALL",
    "id": "1",
    "currency": "EUR",
    "type": "REGULAR",
    "elements": [
        {
            "price_components": [{
              "type": "ENERGY",
              "price": 0.60,
              "vat": 20.0,
              "step_size": 1
            }],
            "restrictions": {
              "max_power": 16.00
            }
        },
        {
            "price_components": [{
              "type": "ENERGY",
              "price": 0.70,
              "vat": 20.0,
              "step_size": 1
            }],
            "restrictions": {
              "max_power": 32.00
            }
        },
        {
            "price_components": [{
                  "type": "ENERGY",
                  "price": 0.50,
                  "vat": 20.0,
                  "step_size": 1
            }]
        }
    ],
    "last_updated": "2018-12-05T12:01:09Z"
}
"#;

    crate::test::setup();

    let config = config_ten_mins();
    let tariff_elem = tariff::build_versioned(
        tariff::infer_version(json::parse_object(TARIFF_JSON).unwrap()).unwrap_certain(),
    )
    .ignore_warnings();
    let (metrics, _tz) = generate::metrics(&tariff_elem, &config).unwrap().unwrap();
    let tariff = tariff::v221::Tariff::from_json(tariff_elem.as_element())
        .unwrap()
        .unwrap();
    let timeline = super::timeline(chrono_tz::Tz::Europe__Amsterdam, &metrics, &tariff);
    let periods = super::charge_periods(&metrics, timeline);

    // let periods = periods(TARIFF_JSON);
    let [ref period] = periods
        .try_into()
        .expect("There are no restrictions so there should be one big period");

    let price::Period {
        start_date_time,
        consumed,
    } = period;

    assert_eq!(*start_date_time, test::datetime_utc(DATE, "15:02:12"));

    let price::Consumed {
        duration_charging,
        duration_idle,
        energy,
        current_max,
        current_min,
        power_max,
        power_min,
    } = consumed;

    assert_eq!(
        *duration_charging,
        Some(TimeDelta::minutes(10)),
        "The battery is charged for 10 mins and the plug is pulled"
    );
    assert_eq!(*duration_idle, None, "The battery never fully charges");
    assert_approx_eq!(
            energy,
            Some(Kwh::from(4)),
            "The energy supplied is 24 Kwh from a session duration of 10 Mins (0.1666 Hours), so 4 Kwh should be consumed"
        );
    assert_approx_eq!(
        current_max,
        None,
        "There is no `min_current` or `max_current` restriction defined"
    );
    assert_approx_eq!(
        current_min,
        None,
        "There is no `min_current` or `max_current` defined"
    );
    assert_approx_eq!(
        power_max,
        Some(Kw::from(24)),
        "There is a `max_power` defined"
    );
    assert_approx_eq!(
        power_min,
        Some(Kw::from(24)),
        "There is a `min_power` defined"
    );
    let report = generate::cdr_from_tariff(&tariff_elem, &config).unwrap();
    let (report, warnings) = report.into_parts();
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());

    let PartialCdr {
        party_id,
        start_date_time,
        end_date_time,
        currency_code,
        total_energy,
        total_charging_duration,
        total_idle_duration,
        total_cost,
        total_energy_cost,
        total_fixed_cost,
        total_idle_duration_cost,
        total_charging_duration_cost,
        charging_periods,
    } = report.partial_cdr;

    assert_eq!(
        party_id.as_ref().map(ToString::to_string).as_deref(),
        Some("DEALL")
    );
    assert_eq!(currency_code, currency::Code::Eur);
    assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
    assert_eq!(end_date_time, test::datetime_utc(DATE, "15:12:12"));

    assert_approx_eq!(
            total_cost,
            Some(Price {
                excl_vat: Money::from(2.80),
                incl_vat: Some(Money::from(3.36))
            }),
            "The power input is 24 Kw and the second tariff element with a price per KwH or 0.70 should be used."
        );
    assert_eq!(
        total_charging_duration,
        Some(TimeDelta::minutes(10)),
        "The charging session is 10 min and is stopped before the battery is fully charged."
    );
    assert_eq!(
        total_idle_duration, None,
        "There is no idle time since the battery never fully charged."
    );
    assert_approx_eq!(total_energy, Some(Kwh::from(4)));
    assert_approx_eq!(
        total_energy_cost,
        Some(Price {
            excl_vat: Money::from(2.80),
            incl_vat: Some(Money::from(3.36))
        }),
        "The cost per KwH is 70 cents and the VAT is 20%."
    );
    assert_eq!(total_fixed_cost, None, "There are no fixed costs.");
    assert_eq!(
        total_idle_duration_cost, None,
        "There is no idle cost as there is no idle time."
    );
    assert_eq!(
        total_charging_duration_cost, None,
        "There are no time costs defined in the tariff."
    );

    let [period] = charging_periods
        .try_into()
        .expect("There should be one period.");

    let ChargingPeriod {
        start_date_time,
        dimensions,
        tariff_id,
    } = period;

    assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
    assert_eq!(tariff_id.as_deref(), Some("1"));

    let [energy, time] = dimensions
        .try_into()
        .expect("There should be an energy dimension");

    let Dimension {
        dimension_type,
        volume: energy_volume,
    } = energy;

    assert_eq!(dimension_type, DimensionType::Energy);
    assert_approx_eq!(energy_volume, dec!(4.0));

    let Dimension {
        dimension_type,
        volume,
    } = time;

    assert_eq!(dimension_type, DimensionType::Time);
    assert_approx_eq!(volume, TimeDelta::minutes(10).to_hours_dec());
    let max_power_kw_input = config.max_power_supply_kw;
    let avg_power = energy_volume.checked_div(volume).unwrap();
    assert!(avg_power <= max_power_kw_input);
}

#[test]
#[expect(clippy::cognitive_complexity, reason = "The test is simply complex")]
fn should_generate_current() {
    const TARIFF_JSON: &str = r#"{
    "country_code": "DE",
    "party_id": "ALL",
    "id": "1",
    "currency": "EUR",
    "type": "REGULAR",
    "elements": [
        {
            "price_components": [{
              "type": "ENERGY",
              "price": 0.60,
              "vat": 20.0,
              "step_size": 1
            }],
            "restrictions": {
              "max_current": 2
            }
        },
        {
            "price_components": [{
              "type": "ENERGY",
              "price": 0.70,
              "vat": 20.0,
              "step_size": 1
            }],
            "restrictions": {
              "max_current": 4
            }
        },
        {
            "price_components": [{
                  "type": "ENERGY",
                  "price": 0.50,
                  "vat": 20.0,
                  "step_size": 1
            }]
        }
    ],
    "last_updated": "2018-12-05T12:01:09Z"
}
"#;

    crate::test::setup();

    let config = config_ten_mins();
    let tariff_elem = tariff::build_versioned(
        tariff::infer_version(json::parse_object(TARIFF_JSON).unwrap()).unwrap_certain(),
    )
    .ignore_warnings();
    let (metrics, _tz) = generate::metrics(&tariff_elem, &config).unwrap().unwrap();
    let tariff = tariff::v221::Tariff::from_json(tariff_elem.as_element())
        .unwrap()
        .unwrap();
    let timeline = super::timeline(chrono_tz::Tz::Europe__Amsterdam, &metrics, &tariff);
    let periods = super::charge_periods(&metrics, timeline);

    // let periods = periods(TARIFF_JSON);
    let [ref period] = periods
        .try_into()
        .expect("There are no restrictions so there should be one big period");

    let price::Period {
        start_date_time,
        consumed,
    } = period;

    assert_eq!(*start_date_time, test::datetime_utc(DATE, "15:02:12"));

    let price::Consumed {
        duration_charging,
        duration_idle,
        current_max,
        current_min,
        energy,
        power_max,
        power_min,
    } = consumed;

    assert_eq!(
        *duration_charging,
        Some(TimeDelta::minutes(10)),
        "The battery is charged for 10 mins and the plug is pulled"
    );
    assert_eq!(*duration_idle, None, "The battery never fully charges");
    assert_approx_eq!(
            energy,
            Some(Kwh::from(4)),
            "The energy supplied is 24 Kwh from a session duration of 10 Mins (0.1666 Hours), so 4 Kwh should be consumed"
        );
    assert_approx_eq!(
        current_max,
        Some(Ampere::from(4)),
        "There is a `max_current` restriction defined"
    );
    assert_approx_eq!(
        current_min,
        Some(Ampere::from(4)),
        "There is a `max_current` restriction defined"
    );
    assert_approx_eq!(
        power_max,
        None,
        "There is no `min_power` or `max_power` defined"
    );
    assert_approx_eq!(
        power_min,
        None,
        "There is no `min_power` or `max_power` defined"
    );
    let report = generate::cdr_from_tariff(&tariff_elem, &config).unwrap();
    let (report, warnings) = report.into_parts();
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());

    let PartialCdr {
        party_id,
        start_date_time,
        end_date_time,
        currency_code,
        total_energy,
        total_charging_duration,
        total_idle_duration,
        total_cost,
        total_energy_cost,
        total_fixed_cost,
        total_idle_duration_cost,
        total_charging_duration_cost,
        charging_periods,
    } = report.partial_cdr;

    assert_eq!(
        party_id.as_ref().map(ToString::to_string).as_deref(),
        Some("DEALL")
    );
    assert_eq!(currency_code, currency::Code::Eur);
    assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
    assert_eq!(end_date_time, test::datetime_utc(DATE, "15:12:12"));

    assert_approx_eq!(
            total_cost,
            Some(Price {
                excl_vat: Money::from(2.00),
                incl_vat: Some(Money::from(2.40))
            }),
            "The power input is 24 Kw and the second tariff element with a price per KwH or 0.70 should be used."
        );
    assert_eq!(
        total_charging_duration,
        Some(TimeDelta::minutes(10)),
        "The charging session is 10 min and is stopped before the battery is fully charged."
    );
    assert_eq!(
        total_idle_duration, None,
        "There is no idle time since the battery never fully charged."
    );
    assert_approx_eq!(total_energy, Some(Kwh::from(4)));
    assert_approx_eq!(
        total_energy_cost,
        Some(Price {
            excl_vat: Money::from(2.00),
            incl_vat: Some(Money::from(2.40))
        }),
        "The cost per KwH is 70 cents and the VAT is 20%."
    );
    assert_eq!(total_fixed_cost, None, "There are no fixed costs.");
    assert_eq!(
        total_idle_duration_cost, None,
        "There is no idle cost as there is no idle time."
    );
    assert_eq!(
        total_charging_duration_cost, None,
        "There are no time costs defined in the tariff."
    );

    let [period] = charging_periods
        .try_into()
        .expect("There should be one period.");

    let ChargingPeriod {
        start_date_time,
        dimensions,
        tariff_id,
    } = period;

    assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
    assert_eq!(tariff_id.as_deref(), Some("1"));

    let [energy, time] = dimensions
        .try_into()
        .expect("There should be an energy dimension");

    let Dimension {
        dimension_type,
        volume: energy_volume,
    } = energy;

    assert_eq!(dimension_type, DimensionType::Energy);
    assert_approx_eq!(energy_volume, dec!(4.0));

    let Dimension {
        dimension_type,
        volume,
    } = time;

    assert_eq!(dimension_type, DimensionType::Time);
    assert_approx_eq!(volume, TimeDelta::minutes(10).to_hours_dec());
    let max_power_kw_input = config.max_power_supply_kw;
    let avg_power = energy_volume.checked_div(volume).unwrap();
    assert!(avg_power <= max_power_kw_input);
}

#[test]
fn should_split_periods_at_energy_threshold() {
    // A tariff with two rate tiers split at 4 kWh.
    // At 24 kW over 30 minutes (= 12 kWh total), the boundary must fire at exactly 10 minutes
    // (4 kWh / 24 kW = 1/6 h = 10 minutes). The old formula placed it at ~45 s.
    const TARIFF_JSON: &str = r#"{
    "country_code": "DE",
    "party_id": "ALL",
    "id": "1",
    "currency": "EUR",
    "type": "REGULAR",
    "elements": [
        {
            "price_components": [{
                "type": "ENERGY",
                "price": 0.50,
                "vat": 0.0,
                "step_size": 1
            }],
            "restrictions": { "max_kwh": 4.0 }
        },
        {
            "price_components": [{
                "type": "ENERGY",
                "price": 1.00,
                "vat": 0.0,
                "step_size": 1
            }],
            "restrictions": { "min_kwh": 4.0 }
        }
    ],
    "last_updated": "2018-12-05T12:01:09Z"
}
"#;

    crate::test::setup();

    let config = generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:02:12"),
        end_date_time: test::datetime_utc(DATE, "15:32:12"),
        max_power_supply_kw: Decimal::from(24),
        requested_kwh: Decimal::from(80),
        max_current_supply_amp: Decimal::from(4),
    };

    let periods = periods(TARIFF_JSON, &config);
    // The `max_kwh` and `min_kwh` events coincide at the same instant, producing a
    // zero-duration transition period between the two tiers.
    let [period_first_tier, period_transition, period_second_tier] = periods
        .try_into()
        .expect("Two rate tiers with coincident boundary events produce three periods");

    // First tier: 0-10 minutes, 4 kWh at €0.50
    assert_eq!(
        period_first_tier.start_date_time,
        test::datetime_utc(DATE, "15:02:12")
    );
    assert_eq!(
        period_first_tier.consumed.duration_charging,
        Some(TimeDelta::minutes(10)),
        "The 4 kWh threshold at 24 kW is reached at exactly 10 min"
    );
    assert_approx_eq!(period_first_tier.consumed.energy, Some(Kwh::from(4)));

    // Zero-duration transition at the boundary instant
    assert_eq!(
        period_transition.start_date_time,
        test::datetime_utc(DATE, "15:12:12")
    );
    assert_eq!(
        period_transition.consumed.duration_charging,
        Some(TimeDelta::zero())
    );

    // Second tier: 10-30 minutes, 8 kWh at €1.00
    assert_eq!(
        period_second_tier.start_date_time,
        test::datetime_utc(DATE, "15:12:12")
    );
    assert_eq!(
        period_second_tier.consumed.duration_charging,
        Some(TimeDelta::minutes(20)),
    );
    assert_approx_eq!(period_second_tier.consumed.energy, Some(Kwh::from(8)));
}

#[test]
fn should_partially_charge_battery_and_not_exceed_max_power() {
    const TARIFF_JSON: &str = r#"{
    "id": "22",
    "currency": "EUR",
    "elements": [
        {
            "price_components": [
                {
                    "type": "ENERGY",
                    "price": "2.0000",
                    "step_size": 8000
                }
            ]
        },
        {
            "price_components": [
                {
                    "type": "ENERGY",
                    "price": "2.0000",
                    "step_size": 5000
                }
            ]
        },
        {
            "price_components": [
                {
                    "type": "ENERGY",
                    "price": "4.0000",
                    "step_size": 2000
                }
            ]
        }
    ],
    "party_id": "ALL",
    "country_code": "DE",
    "last_updated": "2018-12-18T17:07:11Z"
}
"#;

    crate::test::setup();

    let config = config_two_mins();
    let periods = periods(TARIFF_JSON, &config);
    let [charging_period, _parking_period] = periods
        .try_into()
        .expect("The amount of energy consumed can be reached in small, time window");

    let price::Period {
        start_date_time,
        consumed,
    } = charging_period;

    assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));

    let price::Consumed {
        duration_charging,
        duration_idle: _,
        energy,
        current_max,
        current_min,
        power_max,
        power_min,
    } = consumed;

    let charge_secs = duration_charging.unwrap().num_seconds();
    assert_eq!(
        charge_secs, 2,
        "The battery is charged for 2.61 seconds to reach the requested charge"
    );
    let max_power_kw_input = config.max_power_supply_kw;
    let avg_power = Decimal::from(energy.unwrap())
        .checked_div(duration_charging.unwrap().to_hours_dec())
        .unwrap();
    assert!(avg_power <= max_power_kw_input);

    assert_approx_eq!(
            energy,
            Some(Kwh::from(0.2)),
            "The energy supplied is 0.2 Kwh from a session duration of 2 Mins (0.0333 Hours), so 0.2 Kwh should be consumed"
        );
    assert_approx_eq!(
        current_max,
        None,
        "There is no `min_current` or `max_current` restriction defined"
    );
    assert_approx_eq!(
        current_min,
        None,
        "There is no `min_current` or `max_current` defined"
    );
    assert_approx_eq!(
        power_max,
        None,
        "There is no `min_power` or `max_power` defined"
    );
    assert_approx_eq!(
        power_min,
        None,
        "There is no `min_power` or `max_power` defined"
    );
}
