#![allow(
    clippy::unwrap_in_result,
    reason = "unwraps are allowed anywhere in tests"
)]
#![allow(
    clippy::indexing_slicing,
    reason = "tests are allowed to index collections"
)]
#![allow(clippy::string_slice, reason = "tests are allowed to slice strings")]

use std::{collections::BTreeMap, fmt, path::Path, str::FromStr as _};

use chrono::{DateTime, Utc};
use num_traits::Zero as _;
use rand::RngExt as _;
use rust_decimal::Decimal;
use tracing::debug;

use super::CpoId;
use crate::{
    assert_approx_eq_tolerance, cdr, country, currency,
    duration::Hms,
    json, money,
    number::RoundDecimal as _,
    price::{self, test::UnwrapReport as _, Total},
    tariff,
    test::{self, Expectation},
    warning, Kwh, ToHoursDecimal, Version,
};

const TARIFF_FILE_NAME: &str = "tariff.json";

macro_rules! assert_req_total_field {
    ($expected:expr, $report:expr $(,)?) => {
        let field_name = stringify!($report);

        match $expected {
            Expectation::Present(expected) => match expected {
                test::ExpectValue::Some(expected) => {
                    let value = $report.calculated.unwrap_or_else(|| {
                        panic!("`{field_name}` is not calculated.");
                    });
                    assert_approx_eq_tolerance!(
                        value,
                        expected.value(),
                        expected.tolerance(),
                        "for `{field_name}`"
                    );
                }
                test::ExpectValue::Null => {
                    if let Some(v) = $report.calculated {
                        panic!("Expected `{field_name}` to not be calculated; but it has value `{v}`.")
                    }
                }
            },
            Expectation::Absent => {
                panic!("The `{field_name}` field is required.")
            }
        }
    };
}

macro_rules! assert_req_optional_field {
    ($expected:expr, $report:expr $(,)?) => {
        let field_name = stringify!($report);

        match $expected {
            Expectation::Present(expected) => match expected {
                test::ExpectValue::Some(expected) => {
                    let value = $report.unwrap_or_else(|| {
                        panic!("`{field_name}` is not calculated.");
                    });
                    assert_approx_eq_tolerance!(
                        value,
                        expected.value(),
                        expected.tolerance(),
                        "for `{field_name}`"
                    );
                }
                test::ExpectValue::Null => assert!(
                    $report.is_none(),
                    "Expected `{field_name}` to not be calculated; but it is."
                ),
            },
            Expectation::Absent => {
                panic!("The `{field_name}` field is required.")
            }
        }
    };
}

