//! Referential actions for foreign-key relationships.
//!
//! These map directly onto SQLite's `ON DELETE` / `ON UPDATE`
//! clause vocabulary. `SET DEFAULT` is intentionally omitted
//! until column DEFAULTs (C3 partial) are supported.

use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReferentialAction {
    /// Default — referenced rows can't be deleted while
    /// referencing rows exist (deferred check).
    NoAction,
    /// Like NoAction but immediate.
    Restrict,
    /// On parent row delete/update, set the FK column to NULL.
    SetNull,
    /// On parent row delete/update, propagate to dependent rows.
    Cascade,
}

impl ReferentialAction {
    /// The user-facing keyword as it appears in DSL input. Note
    /// `set null` is two words; the parser handles that as a
    /// single phrase.
    #[must_use]
    pub const fn keyword(self) -> &'static str {
        match self {
            Self::NoAction => "no action",
            Self::Restrict => "restrict",
            Self::SetNull => "set null",
            Self::Cascade => "cascade",
        }
    }

    /// The corresponding SQL clause as written in DDL.
    #[must_use]
    pub const fn sql_clause(self) -> &'static str {
        match self {
            Self::NoAction => "NO ACTION",
            Self::Restrict => "RESTRICT",
            Self::SetNull => "SET NULL",
            Self::Cascade => "CASCADE",
        }
    }

    /// All actions, in stable order.
    #[must_use]
    pub const fn all() -> &'static [Self] {
        &[Self::NoAction, Self::Restrict, Self::SetNull, Self::Cascade]
    }

    /// Default action when none is specified — matches the SQL
    /// standard.
    #[must_use]
    pub const fn default_action() -> Self {
        Self::NoAction
    }
}

impl Default for ReferentialAction {
    fn default() -> Self {
        Self::default_action()
    }
}

impl fmt::Display for ReferentialAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.keyword())
    }
}

/// Error returned when parsing an unknown action keyword.
///
/// Display flows through the i18n catalog
/// (`parse.custom.unknown_action`) so the wording can be
/// localised or adjusted without touching the type.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownAction {
    pub found: String,
    pub expected: String,
}

impl std::fmt::Display for UnknownAction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&crate::t!(
            "parse.custom.unknown_action",
            found = self.found,
            expected = self.expected,
        ))
    }
}

impl std::error::Error for UnknownAction {}

impl FromStr for ReferentialAction {
    type Err = UnknownAction;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Case-insensitive comparison; tolerates internal
        // whitespace ("set  null") by collapsing it.
        let normalised: String = s
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
            .to_ascii_lowercase();
        for &action in Self::all() {
            if normalised == action.keyword() {
                return Ok(action);
            }
        }
        Err(UnknownAction {
            found: s.to_string(),
            expected: Self::all()
                .iter()
                .map(|a| a.keyword())
                .collect::<Vec<_>>()
                .join(", "),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn keyword_round_trip() {
        for &a in ReferentialAction::all() {
            assert_eq!(a.keyword().parse::<ReferentialAction>().unwrap(), a);
        }
    }

    #[test]
    fn parsing_is_case_insensitive_and_whitespace_tolerant() {
        assert_eq!(
            "Cascade".parse::<ReferentialAction>().unwrap(),
            ReferentialAction::Cascade
        );
        assert_eq!(
            "SET NULL".parse::<ReferentialAction>().unwrap(),
            ReferentialAction::SetNull
        );
        assert_eq!(
            "set    null".parse::<ReferentialAction>().unwrap(),
            ReferentialAction::SetNull
        );
        assert_eq!(
            "No Action".parse::<ReferentialAction>().unwrap(),
            ReferentialAction::NoAction
        );
    }

    #[test]
    fn unknown_action_lists_alternatives() {
        let err = "destroy".parse::<ReferentialAction>().unwrap_err();
        assert_eq!(err.found, "destroy");
        assert!(err.expected.contains("cascade"));
        assert!(err.expected.contains("set null"));
    }

    #[test]
    fn sql_clause_mapping() {
        assert_eq!(ReferentialAction::SetNull.sql_clause(), "SET NULL");
        assert_eq!(ReferentialAction::NoAction.sql_clause(), "NO ACTION");
    }
}
