#![allow(
    clippy::unwrap_in_result,
    reason = "unwraps are allowed anywhere in tests"
)]
#![allow(
    clippy::arithmetic_side_effects,
    reason = "tests are allowed have arithmetic_side_effects"
)]

use std::{
    env, fmt,
    io::{self, IsTerminal as _},
    path::Path,
    sync::Once,
};

use chrono::{DateTime, Utc};
use serde::{
    de::{value::StrDeserializer, IntoDeserializer as _},
    Deserialize,
};
use tracing::debug;
use tracing_subscriber::util::SubscriberInitExt as _;

use crate::{datetime, json, schema, string, Caveat, Version};

/// The type of OCPI objects that can be parsed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ObjectType {
    /// An OCPI Charge Detail Record.
    ///
    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
    Cdr,

    /// An OCPI tariff.
    ///
    /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>).
    Tariff,
}

impl fmt::Display for ObjectType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ObjectType::Cdr => f.write_str("CDR"),
            ObjectType::Tariff => f.write_str("tariff"),
        }
    }
}
/// Creates and sets the default tracing subscriber if not already done.
#[track_caller]
pub fn setup() {
    static INITIALIZED: Once = Once::new();

    INITIALIZED.call_once_force(|state| {
        if state.is_poisoned() {
            return;
        }

        let is_tty = io::stderr().is_terminal();

        let level = match env::var("RUST_LOG") {
            Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
            Err(err) => match err {
                env::VarError::NotPresent => tracing::Level::INFO,
                env::VarError::NotUnicode(_) => {
                    panic!("`RUST_LOG` is not unicode");
                }
            },
        };

        let subscriber = tracing_subscriber::fmt()
            .with_ansi(is_tty)
            .with_file(true)
            .with_level(false)
            .with_line_number(true)
            .with_max_level(level)
            .with_target(false)
            .with_test_writer()
            .without_time()
            .finish();

        subscriber
            .try_init()
            .expect("Init tracing_subscriber::Subscriber");
    });
}

/// Return `Ok` with the text contained in the file, if the file exists.
/// Otherwise, return `Err`.
pub fn read_file_content(file_path: &Path) -> io::Result<String> {
    let mut content = std::fs::read_to_string(file_path)?;
    json_strip_comments::strip(&mut content)?;
    Ok(content)
}

/// Make it easy to create a `ReasonableLen` in test code.
impl<'buf> From<&'buf str> for string::ReasonableLen<'buf> {
    fn from(value: &'buf str) -> Self {
        string::ReasonableLen::new(value).unwrap()
    }
}

/// Approximately compares two objects in tests.
///
/// We need to approximately compare values in tests as we are not concerned with bitwise
/// accuracy. Only that the values are equal within an object specific tolerance.
///
/// # Examples
///
/// - A `Money` object considers an amount equal if there is only 2 cent difference.
/// - A `HoursDecimal` object considers a duration equal if there is only 3 second difference.
pub trait ApproxEq<Rhs = Self> {
    type Tolerance;

    fn default_tolerance() -> Self::Tolerance;

    #[must_use]
    fn approx_eq(&self, other: &Rhs) -> bool {
        self.approx_eq_tolerance(other, Self::default_tolerance())
    }

    #[must_use]
    fn approx_eq_tolerance(&self, other: &Rhs, tolerance: Self::Tolerance) -> bool;
}

impl<T> ApproxEq for Option<T>
where
    T: ApproxEq,
{
    type Tolerance = T::Tolerance;

    fn default_tolerance() -> Self::Tolerance {
        T::default_tolerance()
    }

    fn approx_eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Some(a), Some(b)) => a.approx_eq(b),
            (None, None) => true,
            _ => false,
        }
    }

    fn approx_eq_tolerance(&self, other: &Self, tolerance: Self::Tolerance) -> bool {
        match (self, other) {
            (Some(a), Some(b)) => a.approx_eq_tolerance(b, tolerance),
            (None, None) => true,
            _ => false,
        }
    }
}

