pub mod runtime;
pub mod syntax;

use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
use std::borrow::Cow;

use crate::{
    ModuleLoader, ModuleResolver, Token, TokenKind,
    error::{runtime::RuntimeError, syntax::SyntaxError},
    module::{self, error::ModuleError},
};

#[allow(clippy::useless_conversion)]
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum InnerError {
    #[error(transparent)]
    Runtime(#[from] RuntimeError),
    #[error(transparent)]
    Syntax(#[from] SyntaxError),
    #[error(transparent)]
    Module(#[from] ModuleError),
}

impl InnerError {
    #[cold]
    pub fn token(&self) -> Option<&Token> {
        match self {
            InnerError::Syntax(err) => err.token(),
            InnerError::Runtime(err) => err.token(),
            InnerError::Module(err) => err.token(),
        }
    }

    /// Returns the opening delimiter token for errors that have one (e.g. unclosed brackets).
    #[cold]
    pub fn secondary_token(&self) -> Option<&Token> {
        match self {
            InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, opening))
            | InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, opening))
            | InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, opening)) => {
                opening.as_ref().map(|v| v.as_ref())
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, opening)))
            | InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, opening)))
            | InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, opening))) => {
                opening.as_ref().map(|v| v.as_ref())
            }
            _ => None,
        }
    }
}

/// Represents a high-level error with diagnostic information for the user.
#[derive(PartialEq, Debug, thiserror::Error)]
#[error("{cause}")]
pub struct Error {
    /// The underlying cause of the error.
    pub cause: InnerError,
    /// The named source code for diagnostics (includes the module filename when available).
    pub source_code: NamedSource<String>,
    /// The location in the source code for diagnostics.
    pub location: SourceSpan,
    /// The location of the opening delimiter, if applicable (e.g. unclosed `(`, `[`, `{`).
    pub secondary_location: Option<SourceSpan>,
}

impl Error {
    #[cold]
    pub fn from_error(
        top_level_source_code: impl Into<String>,
        cause: InnerError,
        module_loader: ModuleLoader<impl ModuleResolver>,
    ) -> Self {
        let source_code = top_level_source_code.into();
        let token = cause.token();

        match token {
            Some(token) => {
                let source_str = module_loader
                    .get_source_code(token.module_id, source_code)
                    .unwrap_or_default();
                let source_name = module_loader.module_file_name(token.module_id);

                let span_for = |t: &Token| {
                    SourceSpan::new(
                        SourceOffset::from_location(&source_str, t.range.start.line as usize, t.range.start.column),
                        std::cmp::max(
                            SourceOffset::from_location(&source_str, t.range.end.line as usize, t.range.end.column)
                                .offset()
                                .saturating_sub(
                                    SourceOffset::from_location(
                                        &source_str,
                                        t.range.start.line as usize,
                                        t.range.start.column,
                                    )
                                    .offset(),
                                ),
                            1,
                        ),
                    )
                };

                let location = span_for(token);
                let secondary_location = cause.secondary_token().map(span_for);

                Self {
                    cause,
                    source_code: NamedSource::new(source_name, source_str),
                    location,
                    secondary_location,
                }
            }
            None => {
                let (module_id, is_eof) = match &cause {
                    InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(module_id)) => (Some(module_id), true),
                    InnerError::Runtime(_) => (None, false),
                    InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(module_id))) => {
                        (Some(module_id), true)
                    }
                    _ => (None, false),
                };

                let source_name = module_id
                    .map(|id| module_loader.module_file_name(*id))
                    .unwrap_or_default();

                let source_str = module_id
                    .map(|id| {
                        module_loader
                            .get_source_code(*id, source_code.clone())
                            .unwrap_or_default()
                    })
                    .unwrap_or(source_code);

                let location = if is_eof {
                    let lines = source_str.lines();
                    let loc_line = lines.clone().count().saturating_sub(1);
                    let loc_col = lines.last().map(|lines| lines.len()).unwrap_or(0);
                    SourceSpan::new(SourceOffset::from_location(&source_str, loc_line, loc_col), 1)
                } else {
                    SourceSpan::new(SourceOffset::from_location(&source_str, 0, 0), 1)
                };

