//! Elm-style diagnostic messages for beautiful error reporting
//!
//! This module provides Elm-quality error messages with:
//! - Error codes (E0001, E0002, etc.) for easy searchability
//! - Visual type diffs showing expected vs actual
//! - "Did you mean?" suggestions using Levenshtein distance
//! - Context-aware hints based on error type
//! - Colored output with syntax highlighting

#![allow(dead_code)]

use crate::ast::Span;

/// Error codes for all Lux compiler errors
/// These follow the pattern E{category}{number}:
/// - E01xx: Parse errors
/// - E02xx: Type errors
/// - E03xx: Name resolution errors
/// - E04xx: Effect errors
/// - E05xx: Pattern matching errors
/// - E06xx: Import/module errors
/// - E07xx: Behavioral type errors
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
    // Parse errors (E01xx)
    E0100, // Generic parse error
    E0101, // Unexpected token
    E0102, // Unclosed delimiter
    E0103, // Invalid literal
    E0104, // Missing semicolon or delimiter
    E0105, // Invalid operator

    // Type errors (E02xx)
    E0200, // Generic type error
    E0201, // Type mismatch
    E0202, // Cannot unify types
    E0203, // Missing type annotation
    E0204, // Invalid type application
    E0205, // Recursive type without indirection
    E0206, // Field type mismatch
    E0207, // Missing record field
    E0208, // Extra record field
    E0209, // Wrong number of type arguments
    E0210, // Type not found
    E0211, // Return type mismatch

    // Name resolution errors (E03xx)
    E0300, // Generic name error
    E0301, // Undefined variable
    E0302, // Undefined function
    E0303, // Undefined type
    E0304, // Undefined module
    E0305, // Duplicate definition
    E0306, // Shadowing warning
    E0307, // Unused variable warning
    E0308, // Undefined field

    // Effect errors (E04xx)
    E0400, // Generic effect error
    E0401, // Unhandled effect
    E0402, // Effect not in scope
    E0403, // Missing effect handler
    E0404, // Invalid effect operation
    E0405, // Effect mismatch

    // Pattern matching errors (E05xx)
    E0500, // Generic pattern error
    E0501, // Non-exhaustive patterns
    E0502, // Unreachable pattern
    E0503, // Invalid pattern
    E0504, // Duplicate pattern binding

    // Import/module errors (E06xx)
    E0600, // Generic module error
    E0601, // Module not found
    E0602, // Circular import
    E0603, // Export not found
    E0604, // Private item accessed

    // Behavioral type errors (E07xx)
    E0700, // Generic behavioral error
    E0701, // Purity violation
    E0702, // Totality violation
    E0703, // Idempotency violation
    E0704, // Commutativity violation
}

