//! The collection of lints that apply to all supported OCPI versions.

pub(crate) mod currency {
    use tracing::{debug, instrument};

    use crate::{
        currency,
        json::{self, FromJson as _},
        warning::{self, GatherWarnings as _, IntoCaveat as _},
        Verdict,
    };

    /// Validate the `country_code` field.
    #[instrument(skip_all)]
    pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), currency::Warning> {
        let mut warnings = warning::Set::<currency::Warning>::new();
        let code = currency::Code::from_json(elem)?.gather_warnings_into(&mut warnings);

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

        Ok(().into_caveat(warnings))
    }
}

pub(crate) mod datetime {
    use chrono::{DateTime, Utc};
    use tracing::instrument;

    use crate::{
        json::{self, FromJson as _},
        lint::tariff::Warning,
        warning::{self, GatherWarnings as _, IntoCaveat as _},
        Verdict,
    };

    /// Lint both `start_date_time` and `end_date_time`.
    ///
    /// It's allowed for the `start_date_time` to be equal to the `end_date_time` but the
    /// `start_date_time` should not be greater than the `end_date_time`.
    #[instrument(skip_all)]
    pub(crate) fn lint_start_end(
        start_date_time_elem: Option<&json::Element<'_>>,
        end_date_time_elem: Option<&json::Element<'_>>,
    ) -> Verdict<(), Warning> {
        let mut warnings = warning::Set::<Warning>::new();

        let start_date = start_date_time_elem
            .map(DateTime::<Utc>::from_json)
            .transpose()?
            .gather_warnings_into(&mut warnings);
        let end_date = end_date_time_elem
            .map(DateTime::<Utc>::from_json)
            .transpose()?
            .gather_warnings_into(&mut warnings);

        if let Some(((start, start_elem), end)) = start_date.zip(start_date_time_elem).zip(end_date)
        {
            if start > end {
                warnings.insert(start_elem, Warning::StartDateTimeIsAfterEndDateTime);
            }
        }

        Ok(().into_caveat(warnings))
    }
}

pub mod time {
    //! Linting and warning infrastructure for the `start_time` and `end_time` fields.
    //!
    //! * See: [OCPI spec 2.2.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>)
    //! * See: [OCPI spec 2.1.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#45-tariffrestrictions-class>)
    use std::fmt;

    use chrono::{NaiveTime, Timelike as _};

    use crate::{
        datetime, from_warning_all,
        json::{self, FromJson as _},
        warning::{self, GatherWarnings as _, IntoCaveat as _},
        Verdict,
    };

    const DAY_BOUNDARY: HourMin = HourMin::new(0, 0);
    const NEAR_END_OF_DAY: HourMin = HourMin::new(23, 59);

    #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
    pub enum Warning {
        /// Both `start_time` and `end_time` are defined and contain the entire day,
        /// making the restriction superfluous.
        ContainsEntireDay,

        /// The `end_time` restriction is set to `23::59`.
        ///
        /// The spec states: "To stop at end of the day use: 00:00.".
        ///
        /// * See: [OCPI spec 2.2.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>).
        EndTimeIsNearEndOfDay,

        /// The `start_time` and `end_time` are equal and so the element is never valid.
        NeverValid,

        /// Each field needs to be a valid time.
        DateTime(datetime::Warning),
    }

