//! Compile-time diagnostic message types, modelled on rustc-style output.
//!
//! Used exclusively during the compilation of log matchers from YAML into
//! [`crate::log_matcher::types::CompiledMatcher`]. Never used at runtime.

use std::fmt;

// ---------------------------------------------------------------------------
// Reference
// ---------------------------------------------------------------------------

/// A location pointer within a YAML source.
///
/// Since serde does not expose line numbers, `filename` typically encodes a
/// field path like `"extension.yaml:matchers[0].start.match"`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
    /// Field path or filename, e.g. `"extension.yaml:matchers[0].id"`.
    pub filename: String,
    pub line: Option<u32>,
    pub column: Option<u32>,
}

impl Reference {
    pub fn new(filename: impl Into<String>) -> Self {
        Self {
            filename: filename.into(),
            line: None,
            column: None,
        }
    }
}

impl fmt::Display for Reference {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.filename)?;
        if let Some(l) = self.line {
            write!(f, ":{}", l)?;
            if let Some(c) = self.column {
                write!(f, ":{}", c)?;
            }
        }
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// AnnotatedRef
// ---------------------------------------------------------------------------

/// A reference with a label, used for secondary "see also" locations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnotatedRef {
    pub reference: Reference,
    /// Short label, e.g. `"first defined here"`.
    pub label: String,
}

// ---------------------------------------------------------------------------
// MessageLevel
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessageLevel {
    Info,
    Warning,
    Error,
}

impl fmt::Display for MessageLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Error => write!(f, "error"),
            Self::Warning => write!(f, "warning"),
            Self::Info => write!(f, "info"),
        }
    }
}

// ---------------------------------------------------------------------------
// Message
// ---------------------------------------------------------------------------

/// A compile-time diagnostic, modelled on rustc-style output.
///
/// ```text
/// error: duplicate matcher id: rust.cargo.error
///   --> extension.yaml:matchers[2].id
///   = note: first defined here: extension.yaml:matchers[0].id
/// ```
#[derive(Debug, Clone)]
pub struct Message {
    pub level: MessageLevel,
    pub text: String,
    /// Primary location where the issue was found.
    pub reference: Option<Reference>,
    /// Secondary annotated locations (e.g. "first defined here").
    pub related: Vec<AnnotatedRef>,
}

impl Message {
    pub fn error(text: impl Into<String>) -> Self {
        Self {
            level: MessageLevel::Error,
            text: text.into(),
            reference: None,
            related: vec![],
        }
    }

    pub fn error_at(text: impl Into<String>, reference: impl Into<String>) -> Self {
        Self {
            level: MessageLevel::Error,
            text: text.into(),
            reference: Some(Reference::new(reference)),
            related: vec![],
        }
    }

    pub fn warning_at(text: impl Into<String>, reference: impl Into<String>) -> Self {
        Self {
            level: MessageLevel::Warning,
            text: text.into(),
            reference: Some(Reference::new(reference)),
            related: vec![],
        }
    }

    /// Attach a secondary annotated reference to this message.
    pub fn with_related(mut self, reference: impl Into<String>, label: impl Into<String>) -> Self {
        self.related.push(AnnotatedRef {
            reference: Reference::new(reference),
            label: label.into(),
        });
        self
    }
}

impl fmt::Display for Message {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.level, self.text)?;
        if let Some(r) = &self.reference {
            write!(f, "\n  --> {}", r)?;
        }
        for ann in &self.related {
            write!(f, "\n  = note: {}: {}", ann.label, ann.reference)?;
        }
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Returns `true` if any message in `msgs` has level [`MessageLevel::Error`].
pub fn has_errors(msgs: &[Message]) -> bool {
    msgs.iter().any(|m| m.level == MessageLevel::Error)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn message_display_error_with_reference() {
        let msg = Message::error_at("something went wrong", "file.yaml:matchers[0].id");
        let s = msg.to_string();
        assert!(s.contains("error: something went wrong"));
        assert!(s.contains("file.yaml:matchers[0].id"));
    }

    #[test]
    fn message_display_with_related() {
        let msg = Message::error_at("duplicate id: foo", "file.yaml:matchers[1].id")
            .with_related("file.yaml:matchers[0].id", "first defined here");
        let s = msg.to_string();
        assert!(s.contains("first defined here"));
        assert!(s.contains("file.yaml:matchers[0].id"));
    }

    #[test]
    fn has_errors_detects_error_level() {
        let msgs = vec![
            Message::warning_at("a warning", "somewhere"),
            Message::error_at("an error", "somewhere"),
        ];
        assert!(has_errors(&msgs));
    }

    #[test]
    fn has_errors_false_for_warnings_only() {
        let msgs = vec![Message::warning_at("just a warning", "somewhere")];
        assert!(!has_errors(&msgs));
    }

    #[test]
    fn has_errors_false_for_empty() {
        assert!(!has_errors(&[]));
    }
}