/// Each `config.json` found in the tariff test directory results in a test run.
#[test_each::file(
    glob = "ocpi-tariffs/test_data/popular/*/test_run*.json",
    name(segments = 2)
)]
#[expect(clippy::cognitive_complexity, reason = "The test is simply complex")]
fn should_price_cdr_generated_from_tariff(test_run_json: &str, test_run_file_path: &Path) {
    const VERSION: Version = Version::V221;

    test::setup();

    let json_dir = test_run_file_path
        .parent()
        .expect("The given file should live in a dir");

    let tariff_path = json_dir.join(TARIFF_FILE_NAME);
    debug!("Try to read tariff file: `{TARIFF_FILE_NAME}`");
    let tariff_json = test::read_file_content(&tariff_path).unwrap_or_else(|err| {
        panic!("Unable to read `{TARIFF_FILE_NAME}` file:\n{err}");
    });

    let mut test_run_json = test_run_json.to_owned();
    let test_run = {
        json_strip_comments::strip(&mut test_run_json).unwrap_or_else(|err| {
            panic!(
                "Unable to strip comments from {}:\n{:#?}",
                test_run_file_path.display(),
                err
            );
        });
        serde_json::from_str::<TestRun>(&test_run_json).unwrap_or_else(|err| {
            panic!(
                "Unable to parse {}:\n{:#?}",
                test_run_file_path.display(),
                err
            );
        })
    };
    let TestRun { input, expect } = test_run;
    let Expect {
        currency: expected_currency,
        total_cost: expected_total_cost,
        total_energy_cost: expected_total_energy_cost,
        total_idle_cost: expected_total_idle_cost,
        total_charging_time: expected_total_charging_time,
        total_idle_time: expected_total_idle_time,
        total_energy: expected_total_energy,
        warnings: expected_warnings,
    } = expect;

    let tariff = {
        debug!("Successfully read tariff file: `{TARIFF_FILE_NAME}`");

        let tariff = json::parse_object(&tariff_json).unwrap();
        let (tariff, warnings) = tariff::build(tariff, VERSION).into_parts();
        warning::test::assert_warnings(
            &test_run_file_path.display().to_string(),
            &warnings,
            expected_warnings.clone(),
        );
        tariff
    };

    let config = super::Config::from(input);
    let timezone = config.timezone;

    let cdr_json = {
        let report = super::cdr_from_tariff(&tariff, &config).unwrap_or_else(|err| {
            panic!("Unable to generate a CDR:\n{err:#?}");
        });
        let (report, warnings) = report.into_parts();
        warning::test::assert_warnings(
            &test_run_file_path.display().to_string(),
            &warnings,
            expected_warnings,
        );

        let cdr_json = partial_to_cdr(report.partial_cdr);
        serde_json::to_string_pretty(&cdr_json).unwrap()
    };

    let tariff_source = price::TariffSource::Override(vec![tariff]);

    let cdr = json::parse_object(&cdr_json).unwrap();
    let cdr = cdr::build(cdr, VERSION);
    test::assert_no_unexpected_fields(&cdr);
    debug!("The CDR generated is:\n{}", cdr.as_json_str());

    let report = price::cdr(&cdr, tariff_source, timezone).unwrap_report(cdr.as_json_str());
    let (report, warnings) = report.into_parts();

    assert!(
        warnings.is_empty(),
        "Generating the CDR from a tariff has warnings;\n{:?}",
        warnings.path_id_map()
    );

    debug!("The CDR's report is:\n{:#?}", report);

    let price::Report {
        periods: _,
        tariff_used,
        tariff_reports: _,
        timezone: _,
        billed_charging_time: _,
        billed_energy: _,
        billed_idle_time: _,
        total_charging_time,
        total_energy,
        total_idle_time,
        total_time: _,
        total_cost,
        total_energy_cost,
        total_fixed_cost: _,
        total_idle_cost,
        total_charging_time_cost: _,
    } = report;

    {
        let expected_currency = match expected_currency {
            Expectation::Present(value) => {
                let code = value.expect_value();
                currency::Code::from_alpha_3_str(&code)
            }
            // Default to `Eur` if the `currency` field is not defined.
            Expectation::Absent => currency::Code::Eur,
        };

        assert_eq!(
            tariff_used.currency,
            expected_currency,
            "Expected `{expected_currency}` currency for tariff test_run `{}`",
            test_run_file_path.display()
        );
    }

    assert_req_total_field!(expected_total_cost, total_cost);
    assert_req_total_field!(expected_total_energy_cost, total_energy_cost);
    assert_req_total_field!(expected_total_idle_cost, total_idle_cost);
    assert_req_total_field!(expected_total_energy, total_energy);

    {
        let total_charging_time = total_charging_time.map(Hms);
        assert_req_optional_field!(expected_total_charging_time, total_charging_time);
    }

    {
        let Total { cdr, calculated } = total_idle_time;
        let total_idle_time = Total {
            cdr: cdr.map(Hms),
            calculated: calculated.map(Hms),
        };
        assert_req_total_field!(expected_total_idle_time, total_idle_time);
    }
}

/// Each `test_run.json` file in the `test_data/popular` directories results in a test run of
/// the `should_price_cdr_generated_from_tariff` function.
#[derive(serde::Deserialize)]
struct TestRun {
    /// The input config for this test run.
    /// This is converted into a `generate::Config` and given to the `cdr::generate_from_tariff` function.
    input: Input,

    /// The expectation for the priced CDR generated from the tariff.
    expect: Expect,
}

/// The config for generating a CDR from a tariff.
///
/// This is read in from a `config.json` file in the `test_data/popular` directories.
/// Each config file triggers a test run on the `should_price_cdr_generated_from_tariff` function.
#[derive(serde::Deserialize)]
struct Input {
    /// The timezone of the EVSE: The timezone where the chargesession took place.
    timezone: String,

    /// The start date of the generated CDR.
    end_date_time: String,