    impl fmt::Display for Warning {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::ContainsEntireDay => f.write_str("Both `start_time` and `end_time` are defined and contain the entire day."),
                Self::EndTimeIsNearEndOfDay => f.write_str(r#"
The `end_time` restriction is set to `23::59`.

The spec states: "To stop at end of the day use: 00:00.".

See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>"#),
                Self::NeverValid => f.write_str("The `start_time` and `end_time` are equal and so the element is never valid."),
                Self::DateTime(kind) => fmt::Display::fmt(kind, f),
            }
        }
    }

    impl crate::Warning for Warning {
        fn id(&self) -> warning::Id {
            match self {
                Self::ContainsEntireDay => warning::Id::from_static("contains_entire_day"),
                Self::EndTimeIsNearEndOfDay => {
                    warning::Id::from_static("end_time_is_near_end_of_day")
                }
                Self::NeverValid => warning::Id::from_static("never_valid"),
                Self::DateTime(kind) => kind.id(),
            }
        }
    }

    from_warning_all!(datetime::Warning => Warning::DateTime);

    /// Lint the `start_time` and `end_time` field.
    pub(crate) fn lint(
        start_time_elem: Option<&json::Element<'_>>,
        end_time_elem: Option<&json::Element<'_>>,
    ) -> Verdict<(), Warning> {
        let mut warnings = warning::Set::<Warning>::new();

        let start = elem_to_time_hm(start_time_elem, &mut warnings)?;
        let end = elem_to_time_hm(end_time_elem, &mut warnings)?;

        // If both `start_time` and `end_time` are defined, then perform range linting.
        if let Some(((start_time, start_elem), (end_time, end_elem))) = start.zip(end) {
            if end_time == NEAR_END_OF_DAY {
                warnings.insert(end_elem, Warning::EndTimeIsNearEndOfDay);
            }

            if start_time == DAY_BOUNDARY && is_day_end(end_time) {
                warnings.insert(start_elem, Warning::ContainsEntireDay);
            } else if start_time == end_time {
                warnings.insert(start_elem, Warning::NeverValid);
            }
        } else if let Some((start_time, start_elem)) = start {
            if start_time == DAY_BOUNDARY {
                warnings.insert(start_elem, Warning::ContainsEntireDay);
            }
        } else if let Some((end_time, end_elem)) = end {
            if is_day_end(end_time) {
                warnings.insert(end_elem, Warning::ContainsEntireDay);
            }
        }

        Ok(().into_caveat(warnings))
    }

    /// The time of day represented as hour and minute.
    #[derive(Copy, Clone, Eq, PartialEq)]
    struct HourMin {
        /// Hour of the day. Stored as `u32` because that's what `chrono` returns from `NaiveTime::hour()`.
        hour: u32,

        /// Minute of the hour. Stored as `u32` because that's what `chrono` returns from `NaiveTime::minute()`.
        min: u32,
    }

    impl HourMin {
        /// Create a new `HourMin` time.
        const fn new(hour: u32, min: u32) -> Self {
            Self { hour, min }
        }
    }

    /// Return true if the given time is close to or at the end of day.
    fn is_day_end(time: HourMin) -> bool {
        time == NEAR_END_OF_DAY || time == DAY_BOUNDARY
    }

    /// Return `Ok((HourMin, json::Element))` if the given [`json::Element`] is a valid [`NaiveTime`].
    fn elem_to_time_hm<'a, 'buf>(
        time_elem: Option<&'a json::Element<'buf>>,
        warnings: &mut warning::Set<Warning>,
    ) -> Result<Option<(HourMin, &'a json::Element<'buf>)>, warning::ErrorSet<Warning>> {
        let v = time_elem.map(NaiveTime::from_json).transpose()?;

        Ok(v.gather_warnings_into(warnings)
            .map(|t| HourMin {
                hour: t.hour(),
                min: t.minute(),
            })
            .zip(time_elem))
    }
}

pub mod elements {
    //! The linting and Warning infrastructure for the `elements` field.
    //!
    //! * See: [OCPI spec 2.2.1: Tariff Element](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#144-tariffelement-class>)
    //! * See: [OCPI spec 2.1.1: Tariff Element](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#43-tariffelement-class>)

    use std::{borrow::Cow, collections::HashSet, fmt};

    use tracing::instrument;

    use super::{price_components, restrictions};
    use crate::{
        from_warning_all,
        json::{self, FieldsAsExt as _},
        lint::Item,
        required_field,
        tariff::v2x::DimensionType,
        warning::{self, DeescalateError as _, GatherWarnings as _, IntoCaveat as _},
        Verdict,
    };

    #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
    pub enum Warning {
        /// The array exists but is empty.
        Empty,

        /// The given field is required.
        FieldRequired { field_name: Cow<'static, str> },

        /// The JSON value given is not an array.
        InvalidType { type_found: json::ValueKind },

        /// The last element should have no restrictions so that it acts as a catch-all case.
        MissingCatchAll,

        /// The `price_components` field is nested in the `elements` array.
        PriceComponents(price_components::Warning),

        /// The `restriction` field is nested in the `elements` array.
        Restrictions(restrictions::Warning),
    }

    impl Warning {
        fn invalid_type(elem: &json::Element<'_>) -> Self {
            Self::InvalidType {
                type_found: elem.value().kind(),
            }
        }
    }

