//! End-to-end lexer tests.

use ecma_lex_cat::error::Error;
use ecma_lex_cat::token::TokenKind;
use ecma_lex_cat::{Token, lex};

#[derive(Debug)]
enum TestFailure {
    Lex(Error),
    Assertion {
        what: &'static str,
        actual: String,
        expected: String,
    },
}

impl From<Error> for TestFailure {
    fn from(value: Error) -> Self {
        Self::Lex(value)
    }
}

impl std::fmt::Display for TestFailure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Lex(e) => write!(f, "lex error: {e}"),
            Self::Assertion {
                what,
                actual,
                expected,
            } => write!(
                f,
                "assertion {what:?} failed: expected {expected:?}, got {actual:?}"
            ),
        }
    }
}

fn check_kinds(
    tokens: &[Token],
    expected: &[TokenKind],
    what: &'static str,
) -> Result<(), TestFailure> {
    let actual: Vec<TokenKind> = tokens.iter().map(|t| t.value().clone()).collect();
    (actual == expected)
        .then_some(())
        .ok_or(TestFailure::Assertion {
            what,
            actual: format!("{actual:?}"),
            expected: format!("{expected:?}"),
        })
}

#[test]
fn lex_simple_let_assignment() -> Result<(), TestFailure> {
    let tokens = lex("let x = 42;")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::KwLet,
            TokenKind::Identifier("x".to_owned()),
            TokenKind::Eq,
            TokenKind::Number(42.0),
            TokenKind::Semicolon,
            TokenKind::Eof,
        ],
        "let x = 42;",
    )
}

#[test]
fn lex_arithmetic_and_compare() -> Result<(), TestFailure> {
    let tokens = lex("a + b * c === d / e")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Identifier("a".to_owned()),
            TokenKind::Plus,
            TokenKind::Identifier("b".to_owned()),
            TokenKind::Star,
            TokenKind::Identifier("c".to_owned()),
            TokenKind::EqEqEq,
            TokenKind::Identifier("d".to_owned()),
            TokenKind::Slash,
            TokenKind::Identifier("e".to_owned()),
            TokenKind::Eof,
        ],
        "arithmetic + compare",
    )
}

#[test]
fn lex_all_numeric_forms() -> Result<(), TestFailure> {
    let tokens = lex("0 1.5 0x1A 0o17 0b101 1e3 .5 100n")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Number(0.0),
            TokenKind::Number(1.5),
            TokenKind::Number(26.0),
            TokenKind::Number(15.0),
            TokenKind::Number(5.0),
            TokenKind::Number(1000.0),
            TokenKind::Number(0.5),
            TokenKind::BigInt("100".to_owned()),
            TokenKind::Eof,
        ],
        "numeric forms",
    )
}

#[test]
fn lex_string_with_escapes() -> Result<(), TestFailure> {
    let tokens = lex(r#""hello\nworld\t!""#)?;
    check_kinds(
        &tokens,
        &[
            TokenKind::String("hello\nworld\t!".to_owned()),
            TokenKind::Eof,
        ],
        "string escapes",
    )
}

#[test]
fn lex_template_no_substitution() -> Result<(), TestFailure> {
    let tokens = lex("`hello world`")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::TemplateNoSubstitution("hello world".to_owned()),
            TokenKind::Eof,
        ],
        "template no subst",
    )
}

#[test]
fn lex_template_with_substitution() -> Result<(), TestFailure> {
    let tokens = lex("`hi ${name}!`")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::TemplateHead("hi ".to_owned()),
            TokenKind::Identifier("name".to_owned()),
            TokenKind::TemplateTail("!".to_owned()),
            TokenKind::Eof,
        ],
        "template with subst",
    )
}

#[test]
fn lex_regex_after_operator() -> Result<(), TestFailure> {
    let tokens = lex("x = /foo/gi")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Identifier("x".to_owned()),
            TokenKind::Eq,
            TokenKind::RegExp {
                pattern: "foo".to_owned(),
                flags: "gi".to_owned(),
            },
            TokenKind::Eof,
        ],
        "regex after assignment",
    )
}

#[test]
fn lex_division_after_identifier() -> Result<(), TestFailure> {
    let tokens = lex("a / b")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Identifier("a".to_owned()),
            TokenKind::Slash,
            TokenKind::Identifier("b".to_owned()),
            TokenKind::Eof,
        ],
        "division after ident",
    )
}

#[test]
fn lex_optional_chaining() -> Result<(), TestFailure> {
    let tokens = lex("a?.b ?? c")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Identifier("a".to_owned()),
            TokenKind::OptionalChain,
            TokenKind::Identifier("b".to_owned()),
            TokenKind::QQ,
            TokenKind::Identifier("c".to_owned()),
            TokenKind::Eof,
        ],
        "optional chaining + nullish",
    )
}

#[test]
fn lex_spread_vs_dot() -> Result<(), TestFailure> {
    let tokens = lex("...args")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Spread,
            TokenKind::Identifier("args".to_owned()),
            TokenKind::Eof,
        ],
        "spread",
    )
}

#[test]
fn lex_arrow_function() -> Result<(), TestFailure> {
    let tokens = lex("(x) => x + 1")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::LParen,
            TokenKind::Identifier("x".to_owned()),
            TokenKind::RParen,
            TokenKind::Arrow,
            TokenKind::Identifier("x".to_owned()),
            TokenKind::Plus,
            TokenKind::Number(1.0),
            TokenKind::Eof,
        ],
        "arrow function",
    )
}