/// A type for a specific OCPI [`Version`].
pub trait VersionedType: fmt::Debug {
    /// The OCPI `Version` of this type.
    const VERSION: Version;
}

/// A Field in the expect JSON.
///
/// We need to distinguish between a field that's present and null and absent.
#[derive(Clone, Debug, Default)]
pub(crate) enum Expectation<T> {
    /// The field is present.
    Present(ExpectValue<T>),

    /// The field is not present.
    #[default]
    Absent,
}

/// The value of an expectation field.
#[derive(Clone, Debug)]
pub(crate) enum ExpectValue<T> {
    /// The field has a value.
    Some(T),

    /// The field is set to `null`.
    Null,
}

impl<T> ExpectValue<T>
where
    T: fmt::Debug,
{
    /// Convert the expectation into an `Option`.
    pub fn into_option(self) -> Option<T> {
        match self {
            Self::Some(v) => Some(v),
            Self::Null => None,
        }
    }

    /// Consume the expectation and return the inner value of type `T`.
    ///
    /// # Panics
    ///
    /// Panics if the `FieldValue` is `Null`.
    #[track_caller]
    pub fn expect_value(self) -> T {
        match self {
            ExpectValue::Some(v) => v,
            ExpectValue::Null => panic!("the field expects a value"),
        }
    }
}

impl<'de, T> Deserialize<'de> for Expectation<T>
where
    T: Deserialize<'de>,
{
    #[expect(clippy::unwrap_in_result, reason = "This is test util code")]
    #[track_caller]
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = serde_json::Value::deserialize(deserializer)?;

        if value.is_null() {
            return Ok(Expectation::Present(ExpectValue::Null));
        }

        let v = T::deserialize(value).unwrap();
        Ok(Expectation::Present(ExpectValue::Some(v)))
    }
}

/// The content and name of an `expect` file.
///
/// An `expect` file contains expectations for tests.
pub(crate) struct ExpectFile<T> {
    // The value of the `expect` file.
    //
    // When the file is read from disk, the value will be a `String`.
    // This `String` will then be parsed into structured data ready for use in a test.
    pub value: Option<T>,

    // The name of the `expect` file.
    //
    // This is written into panic messages.
    pub expect_file_name: String,
}

impl ExpectFile<String> {
    pub fn as_deref(&self) -> ExpectFile<&str> {
        ExpectFile {
            value: self.value.as_deref(),
            expect_file_name: self.expect_file_name.clone(),
        }
    }
}

impl<T> ExpectFile<T> {
    pub fn with_value(value: Option<T>, file_name: &str) -> Self {
        Self {
            value,
            expect_file_name: file_name.to_owned(),
        }
    }

    pub fn only_file_name(file_name: &str) -> Self {
        Self {
            value: None,
            expect_file_name: file_name.to_owned(),
        }
    }
}

pub(crate) trait IntoFields<E> {
    fn into_fields(self) -> E;
}

/// Create a `DateTime` from an RFC 3339 formatted string.
#[track_caller]
pub fn datetime_from_str(s: &str) -> DateTime<Utc> {
    let de: StrDeserializer<'_, serde::de::value::Error> = s.into_deserializer();
    datetime::test::deser_to_utc(de).unwrap()
}

/// Try to read an expectation JSON file based on the name of the given object JSON file.
///
/// If the JSON object file is called `cdr.json` with a feature of `price` an expectation file
/// called `output_price__cdr.json` is searched for.
#[track_caller]
pub fn read_expect_json(json_file_path: &Path, feature: &str) -> ExpectFile<String> {
    let json_dir = json_file_path
        .parent()
        .expect("The given file should live in a dir");

    let json_file_name = json_file_path
        .file_stem()
        .expect("The `json_file_path` should be a file")
        .to_str()
        .expect("The `json_file_path` should have a valid name");

    // An underscore is prefixed to the filename to exclude the file from being included
    // as input for a `test_each` glob driven test.
    let expect_file_name = format!("output_{feature}__{json_file_name}.json");

    debug!("Try to read expectation file: `{expect_file_name}`");

    let json = read_file_content(&json_dir.join(&expect_file_name)).ok();

    debug!("Successfully Read expectation file: `{expect_file_name}`");
    ExpectFile {
        value: json,
        expect_file_name,
    }
}