    impl fmt::Display for Warning {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::Empty => write!(
                    f,
                    "An empty list of days means that no day is allowed. Is this what you want?"
                ),
                Self::FieldRequired { field_name } => {
                    write!(f, "Field is required: `{field_name}`")
                }
                Self::InvalidType { type_found } => {
                    write!(f, "The value should be an array but is `{type_found}`")
                }
                Self::MissingCatchAll => write!(
                    f,
                    "The last element should have no restrictions so that it catches all cases."
                ),
                Self::PriceComponents(warning) => fmt::Display::fmt(warning, f),
                Self::Restrictions(warning) => fmt::Display::fmt(warning, f),
            }
        }
    }

    impl crate::Warning for Warning {
        fn id(&self) -> warning::Id {
            match self {
                Self::Empty => warning::Id::from_static("empty"),
                Self::FieldRequired { field_name } => {
                    warning::Id::from_string(format!("field_required({field_name})"))
                }
                Self::InvalidType { type_found } => {
                    warning::Id::from_string(format!("invalid_type({type_found})"))
                }
                Self::MissingCatchAll => warning::Id::from_static("missing_catch_all"),
                Self::PriceComponents(warning) => warning.id(),
                Self::Restrictions(warning) => warning.id(),
            }
        }
    }

    from_warning_all!(
        price_components::Warning => Warning::PriceComponents,
        restrictions::Warning => Warning::Restrictions
    );

    /// Lint the `elements` field.
    ///
    /// * See: [OCPI v2.2.1 Tariff Element](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#mod_tariffs_tariffelement_class>).
    #[instrument(skip_all)]
    pub(crate) fn lint(elem: &json::Element<'_>) -> Verdict<(), Warning> {
        /// A summary of the `Element` after parsing and linting has taken place.
        #[expect(
            dead_code,
            reason = "The `ElementSummary` will be used in an upcoming analysis PR."
        )]
        struct ElementSummary {
            dimensions: HashSet<DimensionType>,
            has_restrictions: bool,
        }

        let mut warnings = warning::Set::<Warning>::new();

        // The `elements` field should be an array.
        let Some(elements) = elem.as_array() else {
            return warnings.bail(elem, Warning::invalid_type(elem));
        };

        // The `elements` array should contain at least one `Element`.
        if elements.is_empty() {
            return warnings.bail(elem, Warning::Empty);
        }

        // The elements `Vec` will be used in an upcoming analysis PR.
        let _elements = elements
            .iter()
            .map(|elem| {
                let Some(fields) = elem.as_object_fields() else {
                    warnings.insert(elem, Warning::invalid_type(elem));
                    return Item::Invalid;
                };
                let restrictions = fields.find_field("restrictions");
                let mut has_restrictions = false;

                // The `restrictions` field is optional
                if let Some(field) = restrictions {
                    let report = restrictions::lint(field.element())
                        .deescalate_error_into(&mut warnings)
                        .gather_warnings_into(&mut warnings);

                    if let Some(report) = report {
                        let restrictions::Report { is_empty } = report;
                        has_restrictions = is_empty;
                    }
                }

                let fields = fields.as_raw_map();
                // The `price_components` field is required and should contain at least one item.
                let dimensions = required_field!(elem, fields, "price_components", warnings)
                    .and_then(|elem| {
                        price_components::lint(elem)
                            .deescalate_error_into(&mut warnings)
                            .gather_warnings_into(&mut warnings)
                    })
                    .unwrap_or_default();

                // Filter off the invalid dimension types.
                let dimensions = dimensions.into_iter().filter_map(Option::from).collect();

                Item::Valid(ElementSummary {
                    dimensions,
                    has_restrictions,
                })
            })
            .collect::<Vec<_>>();

        Ok(().into_caveat(warnings))
    }
}

pub mod price_components {
    use std::{borrow::Cow, fmt};

    use tracing::instrument;

    use crate::{
        enumeration, from_warning_all,
        json::{self, FieldsAsExt as _, FromJson as _},
        lint::Item,
        number, required_field,
        tariff::v2x::DimensionType,
        warning::{self, DeescalateError as _, GatherWarnings as _, IntoCaveat as _},
        Money, Verdict,
    };

    #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
    pub enum Warning {
        /// The array exists but is empty.
        Empty,