#[test]
fn lex_private_identifier() -> Result<(), TestFailure> {
    let tokens = lex("this.#count")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::KwThis,
            TokenKind::Dot,
            TokenKind::PrivateIdentifier("count".to_owned()),
            TokenKind::Eof,
        ],
        "private identifier",
    )
}

#[test]
fn lex_comments_are_skipped() -> Result<(), TestFailure> {
    let tokens = lex("// line\n/* block */ x")?;
    check_kinds(
        &tokens,
        &[TokenKind::Identifier("x".to_owned()), TokenKind::Eof],
        "comments skipped",
    )
}

#[test]
fn lex_all_assignment_operators() -> Result<(), TestFailure> {
    let tokens = lex("a += 1; b -= 2; c **= 3; d ??= 4")?;
    check_kinds(
        &tokens,
        &[
            TokenKind::Identifier("a".to_owned()),
            TokenKind::PlusEq,
            TokenKind::Number(1.0),
            TokenKind::Semicolon,
            TokenKind::Identifier("b".to_owned()),
            TokenKind::MinusEq,
            TokenKind::Number(2.0),
            TokenKind::Semicolon,
            TokenKind::Identifier("c".to_owned()),
            TokenKind::StarStarEq,
            TokenKind::Number(3.0),
            TokenKind::Semicolon,
            TokenKind::Identifier("d".to_owned()),
            TokenKind::QQEq,
            TokenKind::Number(4.0),
            TokenKind::Eof,
        ],
        "compound assignments",
    )
}

#[test]
fn lex_unterminated_string_errors() -> Result<(), TestFailure> {
    let result = lex("\"unfinished");
    match result {
        Err(Error::UnterminatedString { .. }) => Ok(()),
        Err(other) => Err(TestFailure::Lex(other)),
        Ok(_) => Err(TestFailure::Assertion {
            what: "unterminated string rejected",
            actual: "no error".to_owned(),
            expected: "UnterminatedString".to_owned(),
        }),
    }
}

#[test]
fn lex_unterminated_block_comment_errors() -> Result<(), TestFailure> {
    let result = lex("/* never closes");
    match result {
        Err(Error::UnterminatedComment { .. }) => Ok(()),
        Err(other) => Err(TestFailure::Lex(other)),
        Ok(_) => Err(TestFailure::Assertion {
            what: "unterminated comment rejected",
            actual: "no error".to_owned(),
            expected: "UnterminatedComment".to_owned(),
        }),
    }
}

#[test]
fn lex_keywords_distinguished_from_identifiers() -> Result<(), TestFailure> {
    let tokens = lex("function async let await yield static of as from")?;
    let actual: Vec<TokenKind> = tokens.iter().map(|t| t.value().clone()).collect();
    // `function`, `let`, `await`, `yield`, `static` are reserved keywords;
    // `async`, `of`, `as`, `from` are contextual identifiers.
    let expected = vec![
        TokenKind::KwFunction,
        TokenKind::Identifier("async".to_owned()),
        TokenKind::KwLet,
        TokenKind::KwAwait,
        TokenKind::KwYield,
        TokenKind::KwStatic,
        TokenKind::Identifier("of".to_owned()),
        TokenKind::Identifier("as".to_owned()),
        TokenKind::Identifier("from".to_owned()),
        TokenKind::Eof,
    ];
    (actual == expected)
        .then_some(())
        .ok_or(TestFailure::Assertion {
            what: "keyword vs contextual ident",
            actual: format!("{actual:?}"),
            expected: format!("{expected:?}"),
        })
}

#[test]
fn lex_unicode_escape_in_string() -> Result<(), TestFailure> {
    let tokens = lex(r#""\u{1F600}""#)?;
    check_kinds(
        &tokens,
        &[TokenKind::String("\u{1F600}".to_owned()), TokenKind::Eof],
        "unicode braced escape",
    )
}

#[test]
fn lex_hex_escape_in_string() -> Result<(), TestFailure> {
    let tokens = lex(r#""\x41\x42\x43""#)?;
    check_kinds(
        &tokens,
        &[TokenKind::String("ABC".to_owned()), TokenKind::Eof],
        "hex escapes",
    )
}

#[test]
fn lex_span_tracks_line_and_column() -> Result<(), TestFailure> {
    let tokens = lex("a\nb")?;
    let a_span = tokens
        .first()
        .ok_or(TestFailure::Assertion {
            what: "two tokens",
            actual: "fewer".to_owned(),
            expected: "at least one".to_owned(),
        })?
        .span();
    let b_span = tokens
        .get(1)
        .ok_or(TestFailure::Assertion {
            what: "two tokens",
            actual: "fewer".to_owned(),
            expected: "at least two".to_owned(),
        })?
        .span();
    let lines_correct = a_span.start().line() == 1 && b_span.start().line() == 2;
    lines_correct.then_some(()).ok_or(TestFailure::Assertion {
        what: "newline advances line",
        actual: format!("a={}, b={}", a_span.start(), b_span.start()),
        expected: "a on line 1, b on line 2".to_owned(),
    })
}