    /// The maximum DC current that can be delivered to the battery.
    max_current_supply_amp: Decimal,

    /// The amount of energy(kWh) the requested to be delivered.
    ///
    /// We don't model charging curves for the battery, so we don't care about the existing change of
    /// the battery.
    requested_kwh: Decimal,

    /// The maximum DC power(kw) that can be delivered to the battery.
    ///
    /// This is modeled as a DC system as we don't care if the delivery medium is DC or one of the
    /// various AC forms. We only care what the effective DC power is. The caller of `cdr_from_tariff`
    /// should convert the delivery medium into a DC kw power by using a power factor.
    ///
    /// In practice the maximum power bottleneck is either the EVSE, the cable or the battery itself.
    /// But whatever the bottleneck is, the caller should work that out and set the maximum expected.
    max_power_supply_kw: Decimal,

    /// The start date of the generated CDR.
    start_date_time: String,
}

#[derive(serde::Deserialize)]
struct Expect {
    /// The currency used in the tariff.
    #[serde(default)]
    currency: Expectation<String>,

    /// The expected `total_cost`.
    #[serde(default)]
    total_cost: Expectation<TolerancePrice>,

    /// The expected `total_energy_cost`.
    #[serde(default)]
    total_energy_cost: Expectation<TolerancePrice>,

    /// The expected `total_idle_cost`.
    #[serde(default)]
    total_idle_cost: Expectation<TolerancePrice>,

    /// The expected `total_charging_time`.
    #[serde(default)]
    total_charging_time: Expectation<ToleranceHms>,

    /// The expected `total_idle_time`.
    #[serde(default)]
    total_idle_time: Expectation<ToleranceHms>,

    /// The expected `total_energy`.
    #[serde(default)]
    total_energy: Expectation<ToleranceKwh>,

    #[serde(default)]
    warnings: Expectation<BTreeMap<String, Vec<String>>>,
}

/// An expected value with a given tolerance.
trait Tolerance<V, T> {
    /// Return the value.
    fn value(&self) -> V;

    /// Return the tolerance.
    fn tolerance(&self) -> T;
}

/// An expected `Price` with a given tolerance.
#[derive(Debug, serde::Deserialize)]
struct TolerancePrice {
    value: crate::Price,
    tolerance: Decimal,
}

impl Tolerance<crate::Price, Decimal> for TolerancePrice {
    fn value(&self) -> crate::Price {
        self.value
    }

    fn tolerance(&self) -> Decimal {
        self.tolerance
    }
}

/// An expected kWh amount with a given tolerance.
#[derive(Debug, serde::Deserialize)]
struct ToleranceKwh {
    /// The value in kWh.
    value: Decimal,

    /// The tolerance in kWh.
    tolerance_kwh: Decimal,
}

impl Tolerance<Kwh, Decimal> for ToleranceKwh {
    fn value(&self) -> Kwh {
        self.value.into()
    }

    fn tolerance(&self) -> Decimal {
        self.tolerance_kwh
    }
}

/// An expected duration in `HH:MM::SS` with a given tolerance.
#[derive(Debug, serde::Deserialize)]
struct ToleranceHms {
    value: Hms,
    tolerance_sec: u32,
}

impl Tolerance<Hms, i64> for ToleranceHms {
    fn value(&self) -> Hms {
        self.value
    }

    fn tolerance(&self) -> i64 {
        self.tolerance_sec.into()
    }
}

impl From<Input> for super::Config {
    fn from(config: Input) -> Self {
        let Input {
            timezone,
            end_date_time,
            max_current_supply_amp,
            requested_kwh,
            max_power_supply_kw,
            start_date_time,
        } = config;

        super::Config {
            timezone: chrono_tz::Tz::from_str(&timezone).unwrap(),
            end_date_time: DateTime::<Utc>::from_str(&end_date_time).unwrap(),
            max_current_supply_amp,
            requested_kwh,
            max_power_supply_kw,
            start_date_time: DateTime::<Utc>::from_str(&start_date_time).unwrap(),
        }
    }
}