impl ErrorCode {
    pub fn code(&self) -> &'static str {
        match self {
            // Parse errors
            ErrorCode::E0100 => "E0100",
            ErrorCode::E0101 => "E0101",
            ErrorCode::E0102 => "E0102",
            ErrorCode::E0103 => "E0103",
            ErrorCode::E0104 => "E0104",
            ErrorCode::E0105 => "E0105",
            // Type errors
            ErrorCode::E0200 => "E0200",
            ErrorCode::E0201 => "E0201",
            ErrorCode::E0202 => "E0202",
            ErrorCode::E0203 => "E0203",
            ErrorCode::E0204 => "E0204",
            ErrorCode::E0205 => "E0205",
            ErrorCode::E0206 => "E0206",
            ErrorCode::E0207 => "E0207",
            ErrorCode::E0208 => "E0208",
            ErrorCode::E0209 => "E0209",
            ErrorCode::E0210 => "E0210",
            ErrorCode::E0211 => "E0211",
            // Name errors
            ErrorCode::E0300 => "E0300",
            ErrorCode::E0301 => "E0301",
            ErrorCode::E0302 => "E0302",
            ErrorCode::E0303 => "E0303",
            ErrorCode::E0304 => "E0304",
            ErrorCode::E0305 => "E0305",
            ErrorCode::E0306 => "E0306",
            ErrorCode::E0307 => "E0307",
            ErrorCode::E0308 => "E0308",
            // Effect errors
            ErrorCode::E0400 => "E0400",
            ErrorCode::E0401 => "E0401",
            ErrorCode::E0402 => "E0402",
            ErrorCode::E0403 => "E0403",
            ErrorCode::E0404 => "E0404",
            ErrorCode::E0405 => "E0405",
            // Pattern errors
            ErrorCode::E0500 => "E0500",
            ErrorCode::E0501 => "E0501",
            ErrorCode::E0502 => "E0502",
            ErrorCode::E0503 => "E0503",
            ErrorCode::E0504 => "E0504",
            // Module errors
            ErrorCode::E0600 => "E0600",
            ErrorCode::E0601 => "E0601",
            ErrorCode::E0602 => "E0602",
            ErrorCode::E0603 => "E0603",
            ErrorCode::E0604 => "E0604",
            // Behavioral errors
            ErrorCode::E0700 => "E0700",
            ErrorCode::E0701 => "E0701",
            ErrorCode::E0702 => "E0702",
            ErrorCode::E0703 => "E0703",
            ErrorCode::E0704 => "E0704",
        }
    }

    pub fn title(&self) -> &'static str {
        match self {
            // Parse errors
            ErrorCode::E0100 => "Parse Error",
            ErrorCode::E0101 => "Unexpected Token",
            ErrorCode::E0102 => "Unclosed Delimiter",
            ErrorCode::E0103 => "Invalid Literal",
            ErrorCode::E0104 => "Missing Delimiter",
            ErrorCode::E0105 => "Invalid Operator",
            // Type errors
            ErrorCode::E0200 => "Type Error",
            ErrorCode::E0201 => "Type Mismatch",
            ErrorCode::E0202 => "Cannot Unify Types",
            ErrorCode::E0203 => "Missing Type Annotation",
            ErrorCode::E0204 => "Invalid Type Application",
            ErrorCode::E0205 => "Recursive Type Error",
            ErrorCode::E0206 => "Field Type Mismatch",
            ErrorCode::E0207 => "Missing Record Field",
            ErrorCode::E0208 => "Extra Record Field",
            ErrorCode::E0209 => "Wrong Type Arity",
            ErrorCode::E0210 => "Type Not Found",
            ErrorCode::E0211 => "Return Type Mismatch",
            // Name errors
            ErrorCode::E0300 => "Name Error",
            ErrorCode::E0301 => "Undefined Variable",
            ErrorCode::E0302 => "Undefined Function",
            ErrorCode::E0303 => "Undefined Type",
            ErrorCode::E0304 => "Undefined Module",
            ErrorCode::E0305 => "Duplicate Definition",
            ErrorCode::E0306 => "Variable Shadowing",
            ErrorCode::E0307 => "Unused Variable",
            ErrorCode::E0308 => "Undefined Field",
            // Effect errors
            ErrorCode::E0400 => "Effect Error",
            ErrorCode::E0401 => "Unhandled Effect",
            ErrorCode::E0402 => "Effect Not In Scope",
            ErrorCode::E0403 => "Missing Effect Handler",
            ErrorCode::E0404 => "Invalid Effect Operation",
            ErrorCode::E0405 => "Effect Mismatch",
            // Pattern errors
            ErrorCode::E0500 => "Pattern Error",
            ErrorCode::E0501 => "Non-Exhaustive Patterns",
            ErrorCode::E0502 => "Unreachable Pattern",
            ErrorCode::E0503 => "Invalid Pattern",
            ErrorCode::E0504 => "Duplicate Pattern Binding",
            // Module errors
            ErrorCode::E0600 => "Module Error",
            ErrorCode::E0601 => "Module Not Found",
            ErrorCode::E0602 => "Circular Import",
            ErrorCode::E0603 => "Export Not Found",
            ErrorCode::E0604 => "Private Item Access",
            // Behavioral errors
            ErrorCode::E0700 => "Behavioral Type Error",
            ErrorCode::E0701 => "Purity Violation",
            ErrorCode::E0702 => "Totality Violation",
            ErrorCode::E0703 => "Idempotency Violation",
            ErrorCode::E0704 => "Commutativity Violation",
        }
    }

    /// Get a URL to documentation about this error
    pub fn help_url(&self) -> String {
        format!("https://lux-lang.dev/errors/{}", self.code())
    }
}