                Self {
                    cause,
                    source_code: NamedSource::new(source_name, source_str),
                    location,
                    secondary_location: None,
                }
            }
        }
    }
}

impl Diagnostic for Error {
    #[cold]
    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
        let code = match &self.cause {
            InnerError::Runtime(_) => "mq::runtime",
            InnerError::Syntax(_) => "mq::syntax",
            InnerError::Module(_) => "mq::module",
        };
        Some(Box::new(code) as Box<dyn std::fmt::Display>)
    }

    #[cold]
    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
        match &self.cause {
            InnerError::Runtime(RuntimeError::InvalidDefinition(_, _))
            | InnerError::Runtime(RuntimeError::InvalidTypes { .. }) => {
                Some(Box::new("https://mqlang.org/book") as Box<dyn std::fmt::Display>)
            }
            _ => None,
        }
    }

    #[cold]
    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
        let msg: Option<Cow<'static, str>> = match &self.cause {
            InnerError::Syntax(SyntaxError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
                "Environment variable '{env}' not found. Did you forget to set it?"
            ))),
            InnerError::Syntax(SyntaxError::UnexpectedToken(token)) if token.kind == TokenKind::Eof => {
                Some(Cow::Borrowed(
                    "The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
                ))
            }
            InnerError::Syntax(SyntaxError::UnexpectedToken(_)) => Some(Cow::Borrowed(
                "This token is not valid here. Check for typos, missing operators, or misplaced punctuation.",
            )),
            InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(_)) => Some(Cow::Borrowed(
                "Unexpected end of input. Check for missing closing brackets, parentheses, or incomplete expressions.",
            )),
            InnerError::Syntax(SyntaxError::InsufficientTokens(_)) => Some(Cow::Borrowed(
                "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.",
            )),
            InnerError::Syntax(SyntaxError::UnknownSelector(_)) => Some(Cow::Borrowed(
                "Unknown selector. Valid selectors include node types (e.g. .h1, .p, .code) and bracket access (e.g. .[0], .[n][m]).",
            )),
            InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, _)) => Some(Cow::Borrowed(
                "Expected a closing parenthesis ')'. Check your parentheses for balance.",
            )),
            InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, _)) => Some(Cow::Borrowed(
                "Expected a closing brace '}'. Check your braces for balance.",
            )),
            InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, _)) => Some(Cow::Borrowed(
                "Expected a closing bracket ']'. Check your brackets for balance.",
            )),
            InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => Some(Cow::Borrowed(
                "Invalid assignment target. Ensure you're assigning to a valid variable or property.",
            )),
            InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => Some(Cow::Borrowed(
                "Move this parameter before any parameters that have default values, or give it a default value.",
            )),
            InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => {
                Some(Cow::Borrowed("Macro parameters cannot have default values."))
            }
            InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => Some(Cow::Borrowed(
                "Variadic parameter (*) must be the last parameter in the parameter list.",
            )),
            InnerError::Syntax(SyntaxError::MultipleVariadicParameters(_)) => Some(Cow::Borrowed(
                "Only one variadic parameter (*) is allowed per function.",
            )),
            InnerError::Syntax(SyntaxError::MacroParametersCannotBeVariadic(_)) => {
                Some(Cow::Borrowed("Macro parameters cannot be variadic."))
            }
            InnerError::Syntax(SyntaxError::UnexpectedEOFAfterToken(_)) => Some(Cow::Borrowed(
                "An expression was expected here. Check for incomplete expressions after operators or keywords.",
            )),
            InnerError::Syntax(SyntaxError::UnmatchedEnd(_)) => Some(Cow::Borrowed(
                "This `end` keyword does not match any open block. \
                Note: single-line `if` expressions do not require `end`. \
                Check that each `end` closes a `def`, `fn`, `do`, `while`, `loop`, or `foreach` block.",
            )),
            InnerError::Runtime(RuntimeError::UserDefined { .. }) => {
                Some(Cow::Borrowed("A user-defined error occurred during evaluation."))
            }
            InnerError::Runtime(RuntimeError::InvalidBase64String(_, _)) => Some(Cow::Borrowed(
                "The provided string is not valid Base64. Check your input.",
            )),
            InnerError::Runtime(RuntimeError::NotDefined(_, name)) => Some(Cow::Owned(format!(
                "'{name}' is not defined. Did you forget to declare it?"
            ))),
            InnerError::Runtime(RuntimeError::DateTimeFormatError(_, _)) => Some(Cow::Borrowed(
                "Invalid date/time format. Please check your format string.",
            )),
            InnerError::Runtime(RuntimeError::IndexOutOfBounds(_, _)) => Some(Cow::Borrowed(
                "Index out of bounds. Check your array or string indices.",
            )),
            InnerError::Runtime(RuntimeError::InvalidDefinition(_, _)) => Some(Cow::Borrowed(
                "Invalid definition. Please check your function or variable declaration.",
            )),
            InnerError::Runtime(RuntimeError::AssignToImmutable(_, name)) => Some(Cow::Owned(format!(
                "Cannot assign to immutable variable '{name}'. Consider declaring it as mutable."
            ))),
            InnerError::Runtime(RuntimeError::UndefinedVariable(_, name)) => Some(Cow::Owned(format!(
                "Variable '{name}' is undefined. Did you forget to declare it?"
            ))),
            InnerError::Runtime(RuntimeError::InvalidTypes { .. }) => {
                Some(Cow::Borrowed("Type mismatch. Check the types of your operands."))
            }
            InnerError::Runtime(RuntimeError::InvalidNumberOfArguments {
                token: _,
                name: _,
                expected,
                actual,
            }) => Some(Cow::Owned(format!(
                "Invalid number of arguments: expected {expected}, got {actual}."
            ))),
            InnerError::Runtime(RuntimeError::InvalidRegularExpression(_, _)) => Some(Cow::Borrowed(
                "Invalid regular expression. Please check your regex syntax.",
            )),
            InnerError::Runtime(RuntimeError::InternalError(_)) => Some(Cow::Borrowed(
                "An internal error occurred. Please report this if it persists.",
            )),
            InnerError::Runtime(RuntimeError::Runtime(_, _)) => {
                Some(Cow::Borrowed("A runtime error occurred during evaluation."))
            }
            InnerError::Runtime(RuntimeError::ZeroDivision(_)) => {
                Some(Cow::Borrowed("Division by zero is not allowed."))
            }
            InnerError::Runtime(RuntimeError::RecursionError(_)) => {
                Some(Cow::Borrowed("Maximum recursion depth exceeded."))
            }
            InnerError::Runtime(RuntimeError::ModuleLoadError(_)) => {
                Some(Cow::Borrowed("Failed to load module. Check module paths and names."))
            }
            InnerError::Runtime(RuntimeError::UnexpectedBreak(_)) => {
                Some(Cow::Borrowed("'break' can only be used inside a loop."))
            }
            InnerError::Runtime(RuntimeError::UnexpectedContinue(_)) => {
                Some(Cow::Borrowed("'continue' can only be used inside a loop."))
            }
            InnerError::Runtime(RuntimeError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
                "Environment variable '{env}' not found. Did you forget to set it?"
            ))),
            InnerError::Runtime(RuntimeError::QuoteNotAllowedInRuntimeContext(_)) => Some(Cow::Borrowed(
                "quote() is not allowed in runtime context. It should only appear inside macros.",
            )),
            InnerError::Runtime(RuntimeError::UnquoteNotAllowedOutsideQuote(_)) => {
                Some(Cow::Borrowed("unquote() can only be used inside quote()."))
            }
            InnerError::Runtime(RuntimeError::InvalidConvert(_, msg)) => Some(Cow::Owned(format!(
                "Invalid conversion: {msg}. Check that the conversion is supported and value types match."
            ))),
            InnerError::Module(ModuleError::NotFound(name)) => Some(Cow::Owned(format!(
                "Module '{name}' not found. Check the module name or path."
            ))),
            InnerError::Module(ModuleError::AlreadyLoaded(name)) => {
                Some(Cow::Owned(format!("Module '{name}' is already loaded.")))
            }
            InnerError::Module(ModuleError::IOError(_)) => Some(Cow::Borrowed(
                "An I/O error occurred while loading a module. Check file permissions and paths.",
            )),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, env))) => {
                Some(Cow::Owned(format!("Environment variable '{env}' not found in module.")))
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(token)))
                if token.kind == TokenKind::Eof =>
            {
                Some(Cow::Borrowed(
                    "The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
                ))
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(_))) => Some(Cow::Borrowed(
                "This token is not valid here. Check for typos, missing operators, or misplaced punctuation.",
            )),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(_))) => Some(Cow::Borrowed(
                "Unexpected end of input. Check for missing closing brackets, parentheses, or incomplete expressions.",
            )),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(_))) => Some(Cow::Borrowed(
                "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.",
            )),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, _))) => Some(
                Cow::Borrowed("Expected a closing bracket ']'. Check your brackets for balance."),
            ),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, _))) => Some(
                Cow::Borrowed("Expected a closing brace '}'. Check your braces for balance."),
            ),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, _))) => Some(
                Cow::Borrowed("Expected a closing parenthesis ')'. Check your parentheses for balance."),
            ),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InvalidAssignmentTarget(_))) => Some(
                Cow::Borrowed("Invalid assignment target. Ensure you're assigning to a valid variable or property."),
            ),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnknownSelector(_))) => Some(Cow::Borrowed(
                "Unknown selector. Valid selectors include node types (e.g. .h1, .p, .code) and bracket access (e.g. .[0], .[n][m]).",
            )),
            InnerError::Module(ModuleError::InvalidModule) => Some(Cow::Borrowed("Invalid module format or content.")),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ParameterWithoutDefaultAfterDefault(_))) => {
                Some(Cow::Borrowed(
                    "Move this parameter before any parameters that have default values, or give it a default value.",
                ))
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotHaveDefaults(_))) => {
                Some(Cow::Borrowed("Macro parameters cannot have default values."))
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::VariadicParameterMustBeLast(_))) => Some(
                Cow::Borrowed("Variadic parameter (*) must be the last parameter in the parameter list."),
            ),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MultipleVariadicParameters(_))) => Some(
                Cow::Borrowed("Only one variadic parameter (*) is allowed per function."),
            ),
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotBeVariadic(_))) => {
                Some(Cow::Borrowed("Macro parameters cannot be variadic."))
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFAfterToken(_))) => {
                Some(Cow::Borrowed(
                    "An expression was expected here. Check for incomplete expressions after operators or keywords.",
                ))
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnmatchedEnd(_))) => Some(Cow::Borrowed(
                "This `end` keyword does not match any open block. \
                Note: single-line `if` expressions do not require `end`. \
                Check that each `end` closes a `def`, `fn`, `do`, `while`, `loop`, or `foreach` block.",
            )),
            InnerError::Runtime(RuntimeError::UndefinedMacro(_)) => {
                Some(Cow::Borrowed("Macro expansion error: undefined macro used."))
            }
            InnerError::Runtime(RuntimeError::ArityMismatch { .. }) => {
                Some(Cow::Borrowed("Macro expansion error: macro arity mismatch."))
            }
            InnerError::Runtime(RuntimeError::RecursionLimit) => {
                Some(Cow::Borrowed("Macro expansion error: recursion limit exceeded."))
            }
            InnerError::Runtime(RuntimeError::InvalidMacroResultAst(_)) => {
                Some(Cow::Borrowed("Invalid macro result AST during macro expansion."))
            }
            InnerError::Runtime(RuntimeError::InvalidMacroResult(_)) => Some(Cow::Borrowed(
                "Invalid macro result: expected AST value during macro body evaluation.",
            )),
            InnerError::Runtime(RuntimeError::DestructuringFailed(_)) => Some(Cow::Borrowed(
                "Destructuring pattern did not match the value. Check that the pattern structure matches the value.",
            )),
            #[cfg(feature = "http-import")]
            InnerError::Module(ModuleError::HttpImportNotAllowed(_)) => Some(Cow::Borrowed(
                "HTTP imports are only allowed at the top level. \
                Move the HTTP import to the top-level script instead of inside an imported module.",
            )),
        };

        msg.map(|m| Box::new(m) as Box<dyn std::fmt::Display>)
    }

    #[cold]
    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
        let label = match &self.cause {
            InnerError::Syntax(SyntaxError::UnexpectedToken(_)) => "unexpected token",
            InnerError::Syntax(SyntaxError::InsufficientTokens(_)) => "expression incomplete here",
            InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, _)) => "expected `)` here",
            InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, _)) => "expected `}` here",
            InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, _)) => "expected `]` here",
            InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => "invalid assignment target",
            InnerError::Syntax(SyntaxError::UnknownSelector(_)) => "unknown selector",
            InnerError::Syntax(SyntaxError::EnvNotFound(_, _)) => "environment variable not found",
            InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => "parameter without default",
            InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => "parameter with default value",
            InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => "misplaced variadic parameter",
            InnerError::Syntax(SyntaxError::MultipleVariadicParameters(_)) => "duplicate variadic parameter",
            InnerError::Syntax(SyntaxError::MacroParametersCannotBeVariadic(_)) => "variadic macro parameter",
            InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(_)) => "unexpected end of input",
            InnerError::Syntax(SyntaxError::UnexpectedEOFAfterToken(_)) => "expected expression here",
            InnerError::Syntax(SyntaxError::UnmatchedEnd(_)) => "unmatched `end` keyword",
            InnerError::Runtime(_) => "error occurred here",
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(_))) => "unexpected token",
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(_))) => {
                "expression incomplete here"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, _))) => {
                "expected `)` here"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, _))) => {
                "expected `}` here"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, _))) => {
                "expected `]` here"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::InvalidAssignmentTarget(_))) => {
                "invalid assignment target"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnknownSelector(_))) => "unknown selector",
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, _))) => {
                "environment variable not found"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::ParameterWithoutDefaultAfterDefault(_))) => {
                "parameter without default"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotHaveDefaults(_))) => {
                "parameter with default value"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::VariadicParameterMustBeLast(_))) => {
                "misplaced variadic parameter"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MultipleVariadicParameters(_))) => {
                "duplicate variadic parameter"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotBeVariadic(_))) => {
                "variadic macro parameter"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(_))) => {
                "unexpected end of input"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFAfterToken(_))) => {
                "expected expression here"
            }
            InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnmatchedEnd(_))) => "unmatched `end` keyword",
            InnerError::Module(_) => "module error here",
        };

        let primary = miette::LabeledSpan::new_with_span(Some(label.to_string()), self.location);

        if let Some(secondary_span) = self.secondary_location {
            Some(Box::new(
                [
                    miette::LabeledSpan::new_with_span(Some("opened here".to_string()), secondary_span),
                    primary,
                ]
                .into_iter(),
            ) as Box<dyn Iterator<Item = miette::LabeledSpan>>)
        } else {
            Some(Box::new(std::iter::once(primary)))
        }
    }

    #[cold]
    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
        Some(&self.source_code as &dyn miette::SourceCode)
    }
}

