#![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
#![allow(clippy::panic, reason = "tests are allowed panic")]

use std::collections::{BTreeMap, BTreeSet};

use chrono::TimeDelta;
use rust_decimal::Decimal;
use serde::Deserialize;
use tracing::debug;

use super::{Report, TariffReport, Total, Warning};
use crate::{
    assert_approx_eq,
    duration::ToHoursDecimal,
    json,
    number::{self, IsZero as _},
    test::{self, ApproxEq as _, ExpectFile, Expectation},
    timezone,
    warning::{self, Warning as _},
    Caveat, Kwh, Price,
};

// Decimal precision used when comparing the outcomes of the calculation with the CDR.
const PRECISION: u32 = 2;

#[test]
const fn warning_kind_should_be_send_and_sync() {
    const fn f<T: Send + Sync>() {}

    f::<Warning>();
}

pub trait UnwrapReport {
    #[track_caller]
    fn unwrap_report(self, json_source: &str) -> Caveat<Report, Warning>;
}

impl UnwrapReport for super::Verdict<Report> {
    fn unwrap_report(self, json_source: &str) -> Caveat<Report, Warning> {
        match self {
            Ok(v) => v,
            Err(set) => {
                let (error, _warnings) = set.into_parts();
                let warning::test::ErrorSourceContext {
                    context: _,
                    element_path,
                    element_position,
                    error,
                } = error.into_context(json_source).unwrap();
                let json_source = serde_json::from_str::<serde_json::Value>(json_source).unwrap();
                panic!(
                    "Unable to price the CDR due to an error at path `{element_path}`:\n{error}\n{}",
                    json::test::LineHighlighter::from_value(&json_source, element_position)
                );
            }
        }
    }
}

/// A `TimeDelta` wrapper used to serialize and deserialize to/from a `Decimal` representation of hours.
#[derive(Debug, Default)]
pub(crate) struct HoursDecimal(Decimal);

impl ToHoursDecimal for HoursDecimal {
    fn to_hours_dec(&self) -> Decimal {
        self.0
    }
}

/// Deserialize bytes into a `Decimal` applying the scale defined in the OCPI spec.
///
/// Called from the `impl Deserialize` for a `Decimal` newtype.
fn decimal<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::Deserialize;

    let mut d = <Decimal as Deserialize>::deserialize(deserializer)?;
    d.rescale(number::SCALE);
    Ok(d)
}

impl<'de> Deserialize<'de> for HoursDecimal {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        decimal(deserializer).map(Self)
    }
}

#[derive(serde::Deserialize)]
pub(crate) struct Expect {
    /// Expectations for the result of calling `timezone::find_or_infer`.
    pub timezone_find: Option<timezone::test::FindOrInferExpect>,

    /// Expectations for the unexpected fields reported by validation.
    pub tariff_parse: Option<ParseExpect>,

    /// Expectations for the unexpected fields reported by validation.
    pub cdr_parse: Option<ParseExpect>,

    /// Expectations for the result of calling `cdr::price*`.
    pub cdr_price: Option<PriceExpect>,
}

/// The `Expect` is used to parse the JSON but the tests use the individual fields in separate
/// asset functions. Each of those functions needs to know the `expect_file_name`.
#[expect(
    clippy::struct_field_names,
    reason = "When deconstructed these fields will always be called *_expect. This avoids having to rename them in-place."
)]
pub(crate) struct ExpectFields {
    /// Expectations for the result of calling `timezone::find_or_infer`.
    pub timezone_find_expect: ExpectFile<timezone::test::FindOrInferExpect>,

    /// Expectations for the unexpected fields reported by validation.
    pub tariff_parse_expect: ExpectFile<ParseExpect>,

    /// Expectations for the unexpected fields reported by validation.
    pub cdr_parse_expect: ExpectFile<ParseExpect>,

    /// Expectations for the result of calling `cdr::price*`.
    pub cdr_price_expect: ExpectFile<PriceExpect>,
}