/// Create a CDR from a the `PartialCdr` returned from the `generate` feature.
/// The CDR is then priced.
#[expect(
    clippy::similar_names,
    reason = "evse_id and evse_uid are well known terms"
)]
fn partial_to_cdr(partial_cdr: super::PartialCdr) -> serde_json::Value {
    const DEFAULT_COUNTRY_CODE: country::Code = country::Code::Nl;
    const DEFAULT_PARTY_ID: &str = "ENE";

    // The bytes lengths of various fields.
    const CONNECTOR_ID_LEN: usize = 36;
    const EVSE_ID_LEN: usize = 48;
    const EVSE_UID_LEN: usize = 36;
    const ID_MAX_LEN: usize = 39;
    const LOCATION_ID_LEN: usize = 36;
    const TOKEN_CONTRACT_UID_LEN: usize = 36;
    const TOKEN_UID_LEN: usize = 36;

    let super::PartialCdr {
        currency_code,
        party_id,
        start_date_time,
        end_date_time,
        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,
    } = partial_cdr;

    let (country_code, party_id): (country::Code, &str) = party_id
        .as_ref()
        .map(|CpoId { country_code, id }| (*country_code, id.as_str()))
        .unwrap_or_else(|| (DEFAULT_COUNTRY_CODE, DEFAULT_PARTY_ID));
    let id = random_alpha_num_string(ID_MAX_LEN);
    let start_date_time = start_date_time.to_rfc3339();
    let end_date_time = end_date_time.to_rfc3339();
    let token_uid = random_alpha_num_string(TOKEN_UID_LEN);
    let token_contract_id = random_alpha_num_string(TOKEN_CONTRACT_UID_LEN);
    let location_id = random_alpha_num_string(LOCATION_ID_LEN);
    let evse_uid = random_alpha_num_string(EVSE_UID_LEN);
    let evse_id = random_alpha_num_string(EVSE_ID_LEN);
    let connector_id = random_alpha_num_string(CONNECTOR_ID_LEN);
    let total_energy = total_energy
        .map(Decimal::from)
        .unwrap_or_else(Decimal::zero);
    let total_parking_time = total_idle_duration
        .as_ref()
        .map(ToHoursDecimal::to_hours_dec_in_ocpi_precision)
        .unwrap_or_default();
    let total_time = total_charging_duration
        .and_then(|cd| {
            cd.checked_add(&total_idle_duration.unwrap_or_default())
                .map(|td| ToHoursDecimal::to_hours_dec_in_ocpi_precision(&td))
        })
        .unwrap();
    let total_cost = total_cost.map(Price::from).unwrap_or_default();
    let total_energy_cost = total_energy_cost.map(Price::from).unwrap_or_default();
    let total_fixed_cost = total_fixed_cost.map(Price::from).unwrap_or_default();
    let total_idle_duration_cost = total_idle_duration_cost
        .map(Price::from)
        .unwrap_or_default();
    let total_charging_duration_cost = total_charging_duration_cost
        .map(Price::from)
        .unwrap_or_default();
    let last_updated = Utc::now().to_rfc3339();
    let charging_periods = charging_periods
        .into_iter()
        .map(ChargingPeriod::from)
        .collect::<Vec<_>>();

    // See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>
    serde_json::json!({
        "country_code": country_code.into_alpha_2_str(),
        "party_id": party_id,
        "id": id,
        "start_date_time": start_date_time,
        "end_date_time": end_date_time,
        "cdr_token": {
          "country_code": country_code.into_alpha_2_str(),
          "party_id": party_id,
          "uid": token_uid,
          "contract_id": token_contract_id,
          "type": "RFID"
        },
        "auth_method": "whitelist",
        "cdr_location": {
            "id": location_id,
            "address": "[address]",
            "city": "[city]",
            "country": country_code.into_alpha_3_str(),
            "coordinates": {
                "latitude": "50.770774",
                "longitude": "-126.104965"
            },
            "evse_uid": evse_uid,
            "evse_id": evse_id,
            "connector_id": connector_id,
            "connector_standard": "IEC_62196_T2",
            "connector_format": "SOCKET",
            "connector_power_type": "AC_1_PHASE"
        },
        "currency": currency_code.into_str(),
        "total_energy": total_energy,
        "total_time": total_time,
        "total_parking_time": total_parking_time,
        "total_cost": total_cost,
        "total_energy_cost": total_energy_cost,
        "total_fixed_cost": total_fixed_cost,
        "total_parking_cost": total_idle_duration_cost,
        "total_time_cost": total_charging_duration_cost,
        "last_updated": last_updated,
        "charging_periods": charging_periods
    })
}