impl std::fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.code())
    }
}

/// ANSI color codes for terminal output
pub mod colors {
    pub const RESET: &str = "\x1b[0m";
    pub const BOLD: &str = "\x1b[1m";
    pub const DIM: &str = "\x1b[2m";
    pub const RED: &str = "\x1b[31m";
    pub const GREEN: &str = "\x1b[32m";
    pub const YELLOW: &str = "\x1b[33m";
    pub const BLUE: &str = "\x1b[34m";
    pub const MAGENTA: &str = "\x1b[35m";
    pub const CYAN: &str = "\x1b[36m";
    pub const WHITE: &str = "\x1b[37m";
    pub const GRAY: &str = "\x1b[90m";
}

/// Apply color to text, respecting NO_COLOR / TERM=dumb
pub fn c(color: &str, text: &str) -> String {
    if supports_color() {
        format!("{}{}{}", color, text, colors::RESET)
    } else {
        text.to_string()
    }
}

/// Apply bold + color to text
pub fn bc(color: &str, text: &str) -> String {
    if supports_color() {
        format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET)
    } else {
        text.to_string()
    }
}

/// Severity level for diagnostics
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
    Error,
    Warning,
    Hint,
}

impl Severity {
    fn color(&self) -> &'static str {
        match self {
            Severity::Error => colors::RED,
            Severity::Warning => colors::YELLOW,
            Severity::Hint => colors::CYAN,
        }
    }

    fn label(&self) -> &'static str {
        match self {
            Severity::Error => "ERROR",
            Severity::Warning => "WARNING",
            Severity::Hint => "HINT",
        }
    }
}

/// A diagnostic message with source location and optional hints
#[derive(Debug, Clone)]
pub struct Diagnostic {
    pub severity: Severity,
    pub code: Option<ErrorCode>,
    pub title: String,
    pub message: String,
    pub span: Span,
    pub hints: Vec<String>,
    pub expected_type: Option<String>,
    pub actual_type: Option<String>,
    pub secondary_spans: Vec<(Span, String)>,
}

impl Diagnostic {
    pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
        Self {
            severity: Severity::Error,
            code: None,
            title: title.into(),
            message: message.into(),
            span,
            hints: Vec::new(),
            expected_type: None,
            actual_type: None,
            secondary_spans: Vec::new(),
        }
    }

    pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
        Self {
            severity: Severity::Warning,
            code: None,
            title: title.into(),
            message: message.into(),
            span,
            hints: Vec::new(),
            expected_type: None,
            actual_type: None,
            secondary_spans: Vec::new(),
        }
    }

    /// Create an error with a specific error code
    pub fn with_code(code: ErrorCode, message: impl Into<String>, span: Span) -> Self {
        Self {
            severity: Severity::Error,
            code: Some(code),
            title: code.title().to_string(),
            message: message.into(),
            span,
            hints: Vec::new(),
            expected_type: None,
            actual_type: None,
            secondary_spans: Vec::new(),
        }
    }

    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
        self.hints.push(hint.into());
        self
    }

    pub fn with_hints(mut self, hints: Vec<String>) -> Self {
        self.hints.extend(hints);
        self
    }

    /// Add expected and actual types for type mismatch visualization
    pub fn with_types(mut self, expected: impl Into<String>, actual: impl Into<String>) -> Self {
        self.expected_type = Some(expected.into());
        self.actual_type = Some(actual.into());
        self
    }

    /// Add a secondary span with a label (e.g., "defined here")
    pub fn with_secondary(mut self, span: Span, label: impl Into<String>) -> Self {
        self.secondary_spans.push((span, label.into()));
        self
    }

    /// Set the error code
    pub fn code(mut self, code: ErrorCode) -> Self {
        self.code = Some(code);
        self.title = code.title().to_string();
        self
    }
}