        /// The given field is required.
        FieldRequired {
            field_name: Cow<'static, str>,
        },

        /// The JSON value given is not an array.
        InvalidType {
            type_found: json::ValueKind,
        },

        Money(number::Warning),

        Type(enumeration::Warning),
    }

    impl Warning {
        fn invalid_type(elem: &json::Element<'_>) -> Self {
            Self::InvalidType {
                type_found: elem.value().kind(),
            }
        }
    }

    from_warning_all!(
        enumeration::Warning => Warning::Type,
        number::Warning => Warning::Money
    );

    impl fmt::Display for Warning {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::Empty => write!(
                    f,
                    "An empty list of days means that no day is allowed. Is this what you want?"
                ),
                Self::FieldRequired { field_name } => {
                    write!(f, "Field is required: `{field_name}`")
                }
                Self::InvalidType { type_found } => write!(
                    f,
                    "The value should be an object but a `{type_found}` was found."
                ),
                Self::Money(w) => fmt::Display::fmt(w, f),
                Self::Type(w) => fmt::Display::fmt(w, f),
            }
        }
    }

    impl crate::Warning for Warning {
        fn id(&self) -> warning::Id {
            match self {
                Self::Empty => warning::Id::from_static("empty"),
                Self::FieldRequired { field_name } => {
                    warning::Id::from_string(format!("field_required({field_name})"))
                }
                Self::InvalidType { type_found } => {
                    warning::Id::from_string(format!("invalid_type({type_found})"))
                }
                Self::Money(w) => w.id(),
                Self::Type(w) => w.id(),
            }
        }
    }

    /// Lint the `price_components` field.
    #[instrument(skip_all)]
    pub(super) fn lint(elem: &json::Element<'_>) -> Verdict<Vec<Item<DimensionType>>, Warning> {
        let mut warnings = warning::Set::<Warning>::new();

        // The `elements` field should be an array.
        let Some(items) = elem.as_array() else {
            return warnings.bail(elem, Warning::invalid_type(elem));
        };

        // The `price_components` array should contain at least one `PriceComponent` object.
        if items.is_empty() {
            return warnings.bail(elem, Warning::Empty);
        }

        let dimensions: Vec<Item<DimensionType>> = items
            .iter()
            .map(|elem| {
                let Some(fields) = elem.as_object_fields() else {
                    warnings.insert(elem, Warning::invalid_type(elem));
                    return Item::Invalid;
                };

                let fields = fields.as_raw_map();

                {
                    let price_elem = fields.get("price");

                    if let Some(elem) = price_elem {
                        let _money = Money::from_json(elem)
                            .deescalate_error_into(&mut warnings)
                            .gather_warnings_into(&mut warnings);
                    }
                }

                {
                    let Some(type_elem) = required_field!(elem, fields, "type", warnings) else {
                        // The `type` field is required.
                        return Item::Invalid;
                    };

                    let dimension = DimensionType::from_json(type_elem)
                        .deescalate_error_into(&mut warnings)
                        .gather_warnings_into(&mut warnings);

                    Item::from(dimension)
                }
            })
            .collect();

        Ok(dimensions.into_caveat(warnings))
    }
}

pub mod restrictions {
    //! The linting and Warning infrastructure for the `restriction` field.
    //!
    //! * See: [OCPI spec 2.2.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#mod_tariffs_tariffrestrictions_class>)
    //! * See: [OCPI spec 2.1.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#45-tariffrestrictions-class>)

    use std::fmt;

    use tracing::instrument;

    use super::{time, weekday};
    use crate::{
        duration::{self, Seconds},
        from_warning_all,
        json::{self, FieldsAsExt as _},
        number,
        warning::{self, DeescalateError as _, GatherWarnings as _, IntoCaveat as _},
        Ampere, Kw, Kwh, Verdict,
    };

    #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
    pub enum Warning {
        Duration(duration::Warning),

        /// The JSON value given is not an object.
        InvalidType {
            type_found: json::ValueKind,
        },

        Number(number::Warning),

        MaxZeroNeverMatch,

        /// The `start_time` and `end_time` fields are nested in the `restrictions` object.
        Time(time::Warning),

        /// The `day_of_week` field is nested in the `restrictions` object.
        Weekday(weekday::Warning),
    }

    impl Warning {
        fn invalid_type(elem: &json::Element<'_>) -> Self {
            Self::InvalidType {
                type_found: elem.value().kind(),
            }
        }
    }