/// Parse the JSON from disk into structured data ready for use in a test.
///
/// The input and output have an `ExpectFile` wrapper so the `expect_file_name` can
/// potentially be used in panic messages.
#[track_caller]
pub fn parse_expect_json<'de, T>(json: ExpectFile<&'de str>) -> ExpectFile<T>
where
    T: Deserialize<'de>,
{
    let ExpectFile {
        value,
        expect_file_name,
    } = json;
    let value = value.map(|json| {
        serde_json::from_str(json)
            .unwrap_or_else(|_| panic!("Unable to parse expect JSON `{expect_file_name}`"))
    });
    ExpectFile {
        value,
        expect_file_name: expect_file_name.clone(),
    }
}

#[track_caller]
pub fn assert_no_unexpected_fields<T>(object: &Caveat<T, schema::Warning>)
where
    T: crate::Versioned,
{
    const MAX_FIELD_DISPLAY: usize = 20;

    let warnings = object.warnings();
    let object_type = std::any::type_name::<T>();
    let unexpected_fields = warnings.unexpected_fields();

    if unexpected_fields.is_empty() {
        return;
    }

    if unexpected_fields.len() > MAX_FIELD_DISPLAY {
        let truncated_fields = unexpected_fields
            .iter()
            .take(MAX_FIELD_DISPLAY)
            .copied()
            .collect::<Vec<_>>();

        panic!(
            "The {object_type} has `{}` unexpected fields;\n\
                    displaying the first ({}):\n{}\n... and {} more",
            unexpected_fields.len(),
            truncated_fields.len(),
            truncated_fields
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(",\n"),
            unexpected_fields.len() - truncated_fields.len(),
        )
    } else {
        panic!(
            "The {object_type} has `{}` unexpected fields unexpected fields:\n{}",
            unexpected_fields.len(),
            unexpected_fields.to_strings().join(",\n")
        )
    };
}

#[track_caller]
pub fn assert_unexpected_fields<T>(object: &Caveat<T, schema::Warning>, expected: &[&'static str])
where
    T: crate::Versioned,
{
    let warnings = object.warnings();
    let object_name = std::any::type_name::<T>();
    let unexpected_fields = warnings.unexpected_fields();

    if unexpected_fields.len() != expected.len() {
        let unexpected_fields = unexpected_fields
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>();

        panic!(
            "The {object_name}'s' unexpected fields and the expected fields lists have different lengths.\n\nUnexpected fields found:\n{}",
            unexpected_fields.join(",\n")
        );
    }

    let unmatched_paths = unexpected_fields
        .iter()
        .zip(expected.iter())
        .filter(|(a, b)| a.as_str() != **b)
        .collect::<Vec<_>>();

    if !unmatched_paths.is_empty() {
        let unmatched_paths = unmatched_paths
            .into_iter()
            .map(|(a, b)| format!("{a} != {b}"))
            .collect::<Vec<_>>();

        panic!(
            "The {object_name}'s' unexpected fields don't match the expected fields.\n\nUnexpected fields found:\n{}",
            unmatched_paths.join(",\n")
        );
    }
}

/// The `unexpected_fields` should be empty. If not then panic with a truncated list of the fields.
#[track_caller]
#[expect(
    clippy::arithmetic_side_effects,
    reason = "subtraction is safe: we only reach this branch when len > MAX_FIELD_DISPLAY >= truncated len"
)]
pub(crate) fn expect_no_unexpected_fields(
    expect_file_name: &str,
    unexpected_fields: &json::PathSet<'_>,
) {
    if !unexpected_fields.is_empty() {
        const MAX_FIELD_DISPLAY: usize = 20;

        if unexpected_fields.len() > MAX_FIELD_DISPLAY {
            let truncated_fields = unexpected_fields
                .iter()
                .take(MAX_FIELD_DISPLAY)
                .copied()
                .collect::<Vec<_>>();

            panic!(
                "The expect file `{expect_file_name}` didn't expect `{}` unexpected fields;\n\
                    displaying the first ({}):\n{}\n... and {} more",
                unexpected_fields.len(),
                truncated_fields.len(),
                truncated_fields
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join(",\n"),
                unexpected_fields.len() - truncated_fields.len(),
            )
        } else {
            panic!(
                "The expect file `{expect_file_name}` didn't expect `{}` unexpected fields:\n{}",
                unexpected_fields.len(),
                unexpected_fields.to_strings().join(",\n")
            )
        };
    }
}