/// Format a visual type diff between expected and actual types
/// Highlights the differences in red
pub fn format_type_diff(expected: &str, actual: &str) -> String {
    let mut output = String::new();

    output.push_str(&format!(
        "\n    {}{}Expected:{} {}{}{}\n",
        colors::BOLD,
        colors::CYAN,
        colors::RESET,
        colors::BOLD,
        expected,
        colors::RESET
    ));
    output.push_str(&format!(
        "    {}{}Found:   {} {}{}{}\n",
        colors::BOLD,
        colors::RED,
        colors::RESET,
        colors::BOLD,
        actual,
        colors::RESET
    ));

    // If types are structurally similar, show where they differ
    if let Some(diff) = compute_type_diff(expected, actual) {
        output.push_str(&format!("\n    {}\n", diff));
    }

    output
}

/// Compute a simple diff between two type strings
fn compute_type_diff(expected: &str, actual: &str) -> Option<String> {
    // Simple case: find the first difference
    let exp_chars: Vec<char> = expected.chars().collect();
    let act_chars: Vec<char> = actual.chars().collect();

    let mut diff_start = None;

    for (i, (e, a)) in exp_chars.iter().zip(act_chars.iter()).enumerate() {
        if e != a && diff_start.is_none() {
            diff_start = Some(i);
            break;
        }
    }

    // If the types are identical, no diff needed
    if diff_start.is_none() && exp_chars.len() == act_chars.len() {
        return None;
    }

    // Show the difference location with an arrow
    if let Some(start) = diff_start {
        let pointer = format!(
            "{}{}^-- difference starts here{}",
            " ".repeat(start + 9), // Account for "Found:   " prefix
            colors::YELLOW,
            colors::RESET
        );
        return Some(pointer);
    }

    // Length difference
    if exp_chars.len() != act_chars.len() {
        return Some(format!(
            "{}Note:{} Types have different lengths ({} vs {} characters)",
            colors::YELLOW,
            colors::RESET,
            exp_chars.len(),
            act_chars.len()
        ));
    }

    None
}

/// Format a plain type diff (no colors)
pub fn format_type_diff_plain(expected: &str, actual: &str) -> String {
    format!(
        "\n    Expected: {}\n    Found:    {}\n",
        expected, actual
    )
}

/// Calculate the Levenshtein edit distance between two strings
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
    let a_len = a.chars().count();
    let b_len = b.chars().count();

    if a_len == 0 {
        return b_len;
    }
    if b_len == 0 {
        return a_len;
    }

    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();

    let mut matrix = vec![vec![0usize; b_len + 1]; a_len + 1];

    for i in 0..=a_len {
        matrix[i][0] = i;
    }
    for j in 0..=b_len {
        matrix[0][j] = j;
    }

    for i in 1..=a_len {
        for j in 1..=b_len {
            let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
            matrix[i][j] = std::cmp::min(
                std::cmp::min(
                    matrix[i - 1][j] + 1,     // deletion
                    matrix[i][j - 1] + 1,     // insertion
                ),
                matrix[i - 1][j - 1] + cost,  // substitution
            );
        }
    }

    matrix[a_len][b_len]
}

/// Find similar names from a list of candidates
/// Returns names within the given edit distance, sorted by similarity
pub fn find_similar_names<'a>(
    target: &str,
    candidates: impl IntoIterator<Item = &'a str>,
    max_distance: usize,
) -> Vec<String> {
    let mut matches: Vec<(String, usize)> = candidates
        .into_iter()
        .filter(|&c| c != target) // Don't suggest the same name
        .map(|c| (c.to_string(), levenshtein_distance(target, c)))
        .filter(|(_, dist)| *dist <= max_distance && *dist > 0)
        .collect();

    // Sort by distance (closest first), then alphabetically
    matches.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));

    // Return just the names, limited to top 3
    matches.into_iter().take(3).map(|(name, _)| name).collect()
}