    impl fmt::Display for Warning {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::Duration(warning) => fmt::Display::fmt(warning, f),
                Self::InvalidType { type_found } => {
                    write!(f, "The value should be an object but is `{type_found}`")
                }
                Self::MaxZeroNeverMatch => write!(f, "This element contains a `max_*` restriction and so will never match. This element can be removed"),
                Self::Number(warning) => fmt::Display::fmt(warning, f),
                Self::Time(warning) => fmt::Display::fmt(warning, f),
                Self::Weekday(warning) => fmt::Display::fmt(warning, f),
            }
        }
    }

    impl crate::Warning for Warning {
        fn id(&self) -> warning::Id {
            match self {
                Self::Duration(warning) => warning.id(),
                Self::InvalidType { type_found } => {
                    warning::Id::from_string(format!("invalid_type({type_found})"))
                }
                Self::MaxZeroNeverMatch => warning::Id::from_static("max_zero_will_never_match"),
                Self::Number(warning) => warning.id(),
                Self::Time(warning) => warning.id(),
                Self::Weekday(warning) => warning.id(),
            }
        }
    }

    from_warning_all!(
        duration::Warning => Warning::Duration,
        number::Warning => Warning::Number,
        time::Warning => Warning::Time,
        weekday::Warning => Warning::Weekday
    );

    /// Observations from linting the `restrictions` field.
    pub struct Report {
        /// True the object has no fields.
        pub is_empty: bool,
    }

    /// Lint the `restrictions` field.
    #[instrument(skip_all)]
    pub(super) fn lint(elem: &json::Element<'_>) -> Verdict<Report, Warning> {
        let mut warnings = warning::Set::<Warning>::new();

        let Some(fields) = elem.as_object_fields() else {
            return warnings.bail(elem, Warning::invalid_type(elem));
        };

        let fields = fields.as_raw_map();

        {
            let start_time = fields.get("start_time").map(|e| &**e);
            let end_time = fields.get("end_time").map(|e| &**e);

            let _drop: Option<()> = time::lint(start_time, end_time)
                .deescalate_error_into(&mut warnings)
                .gather_warnings_into(&mut warnings);
        }

        {
            let day_of_week = fields.get("day_of_week").map(|e| &**e);

            let _drop: Option<()> = weekday::lint(day_of_week)
                .deescalate_error_into(&mut warnings)
                .gather_warnings_into(&mut warnings);
        }

        {
            fields
                .get("max_current")
                .map(|elem| from_json_lint_zero::<Ampere>(elem, &mut warnings));

            fields
                .get("max_duration")
                .map(|elem| from_json_lint_zero::<Seconds>(elem, &mut warnings));

            fields
                .get("max_kwh")
                .map(|elem| from_json_lint_zero::<Kwh>(elem, &mut warnings));

            fields
                .get("max_power")
                .map(|elem| from_json_lint_zero::<Kw>(elem, &mut warnings));
        }

        Ok(Report {
            is_empty: fields.is_empty(),
        }
        .into_caveat(warnings))
    }

    /// Parse a given `Element` as a `max_*` restriction and raise a `Warning::MaxZeroNeverMatch`
    /// if the value is zero.
    fn from_json_lint_zero<'elem, 'buf, T>(
        element: &'elem json::Element<'buf>,
        warnings: &mut warning::Set<Warning>,
    ) -> Option<T>
    where
        T: json::FromJson<'buf, Warning: Into<Warning>> + number::IsZero,
    {
        let value = T::from_json(element)
            .deescalate_error_into(warnings)
            .gather_warnings_into(warnings);

        if value.as_ref().is_some_and(number::IsZero::is_zero) {
            warnings.insert(element, Warning::MaxZeroNeverMatch);
        }

        value
    }
}

pub mod weekday {
    //! Linting and warning infrastructure for the `day_of_week` field.
    //!
    //! * See: [OCPI spec 2.2.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>)
    //! * See: [OCPI spec 2.2.1: Tariff DayOfWeek](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#mod_tariffs_dayofweek_enum>)
    //! * See: [OCPI spec 2.1.1: Tariff Restrictions](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#45-tariffrestrictions-class>)
    //! * See: [OCPI spec 2.1.1: Tariff DayOfWeek](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#41-dayofweek-enum>)