#[cfg(test)]
mod test {
    use rstest::{fixture, rstest};
    use scopeguard::defer;
    use std::io::Write;
    use std::{fs::File, path::PathBuf};

    use super::*;
    use crate::module::resolver::DefaultModuleResolver;
    use crate::{Arena, Range, Shared, SharedCell, Token, TokenKind, arena::ArenaId};

    type TempDir = PathBuf;
    type TempFile = PathBuf;

    fn create_file(name: &str, content: &str) -> (TempDir, TempFile) {
        let temp_dir = std::env::temp_dir();
        let temp_file_path = temp_dir.join(name);
        let mut file = File::create(&temp_file_path).expect("Failed to create temp file");
        file.write_all(content.as_bytes())
            .expect("Failed to write to temp file");

        (temp_dir, temp_file_path)
    }

    #[fixture]
    fn module_loader() -> ModuleLoader {
        ModuleLoader::default()
    }

    #[test]
    fn test_from_error_with_eof_error() {
        let cause = InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)));
        let module_loader: ModuleLoader = ModuleLoader::default();
        let error = Error::from_error("line 1\nline 2", cause, module_loader);

        assert_eq!(error.source_code.inner(), "line 1\nline 2");
    }

    #[rstest]
    #[case::parse_unexpected_token(
        InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })),
        "source code"
    )]
    #[case::parse_unexpected_eof_detected(
        InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))),
        "source code"
    )]
    #[case::parse_env_not_found(
        InnerError::Syntax(SyntaxError::EnvNotFound(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "ENV_VAR".into())),
        "source code"
    )]
    #[case::parse_env_not_found(
        InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })),
        "source code"
    )]
    #[case::parse_env_not_found(
        InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })),
        "source code"
    )]
    #[case::eval_zero_division(
        InnerError::Runtime(RuntimeError::ZeroDivision(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })),
        "source code"
    )]
    #[case::eval_invalid_base64_string(
        InnerError::Runtime(RuntimeError::InvalidBase64String(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "".to_string())),
        "source code"
    )]
    #[case::eval_not_defined(
        InnerError::Runtime(RuntimeError::NotDefined(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "".to_string())),
        "source code"
    )]
    #[case::eval_index_out_of_bounds(
        InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, 1.into())),
        "source code"
    )]
    #[case::eval_invalid_definition(
        InnerError::Runtime(RuntimeError::InvalidDefinition(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "".to_string())),
        "source code"
    )]
    #[case::eval_invalid_number_of_arguments(
        InnerError::Runtime(RuntimeError::InvalidNumberOfArguments{token: Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, name: "".to_string(), expected: 1, actual:1}),
        "source code"
    )]
    #[case::eval_invalid_regular_expression(
        InnerError::Runtime(RuntimeError::InvalidRegularExpression(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "".to_string())),
        "source code"
    )]
    #[case::eval_internal_error(
        InnerError::Runtime(RuntimeError::InternalError(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })),
        "source code"
    )]
    #[case::eval_internal_error(
        InnerError::Runtime(RuntimeError::Runtime(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "".to_string())),
        "source code"
    )]
    #[case::module_not_found(InnerError::Module(ModuleError::NotFound(Cow::Borrowed("test"))), "source code")]
    #[case::module_io_error(InnerError::Module(ModuleError::IOError(Cow::Borrowed("test"))), "source code")]
    #[case::module_parse_error(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "test".into()))),
        "source code"
    )]
    #[case::module_parse_error(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }))),
        "source code"
    )]
    #[case::module_parse_error(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)))),
        "source code"
    )]
    #[case::module_parse_error(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }
        ))),
        "source code"
    )]
    fn test_from_error(
        module_loader: module::ModuleLoader<impl ModuleResolver>,
        #[case] cause: InnerError,
        #[case] source_code: &str,
    ) {
        let error = Error::from_error(source_code, cause, module_loader);
        assert_eq!(error.source_code.inner(), source_code);
    }

    #[test]
    fn test_from_error_with_module_source() {
        let (temp_dir, temp_file_path) = create_file(
            "test_from_error_with_module_source.mq",
            "def func1(): 42; | let val1 = 1",
        );

        defer! {
            if temp_file_path.exists() {
                std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file");
            }
        }

        let token_arena = Shared::new(SharedCell::new(Arena::new(10)));
        let mut loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()]));

        loader
            .load_from_file("test_from_error_with_module_source", token_arena)
            .unwrap();

        let token = Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(1),
        };

        let cause = InnerError::Runtime(RuntimeError::ZeroDivision(token));
        let error = Error::from_error("top level source", cause, loader);

        assert_eq!(error.source_code.inner(), "def func1(): 42; | let val1 = 1");
    }

    #[test]
    fn test_from_error_with_builtin_module() {
        let token_arena = Shared::new(SharedCell::new(Arena::new(10)));
        let mut loader: ModuleLoader = ModuleLoader::default();
        loader.load_builtin(token_arena).unwrap();
        let token = Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(1),
        };

        let cause = InnerError::Runtime(RuntimeError::ZeroDivision(token));
        let error = Error::from_error("top level source", cause, loader);

        assert_eq!(error.source_code.inner(), module::BUILTIN_FILE);
    }

    #[rstest]
    #[case::parse_env_not_found(
        InnerError::Syntax(SyntaxError::EnvNotFound(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "ENV_VAR".into()))
    )]
    #[case::parse_unexpected_token(
        InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }))
    )]
    #[case::parse_unexpected_eof_detected(InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))))]
    #[case::parse_insufficient_tokens(
        InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }))
    )]
    #[case::parse_expected_closing_paren(
        InnerError::Syntax(SyntaxError::ExpectedClosingParen(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, None))
    )]
    #[case::parse_expected_closing_brace(
        InnerError::Syntax(SyntaxError::ExpectedClosingBrace(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, None))
    )]
    #[case::parse_expected_closing_bracket(
        InnerError::Syntax(SyntaxError::ExpectedClosingBracket(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, None))
    )]
    #[case::eval_recursion_error(InnerError::Runtime(RuntimeError::RecursionError(0)))]
    #[case::eval_module_load_error(
        InnerError::Runtime(RuntimeError::ModuleLoadError(ModuleError::NotFound("mod".into())))
    )]
    #[case::eval_user_defined(
        InnerError::Runtime(RuntimeError::UserDefined {
            token: Token {
                range: Range::default(),
                kind: TokenKind::Eof,
                module_id: ArenaId::new(0),
            },
            message: "msg".to_string(),
        })
    )]
    #[case::eval_invalid_base64_string(
        InnerError::Runtime(RuntimeError::InvalidBase64String(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "bad".to_string()))
    )]
    #[case::eval_not_defined(
        InnerError::Runtime(RuntimeError::NotDefined(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "name".to_string()))
    )]
    #[case::eval_datetime_format_error(
        InnerError::Runtime(RuntimeError::DateTimeFormatError(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "fmt".to_string()))
    )]
    #[case::eval_index_out_of_bounds(
        InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, 1.into()))
    )]
    #[case::eval_invalid_definition(
        InnerError::Runtime(RuntimeError::InvalidDefinition(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "bad".into()))
    )]
    #[case::eval_invalid_types(
        InnerError::Runtime(RuntimeError::InvalidTypes {
            token: Token {
                range: Range::default(),
                kind: TokenKind::Eof,
                module_id: ArenaId::new(0),
            },
            name: "int".into(),
            args: vec!["str".into()],
        })
    )]
    #[case::eval_invalid_number_of_arguments(
        InnerError::Runtime(RuntimeError::InvalidNumberOfArguments{token: Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, name: "func".to_string(), expected: 2, actual: 1})
    )]
    #[case::eval_invalid_regular_expression(
        InnerError::Runtime(RuntimeError::InvalidRegularExpression(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "bad".to_string()))
    )]
    #[case::eval_internal_error(
        InnerError::Runtime(RuntimeError::InternalError(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }))
    )]
    #[case::eval_runtime_error(
        InnerError::Runtime(RuntimeError::Runtime(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "err".to_string()))
    )]
    #[case::eval_zero_division(
        InnerError::Runtime(RuntimeError::ZeroDivision(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }))
    )]
    #[case::eval_unexpected_break(InnerError::Runtime(RuntimeError::UnexpectedBreak(Token {
        range: Range::default(),
        kind: TokenKind::Eof,
        module_id: ArenaId::new(0),
    })))]
    #[case::eval_unexpected_continue(InnerError::Runtime(RuntimeError::UnexpectedContinue(Token {
        range: Range::default(),
        kind: TokenKind::Eof,
        module_id: ArenaId::new(0),
    })))]
    #[case::eval_env_not_found(
        InnerError::Runtime(RuntimeError::EnvNotFound(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "ENV".into()))
    )]
    #[case::module_not_found(InnerError::Module(ModuleError::NotFound(Cow::Borrowed("mod"))))]
    #[case::module_io_error(InnerError::Module(ModuleError::IOError(Cow::Borrowed("io"))))]
    #[case::module_parse_error_env_not_found(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, "ENV".into())))
    )]
    #[case::module_parse_error_unexpected_token(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })))
    )]
    #[case::module_parse_error_unexpected_eof(InnerError::Module(ModuleError::SyntaxError(
        SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))
    )))]
    #[case::module_parse_error_insufficient_tokens(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        })))
    )]
    #[case::module_invalid_module(InnerError::Module(ModuleError::InvalidModule))]
    #[case::module_parse_error_expected_closing_paren(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, None)))
    )]
    #[case::module_parse_error_expected_closing_brace(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, None)))
    )]
    #[case::module_parse_error_expected_closing_bracket(
        InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }, None)))
    )]
    fn test_diagnostic_code_and_help(module_loader: ModuleLoader<impl ModuleResolver>, #[case] cause: InnerError) {
        let error = Error::from_error("source code", cause, module_loader);
        // code() and help() must not panic
        let _ = error.code();
        let _ = error.help();
    }

    #[test]
    fn test_code_shows_category() {
        let module_loader: ModuleLoader = ModuleLoader::default();
        let runtime_error = Error::from_error(
            "source code",
            InnerError::Runtime(RuntimeError::ZeroDivision(Token {
                range: Range::default(),
                kind: TokenKind::Eof,
                module_id: ArenaId::new(0),
            })),
            module_loader.clone(),
        );
        assert_eq!(
            runtime_error.code().map(|c| c.to_string()),
            Some("mq::runtime".to_string())
        );

        let syntax_error = Error::from_error(
            "source code",
            InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
                range: Range::default(),
                kind: TokenKind::Eof,
                module_id: ArenaId::new(0),
            })),
            module_loader,
        );
        assert_eq!(
            syntax_error.code().map(|c| c.to_string()),
            Some("mq::syntax".to_string())
        );
    }

    #[test]
    fn test_env_not_found_help_includes_name() {
        let module_loader: ModuleLoader = ModuleLoader::default();
        let cause = InnerError::Runtime(RuntimeError::EnvNotFound(
            Token {
                range: Range::default(),
                kind: TokenKind::Eof,
                module_id: ArenaId::new(0),
            },
            "MY_VAR".into(),
        ));
        let error = Error::from_error("source code", cause, module_loader);
        let help = error.help().map(|h| h.to_string());
        assert_eq!(
            help,
            Some("Environment variable 'MY_VAR' not found. Did you forget to set it?".to_string())
        );
    }

    #[test]
    fn test_invalid_convert_help_shows_message() {
        let module_loader: ModuleLoader = ModuleLoader::default();
        let cause = InnerError::Runtime(RuntimeError::InvalidConvert(
            Token {
                range: Range::default(),
                kind: TokenKind::Eof,
                module_id: ArenaId::new(0),
            },
            "cannot convert array to string".to_string(),
        ));
        let error = Error::from_error("source code", cause, module_loader);
        let help = error.help().map(|h| h.to_string());
        assert!(
            help.as_deref().unwrap_or("").contains("cannot convert array to string"),
            "help text should contain the conversion message, got: {help:?}"
        );
    }
}