/// Format "Did you mean?" hint from suggestions
pub fn format_did_you_mean(suggestions: &[String]) -> Option<String> {
    match suggestions.len() {
        0 => None,
        1 => Some(format!("Did you mean '{}'?", suggestions[0])),
        2 => Some(format!("Did you mean '{}' or '{}'?", suggestions[0], suggestions[1])),
        _ => Some(format!(
            "Did you mean '{}', '{}', or '{}'?",
            suggestions[0], suggestions[1], suggestions[2]
        )),
    }
}

/// Converts byte offset to (line, column) - 1-indexed
pub fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
    let mut line = 1;
    let mut col = 1;

    for (i, ch) in source.char_indices() {
        if i >= offset {
            break;
        }
        if ch == '\n' {
            line += 1;
            col = 1;
        } else {
            col += 1;
        }
    }

    (line, col)
}

/// Extract a source line by line number (1-indexed)
pub fn get_source_line(source: &str, line_num: usize) -> Option<&str> {
    source.lines().nth(line_num.saturating_sub(1))
}

/// Get context lines around a span
pub fn get_context_lines(source: &str, span: Span, context: usize) -> Vec<(usize, &str)> {
    let (start_line, _) = offset_to_line_col(source, span.start);
    let (end_line, _) = offset_to_line_col(source, span.end);

    let first_line = start_line.saturating_sub(context);
    let last_line = end_line + context;

    source
        .lines()
        .enumerate()
        .filter(|(i, _)| *i + 1 >= first_line && *i + 1 <= last_line)
        .map(|(i, line)| (i + 1, line))
        .collect()
}

/// Render a diagnostic to a string with colors
pub fn render_diagnostic(
    diagnostic: &Diagnostic,
    source: &str,
    filename: Option<&str>,
) -> String {
    let mut output = String::new();
    let (line, col) = offset_to_line_col(source, diagnostic.span.start);
    let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
    let severity_color = diagnostic.severity.color();
    let severity_label = diagnostic.severity.label();

    // Header with error code: ── ERROR[E0201] ── filename.lux
    let filename_str = filename.unwrap_or("<input>");
    let code_str = diagnostic
        .code
        .map(|c| format!("[{}]", c.code()))
        .unwrap_or_default();
    output.push_str(&format!(
        "{}{}── {}{} ──────────────────── {}{}\n",
        colors::BOLD,
        severity_color,
        severity_label,
        code_str,
        filename_str,
        colors::RESET
    ));

    // Location
    output.push_str(&format!(
        "\n{}{}{}:{}{}\n\n",
        colors::BOLD,
        colors::WHITE,
        line,
        col,
        colors::RESET
    ));

    // Error title/category
    output.push_str(&format!(
        "{}{}{}{}\n\n",
        colors::BOLD,
        severity_color,
        diagnostic.title,
        colors::RESET
    ));

    // Source code snippet with line numbers and context
    let context_lines = 2; // Show 2 lines before and after
    let start_line = line.saturating_sub(context_lines).max(1);
    let end_context_line = (end_line + context_lines).min(source.lines().count());
    let line_num_width = end_context_line.to_string().len().max(4);

    // Show context before, error lines, and context after
    for line_num in start_line..=end_context_line {
        if let Some(source_line) = get_source_line(source, line_num) {
            let is_error_line = line_num >= line && line_num <= end_line;

            // Line number (dimmed for context, normal for error lines)
            if is_error_line {
                output.push_str(&format!(
                    "{}{:>width$} │{} ",
                    colors::DIM,
                    line_num,
                    colors::RESET,
                    width = line_num_width
                ));
                // Source line (normal)
                output.push_str(source_line);
            } else {
                // Context lines are fully dimmed
                output.push_str(&format!(
                    "{}{:>width$} │ {}{}",
                    colors::DIM,
                    line_num,
                    source_line,
                    colors::RESET,
                    width = line_num_width
                ));
            }
            output.push('\n');

            // Underline the error span (only for error lines)
            if is_error_line {
                let underline_start = if line_num == line { col - 1 } else { 0 };
                let underline_end = if line_num == end_line {
                    end_col - 1
                } else {
                    source_line.len()
                };

                let underline_len = underline_end.saturating_sub(underline_start).max(1);

                output.push_str(&format!(
                    "{}{:>width$} │{} ",
                    colors::DIM,
                    "",
                    colors::RESET,
                    width = line_num_width
                ));
                output.push_str(&" ".repeat(underline_start));
                output.push_str(&format!(
                    "{}{}{}",
                    severity_color,
                    "^".repeat(underline_len),
                    colors::RESET
                ));
                output.push('\n');
            }
        }
    }

    // Error message
    output.push_str(&format!("\n{}\n", diagnostic.message));

    // Type diff visualization (if present)
    if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
        output.push_str(&format_type_diff(expected, actual));
    }

    // Hints
    if !diagnostic.hints.is_empty() {
        output.push('\n');
        for hint in &diagnostic.hints {
            output.push_str(&format!(
                "{}{}Hint:{} {}\n",
                colors::BOLD,
                colors::CYAN,
                colors::RESET,
                hint
            ));
        }
    }

    // Help URL for error codes
    if let Some(code) = diagnostic.code {
        output.push_str(&format!(
            "\n{}{}Learn more:{} {}\n",
            colors::DIM,
            colors::BLUE,
            colors::RESET,
            code.help_url()
        ));
    }

    output.push('\n');
    output
}