impl test::IntoFields<ExpectFields> for ExpectFile<Expect> {
    /// Split the `ExpectFile<Expect>` into its constituent fields and repackage them as `ExpectFile`s.
    fn into_fields(self) -> ExpectFields {
        let ExpectFile {
            value,
            expect_file_name,
        } = self;

        match value {
            Some(expect) => {
                let Expect {
                    timezone_find,
                    tariff_parse,
                    cdr_parse,
                    cdr_price,
                } = expect;
                ExpectFields {
                    timezone_find_expect: ExpectFile::with_value(timezone_find, &expect_file_name),
                    tariff_parse_expect: ExpectFile::with_value(tariff_parse, &expect_file_name),
                    cdr_parse_expect: ExpectFile::with_value(cdr_parse, &expect_file_name),
                    cdr_price_expect: ExpectFile::with_value(cdr_price, &expect_file_name),
                }
            }
            None => ExpectFields {
                timezone_find_expect: ExpectFile::only_file_name(&expect_file_name),
                tariff_parse_expect: ExpectFile::only_file_name(&expect_file_name),
                cdr_parse_expect: ExpectFile::only_file_name(&expect_file_name),
                cdr_price_expect: ExpectFile::only_file_name(&expect_file_name),
            },
        }
    }
}

#[track_caller]
pub(crate) fn assert_parse_report(
    object_type: test::ObjectType,
    unexpected_fields: json::PathSet<'_>,
    expect: ExpectFile<ParseExpect>,
) {
    let ExpectFile {
        value,
        expect_file_name,
    } = expect;
    let unexpected_fields_expect = value
        .map(|exp| exp.unexpected_fields)
        .unwrap_or(Expectation::Absent);

    if let Expectation::Present(expectation) = unexpected_fields_expect {
        let unexpected_fields_expect = expectation.expect_value();

        for field in unexpected_fields {
            assert!(
                unexpected_fields_expect.contains(&field.to_string()),
                "The {object_type} has an unexpected field that's not expected in `{expect_file_name}`: `{field}`"
            );
        }
    } else {
        assert!(
            unexpected_fields.is_empty(),
            "The {object_type} has unexpected fields but the expect file doesn't `{expect_file_name}`; {unexpected_fields:#?}",
        );
    }
}