    use std::{collections::BTreeSet, fmt, sync::LazyLock};

    use crate::{
        from_warning_all,
        json::{self, FromJson as _},
        warning::{self, GatherWarnings as _, IntoCaveat as _},
        Verdict, Weekday,
    };

    #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
    pub enum Warning {
        /// The list contains all days of the week.
        ContainsEntireWeek,

        /// Each field needs to be a valid weekday.
        Enum(crate::enumeration::Warning),

        /// There is at least one duplicate day.
        Duplicates,

        /// An empty array means that no day is allowed.
        Empty,

        /// The JSON value given is not an array.
        InvalidType { type_found: json::ValueKind },

        /// The days are unsorted.
        Unsorted,
    }

    impl Warning {
        fn invalid_type(elem: &json::Element<'_>) -> Self {
            Self::InvalidType {
                type_found: elem.value().kind(),
            }
        }
    }

    impl fmt::Display for Warning {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::ContainsEntireWeek => write!(f, "All days of the week are defined. You can simply leave out the `day_of_week` field."),
                Self::Enum(warning) => fmt::Display::fmt(warning, f),
                Self::Duplicates => write!(f, "There's at least one duplicate day."),
                Self::Empty => write!(
                    f,
                    "An empty list of days means that no day is allowed. Is this what you want?"
                ),
                Self::InvalidType { type_found } => {
                    write!(f, "The value should be an array but is `{type_found}`")
                }
                Self::Unsorted => write!(f, "The days are unsorted."),
            }
        }
    }

    impl crate::Warning for Warning {
        fn id(&self) -> warning::Id {
            match self {
                Self::ContainsEntireWeek => warning::Id::from_static("contains_entire_week"),
                Self::Enum(warning) => warning.id(),
                Self::Duplicates => warning::Id::from_static("duplicates"),
                Self::Empty => warning::Id::from_static("empty"),
                Self::InvalidType { type_found } => {
                    warning::Id::from_string(format!("invalid_type({type_found})"))
                }
                Self::Unsorted => warning::Id::from_static("unsorted"),
            }
        }
    }

    from_warning_all!(crate::enumeration::Warning => Warning::Enum);

    /// Lint the `day_of_week` field.
    pub(super) fn lint(elem: Option<&json::Element<'_>>) -> Verdict<(), Warning> {
        /// This is the correct order of the days of the week.
        static ALL_DAYS_OF_WEEK: LazyLock<BTreeSet<Weekday>> = LazyLock::new(|| {
            BTreeSet::from([
                Weekday::Monday,
                Weekday::Tuesday,
                Weekday::Wednesday,
                Weekday::Thursday,
                Weekday::Friday,
                Weekday::Saturday,
                Weekday::Sunday,
            ])
        });

        let mut warnings = warning::Set::<Warning>::new();

        // The `day_of_week` field is optional.
        let Some(elem) = elem else {
            return Ok(().into_caveat(warnings));
        };

        // The `day_of_week` field should be an array.
        let Some(items) = elem.as_array() else {
            return warnings.bail(elem, Warning::invalid_type(elem));
        };

        // Issue a warning if the `day_of_week` array is defined but empty.
        // This can be a user misunderstanding.
        if items.is_empty() {
            warnings.insert(elem, Warning::Empty);
            return Ok(().into_caveat(warnings));
        }

        // Convert each array item to a day and bail out on serious errors.
        let days = items
            .iter()
            .map(Weekday::from_json)
            .collect::<Result<Vec<_>, _>>()?;

        // Collect warnings from the conversion of each array item to a day.
        let days = days.gather_warnings_into(&mut warnings);

        // Issue a warning if the days are not sorted.
        if !days.is_sorted() {
            warnings.insert(elem, Warning::Unsorted);
        }

        let day_set: BTreeSet<_> = days.iter().copied().collect();

        // If the set length is less than the list, that means at least one duplicate was removed
        // during the conversion.
        if day_set.len() != days.len() {
            warnings.insert(elem, Warning::Duplicates);
        }

        // Issue a warning of all days of the week are defined.
        // This is equivalent to not defining the `day_of_week` array.
        if day_set == *ALL_DAYS_OF_WEEK {
            warnings.insert(elem, Warning::ContainsEntireWeek);
        }

        Ok(().into_caveat(warnings))
    }
}