/// Render a diagnostic without colors (for testing or non-TTY output)
pub fn render_diagnostic_plain(
    diagnostic: &Diagnostic,
    source: &str,
    filename: Option<&str>,
) -> String {
    let mut output = String::new();
    let (line, col) = offset_to_line_col(source, diagnostic.span.start);
    let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);

    let filename_str = filename.unwrap_or("<input>");
    let code_str = diagnostic
        .code
        .map(|c| format!("[{}]", c.code()))
        .unwrap_or_default();
    output.push_str(&format!(
        "-- {}{} ── {} ──────────────────────────────────\n",
        diagnostic.severity.label(),
        code_str,
        filename_str
    ));

    output.push_str(&format!("\n{}:{}\n\n", line, col));
    output.push_str(&format!("{}\n\n", diagnostic.title));

    let line_num_width = end_line.to_string().len().max(4);

    for line_num in line..=end_line {
        if let Some(source_line) = get_source_line(source, line_num) {
            output.push_str(&format!(
                "{:>width$} | ",
                line_num,
                width = line_num_width
            ));
            output.push_str(source_line);
            output.push('\n');

            let underline_start = if line_num == line { col - 1 } else { 0 };
            let underline_end = if line_num == end_line {
                end_col - 1
            } else {
                source_line.len()
            };

            let underline_len = underline_end.saturating_sub(underline_start).max(1);

            output.push_str(&format!(
                "{:>width$} | ",
                "",
                width = line_num_width
            ));
            output.push_str(&" ".repeat(underline_start));
            output.push_str(&"^".repeat(underline_len));
            output.push('\n');
        }
    }

    output.push_str(&format!("\n{}\n", diagnostic.message));

    // Type diff visualization (plain)
    if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
        output.push_str(&format_type_diff_plain(expected, actual));
    }

    if !diagnostic.hints.is_empty() {
        output.push('\n');
        for hint in &diagnostic.hints {
            output.push_str(&format!("Hint: {}\n", hint));
        }
    }

    // Help URL for error codes
    if let Some(code) = diagnostic.code {
        output.push_str(&format!("\nLearn more: {}\n", code.help_url()));
    }

    output.push('\n');
    output
}

/// Check if stdout is a TTY (for color support)
pub fn supports_color() -> bool {
    // Simple check - in production you'd use atty crate
    std::env::var("NO_COLOR").is_err() && std::env::var("TERM").map(|t| t != "dumb").unwrap_or(true)
}