#[track_caller]
pub(crate) fn assert_price_report(
    report: Caveat<Report, Warning>,
    expect: ExpectFile<PriceExpect>,
) {
    let (report, warnings) = report.into_parts();
    let Report {
        mut tariff_reports,
        periods: _,
        tariff_used,
        timezone: _,
        billed_energy: _,
        billed_idle_time: _,
        billed_charging_time: _,
        total_charging_time: _,
        total_cost,
        total_fixed_cost,
        total_time,
        total_charging_time_cost,
        total_energy,
        total_energy_cost,
        total_idle_time,
        total_idle_cost,
    } = report;

    let ExpectFile {
        value: expect,
        expect_file_name,
    } = expect;

    // This destructure isn't pretty but it's at least simple to maintain.
    // The alternative is getting involved with references of references when processing each borrowed field.
    let (
        warnings_expect,
        tariff_index_expect,
        tariff_id_expect,
        tariff_reports_expect,
        total_cost_expectation,
        total_fixed_cost_expectation,
        total_charging_time_expectation,
        total_charging_time_cost_expectation,
        total_energy_expectation,
        total_energy_cost_expectation,
        total_idle_time_expectation,
        total_idle_time_cost_expectation,
    ) = expect
        .map(|exp| {
            let PriceExpect {
                warnings,
                tariff_index,
                tariff_id,
                tariff_reports,
                total_cost,
                total_fixed_cost,
                total_charging_time,
                total_charging_time_cost,
                total_energy,
                total_energy_cost,
                total_idle_time,
                total_idle_time_cost,
            } = exp;

            (
                warnings,
                tariff_index,
                tariff_id,
                tariff_reports,
                total_cost,
                total_fixed_cost,
                total_charging_time,
                total_charging_time_cost,
                total_energy,
                total_energy_cost,
                total_idle_time,
                total_idle_time_cost,
            )
        })
        .unwrap_or((
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
            Expectation::Absent,
        ));

    if let Expectation::Present(expectation) = warnings_expect {
        let warnings_expect = expectation.expect_value();

        debug!("{warnings_expect:?}");

        for group in &warnings {
            let (element, warnings) = group.to_parts();
            let Some(warnings_expect) = warnings_expect.get(element.path.as_str()) else {
                let warning_ids = warnings
                    .iter()
                    .map(|k| format!("  \"{}\",", k.id()))
                    .collect::<Vec<_>>()
                    .join("\n");

                panic!("No warnings expected `{expect_file_name}` for `Element` at `{}` but {} warnings were reported:\n[\n{}\n]", element.path, warnings.len(), warning_ids);
            };

            let warnings_expect = warnings_expect
                .iter()
                .map(|s| &**s)
                .collect::<BTreeSet<_>>();

            for warning_kind in warnings {
                let id = warning_kind.id();
                assert!(
                    warnings_expect.contains(id.as_str()),
                    "Unexpected warning `{id}` for `Element` at `{}`",
                    element.path
                );
            }
        }
    } else {
        assert!(
            warnings.is_empty(),
            "The expectation file at `{expect_file_name}` did not expect warnings, but the CDR has warnings:\n\
            {:#?}\n\
            These warnings have the messages:\n\
            {:#?}",
            warnings.path_id_map(),
            warnings.path_msg_map()
        );
    }

    if let Expectation::Present(expectation) = tariff_reports_expect {
        let tariff_reports_expect: BTreeMap<_, _> = expectation
            .expect_value()
            .into_iter()
            .map(|TariffReportExpect { id, warnings }| (id, warnings))
            .collect();

        for report in &mut tariff_reports {
            let TariffReport { origin, warnings } = report;
            let id = &origin.id;
            let Some(warnings_expect) = tariff_reports_expect.get(id) else {
                panic!("A tariff with {id} is not expected `{expect_file_name}`");
            };

            debug!("{warnings_expect:?}");

            for (elem_path, warnings) in warnings {
                let Some(warnings_expect) = warnings_expect.get(elem_path.as_str()) else {
                    let warning_ids = warnings
                        .iter()
                        .map(|k| format!("  \"{}\",", k.id()))
                        .collect::<Vec<_>>()
                        .join("\n");

                    panic!("No warnings expected for `Element` at `{elem_path}` but {} warnings were reported:\n[\n{}\n]", warnings.len(), warning_ids);
                };

                let warnings_expect = warnings_expect
                    .iter()
                    .map(|s| &**s)
                    .collect::<BTreeSet<_>>();

                for warning_kind in warnings {
                    let id = warning_kind.id();
                    assert!(
                        warnings_expect.contains(id.as_str()),
                        "Unexpected warning `{id}` for `Element` at `{elem_path}`"
                    );
                }
            }
        }
    } else {
        for report in &tariff_reports {
            let TariffReport { origin, warnings } = report;

            let id = &origin.id;

            assert!(
                warnings.is_empty(),
                "The tariff with id `{id}` has warnings but the expect file `{expect_file_name}` has none in the `tariff_reports` map.\n {warnings:#?}"
            );
        }
    }

    if let Expectation::Present(expectation) = tariff_id_expect {
        assert_eq!(tariff_used.id, expectation.expect_value());
    }

    if let Expectation::Present(expectation) = tariff_index_expect {
        assert_eq!(tariff_used.index, expectation.expect_value());
    }

    total_cost_expectation.expect_price("total_cost", &total_cost);
    total_fixed_cost_expectation.expect_opt_price("total_fixed_cost", &total_fixed_cost);
    total_charging_time_expectation.expect_duration("total_time", &total_time);
    total_charging_time_cost_expectation
        .expect_opt_price("total_charging_time_cost", &total_charging_time_cost);
    total_energy_expectation.expect_opt_kwh("total_energy", &total_energy);
    total_energy_cost_expectation.expect_opt_price("total_energy_cost", &total_energy_cost);
    total_idle_time_expectation.expect_opt_duration("total_idle_time", &total_idle_time);
    total_idle_time_cost_expectation.expect_opt_price("total_idle_cost", &total_idle_cost);
}