/// Return a random string of the desired length.
///
/// This is used to create the various dummy ids for the Cdr under test.
fn random_alpha_num_string(len: usize) -> String {
    const ALPHA_NUM_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                                    abcdefghijklmnopqrstuvwxyz\
                                    0123456789";
    const ALPHA_NUM_LEN: usize = ALPHA_NUM_CHARS.len();

    let mut rng = rand::rng();

    std::iter::repeat_with(|| {
        let idx = rng.random_range(0..ALPHA_NUM_LEN);
        char::from(ALPHA_NUM_CHARS[idx])
    })
    .take(len)
    .collect()
}

// ***********************************************************************************************
// The Serializable structs below are specialized copies of the general purpose CDR implementation
// as we only use `serde` in tests.
// ***********************************************************************************************

/// A single charging period, containing a nonempty list of charge dimensions.
///
/// * See: [OCPI spec 2.2.1: CDR ChargingPeriod](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#146-chargingperiod-class>)
#[derive(serde::Serialize)]
struct ChargingPeriod {
    /// Start timestamp of the charging period. This period ends when a next period starts, the
    /// last period ends when the session ends.
    start_date_time: String,

    /// List of relevant values for this charging period.
    dimensions: Vec<Dimension>,

    /// Unique identifier of the Tariff that is relevant for this Charging Period. If not provided, no Tariff is relevant during this period.
    tariff_id: Option<String>,
}

impl From<super::ChargingPeriod> for ChargingPeriod {
    fn from(value: super::ChargingPeriod) -> Self {
        let super::ChargingPeriod {
            start_date_time,
            dimensions,
            tariff_id,
        } = value;
        Self {
            start_date_time: start_date_time.to_rfc3339(),
            dimensions: dimensions.into_iter().map(Dimension::from).collect(),
            tariff_id,
        }
    }
}

/// The volume that has been consumed for a specific dimension during a charging period.
///
/// * See: [OCPI spec 2.2.1: CDR Dimension](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#mod_cdrs_cdrdimension_class>)
#[derive(serde::Serialize)]
struct Dimension {
    #[serde(rename = "type")]
    /// The dimension type.
    dimension_type: String,

    /// Volume of the dimension consumed, measured according to the dimension type.
    volume: Decimal,
}

impl From<super::Dimension> for Dimension {
    fn from(value: super::Dimension) -> Self {
        let super::Dimension {
            dimension_type,
            volume,
        } = value;
        Self {
            dimension_type: dimension_type.to_string(),
            volume: volume.round_to_ocpi_scale(),
        }
    }
}

/// A price consisting of a value including VAT, and a value excluding VAT.
#[derive(serde::Serialize, Default)]
struct Price {
    /// The price excluding VAT.
    excl_vat: f64,

    /// The price including VAT.
    ///
    /// If no vat is applicable this value will be equal to the `excl_vat`.
    ///
    /// If no vat could be determined this value will be `None`.
    /// The v211 tariffs can't determine VAT.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    incl_vat: Option<f64>,
}

impl From<money::Price> for Price {
    fn from(value: money::Price) -> Self {
        use rust_decimal::prelude::ToPrimitive as _;

        let money::Price { excl_vat, incl_vat } = value;
        Self {
            excl_vat: Decimal::from(excl_vat).to_f64().unwrap(),
            incl_vat: incl_vat.map(|d| Decimal::from(d).to_f64().unwrap()),
        }
    }
}

impl fmt::Display for super::DimensionType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            super::DimensionType::Energy => f.write_str("ENERGY"),
            super::DimensionType::MaxCurrent => f.write_str("MAX_CURRENT"),
            super::DimensionType::MinCurrent => f.write_str("MIN_CURRENT"),
            super::DimensionType::MaxPower => f.write_str("MAX_POWER"),
            super::DimensionType::MinPower => f.write_str("MIN_POWER"),
            super::DimensionType::ParkingTime => f.write_str("PARKING_TIME"),
            super::DimensionType::ReservationTime => f.write_str("RESERVATION_TIME"),
            super::DimensionType::Time => f.write_str("TIME"),
        }
    }
}