/// Compare the `unexpected_fields` to the expected fields globs.
///
/// Panic if there are any fields not expected.
#[track_caller]
pub(crate) fn expect_unexpected_fields(
    expect_file_name: &str,
    unexpected_fields: &mut json::PathSet<'_>,
    expected: Expectation<Vec<json::test::PathGlob>>,
) {
    if let Expectation::Present(expectation) = expected {
        let unexpected_fields_expect = expectation.expect_value();

        // Remove any fields that match the expected glob.
        // The remaining fields are truly unexpected.
        for expect_glob in unexpected_fields_expect {
            unexpected_fields.filter_matches(&expect_glob);
        }

        expect_no_unexpected_fields(expect_file_name, unexpected_fields);
    } else {
        expect_no_unexpected_fields(expect_file_name, unexpected_fields);
    }
}

/// This code is copied from the std lib `assert_eq!` and modified for use with `ApproxEq`.
#[macro_export]
macro_rules! assert_approx_eq {
    ($left:expr, $right:expr $(,)?) => ({
        use $crate::test::ApproxEq;

        match (&$left, &$right) {
            (left_val, right_val) => {
                if !((*left_val).approx_eq(&*right_val)) {
                    let left = stringify!($left);
                    let right = stringify!($right);

                    panic!(
                        "assertion `{left} == {right}` failed\n\
                        \tleft: {left_val:?}\n\
                        \tright: {right_val:?}"
                    );
                }
            }
        }
    });
    ($left:expr, $right:expr, $($arg:tt)+) => ({
        use $crate::test::ApproxEq;

        match (&$left, &$right) {
            (left_val, right_val) => {
                if !((*left_val).approx_eq(&*right_val)) {
                    let left = stringify!($left);
                    let right = stringify!($right);

                    panic!(
                        "assertion `{left} == {right}` failed: {}\n\
                        \tleft: {left_val:?}\n\
                        \tright: {right_val:?}",
                        std::format_args!($($arg)+)
                    );
                }
            }
        }
    });
}

/// This code is copied from the std lib `assert_eq!` and modified for use with `ApproxEq`.
#[macro_export]
macro_rules! assert_approx_eq_tolerance {
    ($left:expr, $right:expr, $tolerance:expr $(,)?) => ({
        use $crate::test::ApproxEq;

        match (&$left, &$right) {
            (left_val, right_val) => {
                if !((*left_val).approx_eq_tolerance(&*right_val, $tolerance)) {
                    let left = stringify!($left);
                    let right = stringify!($right);

                    panic!(
                        "assertion `{left} ~= {right}` failed with tolerance `{}`\n\
                        \t{left}: {left_val:?}\n\
                        \t{right}: {right_val:?}",
                        $tolerance
                    );
                }
            }
        }
    });
    ($left:expr, $right:expr, $tolerance:expr, $($arg:tt)+) => ({
        use $crate::test::ApproxEq;

        match (&$left, &$right) {
            (left_val, right_val) => {
                if !((*left_val).approx_eq_tolerance(&*right_val, $tolerance)) {
                    let left = stringify!($left);
                    let right = stringify!($right);

                    panic!(
                        "assertion `{left} ~= {right}` failed with tolerance `{}`: {}\n\
                        \t{left}: {left_val:?}\n\
                        \t{right}: {right_val:?}",
                        $tolerance,
                        std::format_args!($($arg)+)
                    );
                }
            }
        }
    });
}