/// Expectations for the unexpected fields reported by validation.
#[derive(serde::Deserialize)]
pub struct ParseExpect {
    #[serde(default)]
    unexpected_fields: Expectation<Vec<String>>,
}

/// Expectations for the result of calling `cdr::price`.
#[derive(serde::Deserialize)]
pub struct PriceExpect {
    /// Expected Warnings from parsing a CDR.
    ///
    /// Each entry in the map is an element path and a list of associated warnings.
    #[serde(default)]
    warnings: Expectation<BTreeMap<String, Vec<String>>>,

    /// Index of the tariff that was found to be active.
    #[serde(default)]
    tariff_index: Expectation<usize>,

    /// Id of the tariff that was found to be active.
    #[serde(default)]
    tariff_id: Expectation<String>,

    /// A list of the tariff IDs found in the CDR or supplied to the [`cdr::price`](crate::cdr::price) function.
    ///
    /// Each tariff may have a set of unexpected fields encountered while parsing the tariff.
    #[serde(default)]
    tariff_reports: Expectation<Vec<TariffReportExpect>>,

    /// Total sum of all the costs of this transaction in the specified currency.
    #[serde(default)]
    total_cost: Expectation<Price>,

    /// Total sum of all the fixed costs in the specified currency, except fixed price components of parking and reservation. The cost not depending on amount of time/energy used etc. Can contain costs like a start tariff.
    #[serde(default)]
    total_fixed_cost: Expectation<Price>,

    /// Total duration of the charging session (including the duration of charging and not charging), in hours.
    #[serde(default)]
    total_charging_time: Expectation<HoursDecimal>,

    /// Total sum of all the cost related to duration of charging during this transaction, in the specified currency.
    #[serde(default)]
    total_charging_time_cost: Expectation<Price>,

    /// Total energy charged, in kWh.
    #[serde(default)]
    total_energy: Expectation<Kwh>,

    /// Total sum of all the cost of all the energy used, in the specified currency.
    #[serde(default)]
    total_energy_cost: Expectation<Price>,

    /// Total duration of the charging session where the EV was not charging (no energy was transferred between EVSE and EV), in hours.
    #[serde(default)]
    total_idle_time: Expectation<HoursDecimal>,

    /// Total sum of all the cost related to parking of this transaction, including fixed price components, in the specified currency.
    #[serde(default)]
    total_idle_time_cost: Expectation<Price>,
}

#[derive(Debug, Deserialize)]
struct TariffReportExpect {
    /// The id of the tariff.
    id: String,

    /// Expected Warnings from parsing a tariff.
    ///
    /// Each entry in the map is an element path and a list of associated warnings.
    #[serde(default)]
    warnings: BTreeMap<String, Vec<String>>,
}

impl Expectation<Price> {
    #[track_caller]
    fn expect_opt_price(self, field_name: &str, total: &Total<Option<Price>>) {
        if let Expectation::Present(expect_value) = self {
            match (expect_value.into_option(), total.calculated) {
                (Some(a), Some(b)) => assert!(
                    a.approx_eq(&b),
                    "Expected `{a}` but `{b}` was calculated for `{field_name}`"
                ),
                (Some(a), None) => {
                    panic!("Expected `{a}`, but no price was calculated for `{field_name}`")
                }
                (None, Some(b)) => {
                    panic!("Expected no value, but `{b}` was calculated for `{field_name}`")
                }
                (None, None) => (),
            }
        } else {
            match (total.cdr, total.calculated) {
                (None, None) => (),
                (None, Some(calculated)) => {
                    assert!(calculated.is_zero(), "The CDR field `{field_name}` doesn't have a value but a value was calculated; calculated: {calculated}");
                }
                (Some(cdr), None) => {
                    assert!(
                            cdr.is_zero(),
                            "The CDR field `{field_name}` has a value but the calculated value is none; cdr: {cdr}"
                        );
                }
                (Some(cdr), Some(calculated)) => {
                    assert!(
                        cdr.approx_eq(&calculated),
                        "Comparing `{field_name}` field with CDR"
                    );
                }
            }
        }
    }