/// Render diagnostic with automatic color detection
pub fn render(diagnostic: &Diagnostic, source: &str, filename: Option<&str>) -> String {
    if supports_color() {
        render_diagnostic(diagnostic, source, filename)
    } else {
        render_diagnostic_plain(diagnostic, source, filename)
    }
}

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

    #[test]
    fn test_offset_to_line_col() {
        let source = "line one\nline two\nline three";
        assert_eq!(offset_to_line_col(source, 0), (1, 1));
        assert_eq!(offset_to_line_col(source, 5), (1, 6));
        assert_eq!(offset_to_line_col(source, 9), (2, 1));
        assert_eq!(offset_to_line_col(source, 14), (2, 6));
    }

    #[test]
    fn test_get_source_line() {
        let source = "line one\nline two\nline three";
        assert_eq!(get_source_line(source, 1), Some("line one"));
        assert_eq!(get_source_line(source, 2), Some("line two"));
        assert_eq!(get_source_line(source, 3), Some("line three"));
        assert_eq!(get_source_line(source, 4), None);
    }

    #[test]
    fn test_render_diagnostic_plain() {
        let source = "fn test() {\n    unknownVar\n}";
        let diag = Diagnostic::error(
            "Unknown Variable",
            "The variable 'unknownVar' is not in scope.",
            Span { start: 16, end: 26 },
        )
        .with_hint("Did you mean 'knownVar'?");

        let output = render_diagnostic_plain(&diag, source, Some("test.lux"));

        assert!(output.contains("ERROR"));
        assert!(output.contains("test.lux"));
        assert!(output.contains("Unknown Variable"));
        assert!(output.contains("unknownVar"));
        assert!(output.contains("Hint:"));
    }

    #[test]
    fn test_diagnostic_builder() {
        let diag = Diagnostic::error("Test", "Message", Span { start: 0, end: 5 })
            .with_hint("Hint 1")
            .with_hint("Hint 2")
            .with_hints(vec!["Hint 3".to_string()]);

        assert_eq!(diag.hints.len(), 3);
        assert_eq!(diag.severity, Severity::Error);
    }

    #[test]
    fn test_multiline_span() {
        let source = "fn add(a: Int, b: Int): Int {\n    a +\n    b\n}";
        let diag = Diagnostic::error(
            "Multiline Error",
            "This spans multiple lines.",
            Span { start: 29, end: 43 },
        );

        let output = render_diagnostic_plain(&diag, source, None);

        assert!(output.contains("Multiline Error"));
        // Should show both lines
        assert!(output.contains("a +"));
        assert!(output.contains("b"));
    }

    #[test]
    fn test_type_error_categorization() {
        use super::{Diagnostic, ErrorCode};

        // Test that errors are properly categorized
        let source = "let x: Int = \"hello\"";

        // Simulate a type mismatch diagnostic with error code
        let diag = Diagnostic::with_code(
            ErrorCode::E0201,
            "Type mismatch: expected Int, got String",
            Span { start: 13, end: 20 },
        )
        .with_types("Int", "String")
        .with_hint("Check that the types on both sides of the expression are compatible.");

        let output = render_diagnostic_plain(&diag, source, Some("test.lux"));

        assert!(output.contains("Type Mismatch"));
        assert!(output.contains("\"hello\""));
        assert!(output.contains("Hint:"));
        assert!(output.contains("[E0201]"));
        assert!(output.contains("Expected: Int"));
        assert!(output.contains("Found:    String"));
    }

    #[test]
    fn test_empty_source() {
        let source = "";
        let diag = Diagnostic::error("Empty", "No source.", Span { start: 0, end: 0 });

        let output = render_diagnostic_plain(&diag, source, None);
        assert!(output.contains("Empty"));
    }

    #[test]
    fn test_single_char_error() {
        let source = "x + y";
        let diag = Diagnostic::error("Operator", "Invalid op.", Span { start: 2, end: 3 });

        let output = render_diagnostic_plain(&diag, source, None);
        assert!(output.contains("+"));
        assert!(output.contains("^"));
    }

    #[test]
    fn test_levenshtein_distance_identical() {
        assert_eq!(super::levenshtein_distance("hello", "hello"), 0);
    }

    #[test]
    fn test_levenshtein_distance_one_char() {
        assert_eq!(super::levenshtein_distance("hello", "hallo"), 1);
        assert_eq!(super::levenshtein_distance("cat", "car"), 1);
    }

    #[test]
    fn test_levenshtein_distance_insertion() {
        assert_eq!(super::levenshtein_distance("cat", "cats"), 1);
    }

    #[test]
    fn test_levenshtein_distance_deletion() {
        assert_eq!(super::levenshtein_distance("cats", "cat"), 1);
    }

    #[test]
    fn test_levenshtein_distance_empty() {
        assert_eq!(super::levenshtein_distance("", "hello"), 5);
        assert_eq!(super::levenshtein_distance("hello", ""), 5);
    }

    #[test]
    fn test_find_similar_names_basic() {
        let candidates = vec!["println", "print", "printf", "sprint"];
        let similar = super::find_similar_names("prnt", candidates.into_iter(), 2);
        assert!(similar.contains(&"print".to_string()));
    }

    #[test]
    fn test_find_similar_names_no_match() {
        let candidates = vec!["apple", "banana", "cherry"];
        let similar = super::find_similar_names("xyz", candidates.into_iter(), 2);
        assert!(similar.is_empty());
    }

    #[test]
    fn test_find_similar_names_excludes_exact() {
        let candidates = vec!["hello", "hallo", "world"];
        let similar = super::find_similar_names("hello", candidates.into_iter(), 2);
        assert!(!similar.contains(&"hello".to_string()));
        assert!(similar.contains(&"hallo".to_string()));
    }

    #[test]
    fn test_format_did_you_mean_none() {
        assert_eq!(super::format_did_you_mean(&[]), None);
    }

    #[test]
    fn test_format_did_you_mean_one() {
        let hint = super::format_did_you_mean(&["print".to_string()]);
        assert_eq!(hint, Some("Did you mean 'print'?".to_string()));
    }

    #[test]
    fn test_format_did_you_mean_two() {
        let hint = super::format_did_you_mean(&["print".to_string(), "println".to_string()]);
        assert_eq!(hint, Some("Did you mean 'print' or 'println'?".to_string()));
    }

    #[test]
    fn test_format_did_you_mean_three() {
        let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]);
        assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string()));
    }

    #[test]
    fn test_error_code_display() {
        assert_eq!(super::ErrorCode::E0201.code(), "E0201");
        assert_eq!(super::ErrorCode::E0201.title(), "Type Mismatch");
        assert!(super::ErrorCode::E0201.help_url().contains("E0201"));
    }

    #[test]
    fn test_diagnostic_with_code() {
        let diag = super::Diagnostic::with_code(
            super::ErrorCode::E0301,
            "Variable 'x' not found",
            Span { start: 0, end: 1 },
        );
        assert_eq!(diag.code, Some(super::ErrorCode::E0301));
        assert_eq!(diag.title, "Undefined Variable");
    }

    #[test]
    fn test_diagnostic_with_types() {
        let diag = super::Diagnostic::error("Test", "Msg", Span { start: 0, end: 1 })
            .with_types("Int", "String");
        assert_eq!(diag.expected_type, Some("Int".to_string()));
        assert_eq!(diag.actual_type, Some("String".to_string()));
    }

    #[test]
    fn test_type_diff_plain() {
        let diff = super::format_type_diff_plain("Int", "String");
        assert!(diff.contains("Expected: Int"));
        assert!(diff.contains("Found:    String"));
    }

    #[test]
    fn test_diagnostic_render_with_all_features() {
        let source = "let x: Int = \"hello\"";
        let diag = super::Diagnostic::with_code(
            super::ErrorCode::E0201,
            "This value should be an Int but is a String",
            Span { start: 13, end: 20 },
        )
        .with_types("Int", "String")
        .with_hint("Try using String.parseInt to convert the string to an integer");

        let output = super::render_diagnostic_plain(&diag, source, Some("test.lux"));

        // Check all features are present
        assert!(output.contains("[E0201]"));
        assert!(output.contains("Type Mismatch"));
        assert!(output.contains("Expected: Int"));
        assert!(output.contains("Found:    String"));
        assert!(output.contains("Hint:"));
        assert!(output.contains("parseInt"));
        assert!(output.contains("lux-lang.dev/errors/E0201"));
    }
}