    #[track_caller]
    fn expect_price(self, field_name: &str, total: &Total<Price, Option<Price>>) {
        if let Expectation::Present(expect_value) = self {
            match (expect_value.into_option(), total.calculated) {
                (Some(a), Some(b)) => assert!(
                    a.approx_eq(&b),
                    "Expected `{a}` but `{b}` was calculated for `{field_name}`"
                ),
                (Some(a), None) => {
                    panic!("Expected `{a}`, but no price was calculated for `{field_name}`")
                }
                (None, Some(b)) => {
                    panic!("Expected no value, but `{b}` was calculated for `{field_name}`")
                }
                (None, None) => (),
            }
        } else if let Some(calculated) = total.calculated {
            assert!(
                total.cdr.approx_eq(&calculated),
                "CDR contains `{}` but `{}` was calculated for `{field_name}`",
                total.cdr,
                calculated
            );
        } else {
            assert!(
                    total.cdr.is_zero(),
                    "The CDR field `{field_name}` has a value but the calculated value is none; cdr: {:?}",
                    total.cdr
                );
        }
    }
}

impl Expectation<HoursDecimal> {
    #[track_caller]
    fn expect_duration(self, field_name: &str, total: &Total<TimeDelta>) {
        if let Expectation::Present(expect_value) = self {
            assert_approx_eq!(
                expect_value.expect_value().to_hours_dec(),
                total.calculated.to_hours_dec(),
                "Comparing `{field_name}` field with expectation"
            );
        } else {
            assert_approx_eq!(
                total.cdr.to_hours_dec(),
                total.calculated.to_hours_dec(),
                "Comparing `{field_name}` field with CDR"
            );
        }
    }

    #[track_caller]
    fn expect_opt_duration(
        self,
        field_name: &str,
        total: &Total<Option<TimeDelta>, Option<TimeDelta>>,
    ) {
        if let Expectation::Present(expect_value) = self {
            assert_approx_eq!(
                expect_value
                    .into_option()
                    .unwrap_or_default()
                    .to_hours_dec(),
                &total
                    .calculated
                    .as_ref()
                    .map(ToHoursDecimal::to_hours_dec)
                    .unwrap_or_default(),
                "Comparing `{field_name}` field with expectation"
            );
        } else {
            assert_approx_eq!(
                total.cdr.unwrap_or_default().to_hours_dec(),
                total.calculated.unwrap_or_default().to_hours_dec(),
                "Comparing `{field_name}` field with CDR"
            );
        }
    }
}

impl Expectation<Kwh> {
    #[track_caller]
    fn expect_opt_kwh(self, field_name: &str, total: &Total<Kwh, Option<Kwh>>) {
        if let Expectation::Present(expect_value) = self {
            assert_eq!(
                expect_value
                    .into_option()
                    .map(|kwh| kwh.round_dp(PRECISION)),
                total
                    .calculated
                    .map(|kwh| kwh.rescale().round_dp(PRECISION)),
                "Comparing `{field_name}` field with expectation"
            );
        } else {
            assert_eq!(
                total.cdr.round_dp(PRECISION),
                total
                    .calculated
                    .map(|kwh| kwh.rescale().round_dp(PRECISION))
                    .unwrap_or_default(),
                "Comparing `{field_name}` field with CDR"
            );
        }
    }
}
