use super::{Keyword, Token, TokenPosition, TokenizedFile, split_visible_line_segments};

fn reconstruct_source(file: &TokenizedFile<'_>) -> String {
    file.buffer.iter().map(|piece| piece.lexeme).collect()
}

fn token_kinds_and_lexemes<'src>(file: &TokenizedFile<'src>) -> Vec<(Token, &'src str)> {
    file.buffer
        .iter()
        .map(|piece| (piece.token, piece.lexeme))
        .collect()
}

#[test]
fn split_visible_line_segments_returns_empty_for_single_line_text() {
    assert!(split_visible_line_segments("abcdef").is_empty());
    assert!(split_visible_line_segments("").is_empty());
}

#[test]
fn split_visible_line_segments_handles_mixed_line_endings() {
    let text = "ab\r\ncd\ref\n";
    let segments = split_visible_line_segments(text);

    assert_eq!(segments, vec![0..2, 4..6, 7..9, 10..10]);

    let visible: Vec<&str> = segments.iter().map(|range| &text[range.clone()]).collect();
    assert_eq!(visible, vec!["ab", "cd", "ef", ""]);
}

#[test]
fn tokenization_is_lossless_for_mixed_input() {
    let source = concat!(
        "class Foo extends Bar;\r\n",
        "var string S;\n",
        "/* block comment */\r",
        "defaultproperties {}\n",
        "X = 1.25e+2;\n",
    );

    let file = TokenizedFile::tokenize(source);

    assert_eq!(reconstruct_source(&file), source);
}

#[test]
fn trailing_newline_does_not_create_extra_empty_line() {
    let source = "a\n";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(file.lines.len(), 1);
    assert_eq!(file.lines[0].continued_from, None);
    assert_eq!(file.lines[0].local_range(), Some(0..2));

    assert_eq!(
        token_kinds_and_lexemes(&file),
        vec![(Token::Identifier, "a"), (Token::Newline, "\n")]
    );
}

#[test]
fn final_line_without_trailing_newline_is_committed() {
    let source = "a\nb";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(file.lines.len(), 2);

    assert_eq!(file.lines[0].continued_from, None);
    assert_eq!(file.lines[0].local_range(), Some(0..2));

    assert_eq!(file.lines[1].continued_from, None);
    assert_eq!(file.lines[1].local_range(), Some(2..3));

    assert_eq!(
        token_kinds_and_lexemes(&file),
        vec![
            (Token::Identifier, "a"),
            (Token::Newline, "\n"),
            (Token::Identifier, "b"),
        ]
    );
}

#[test]
fn multiline_block_comment_creates_continuation_line_with_local_tokens() {
    let source = "a/*x\ny*/b";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(
        token_kinds_and_lexemes(&file),
        vec![
            (Token::Identifier, "a"),
            (Token::BlockComment, "/*x\ny*/"),
            (Token::Identifier, "b"),
        ]
    );

    assert_eq!(file.lines.len(), 2);

    assert_eq!(file.lines[0].continued_from, None);
    assert_eq!(file.lines[0].local_range(), Some(0..2));

    assert_eq!(file.lines[1].continued_from, Some(0));
    assert_eq!(file.lines[1].local_range(), Some(2..3));

    let block_comment_index = 1;
    assert_eq!(
        file.multi_line_map.get(&block_comment_index),
        Some(&vec![0..3, 4..7])
    );
}

#[test]
fn pure_multiline_token_finishes_with_bare_continuation_line() {
    let source = "/*a\nb*/";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(
        token_kinds_and_lexemes(&file),
        vec![(Token::BlockComment, "/*a\nb*/")]
    );

    assert_eq!(file.lines.len(), 2);

    assert_eq!(file.lines[0].continued_from, None);
    assert_eq!(file.lines[0].local_range(), Some(0..1));

    assert_eq!(file.lines[1].continued_from, Some(0));
    assert_eq!(file.lines[1].local_range(), None);

    assert_eq!(file.multi_line_map.get(&0), Some(&vec![0..3, 4..7]));
}

#[test]
fn nested_block_comments_are_consumed_as_one_token() {
    let source = "/* outer /* inner */ still outer */";
    let file = TokenizedFile::tokenize(source);

    assert!(!file.has_errors());
    assert_eq!(file.buffer.len(), 1);
    assert_eq!(file.buffer[0].token, Token::BlockComment);
    assert_eq!(file.buffer[0].lexeme, source);
}

#[test]
fn cpptext_with_trivia_before_brace_produces_cpp_block_token() {
    let source = "cpptext /* gap */\n{ int x; if (y) { z(); } }";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(
        token_kinds_and_lexemes(&file),
        vec![
            (Token::Keyword(Keyword::CppText), "cpptext"),
            (Token::Whitespace, " "),
            (Token::BlockComment, "/* gap */"),
            (Token::Newline, "\n"),
            (Token::CppBlock, "{ int x; if (y) { z(); } }"),
        ]
    );

    assert_eq!(file.lines.len(), 2);

    assert_eq!(file.lines[0].continued_from, None);
    assert_eq!(file.lines[0].local_range(), Some(0..4));

    assert_eq!(file.lines[1].continued_from, None);
    assert_eq!(file.lines[1].local_range(), Some(4..5));
}

#[test]
fn cpptext_without_following_brace_does_not_start_cpp_block_mode() {
    let source = "cpptext Foo { bar }";
    let file = TokenizedFile::tokenize(source);

    let tokens = token_kinds_and_lexemes(&file);

    assert!(!tokens.iter().any(|(token, _)| *token == Token::CppBlock));
    assert!(
        tokens
            .iter()
            .any(|(token, lexeme)| *token == Token::Keyword(Keyword::CppText)
                && *lexeme == "cpptext")
    );
    assert!(
        tokens
            .iter()
            .any(|(token, lexeme)| *token == Token::LeftBrace && *lexeme == "{")
    );
    assert!(
        tokens
            .iter()
            .any(|(token, lexeme)| *token == Token::RightBrace && *lexeme == "}")
    );
}

#[test]
fn utf16_length_is_precomputed_per_token() {
    let source = "\"😀\"";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(file.buffer.len(), 1);
    assert_eq!(file.buffer[0].token, Token::StringLiteral);
    assert_eq!(file.buffer[0].utf16_length, source.encode_utf16().count());
    assert_eq!(file.buffer[0].utf16_length, 4);
}

#[test]
fn lexer_reports_error_tokens() {
    let source = "`";
    let file = TokenizedFile::tokenize(source);

    assert!(file.has_errors());
    assert_eq!(reconstruct_source(&file), source);
    assert_eq!(file.buffer.len(), 1);
    assert_eq!(file.buffer[0].token, Token::Error);
    assert_eq!(file.buffer[0].lexeme, "`");
}

#[test]
fn token_predicates_match_current_rules() {
    assert!(Token::Identifier.is_valid_identifier_name());
    assert!(Token::Keyword(Keyword::Int).is_valid_identifier_name());
    assert!(Token::Keyword(Keyword::Int).is_valid_type_name());
    assert!(Token::Keyword(Keyword::Delegate).is_valid_type_name());

    assert!(Token::Keyword(Keyword::Exec).is_valid_function_modifier());
    assert!(Token::Keyword(Keyword::Operator).is_valid_function_modifier());
    assert!(Token::Keyword(Keyword::Config).is_valid_function_modifier());

    assert!(!Token::Plus.is_valid_identifier_name());
    assert!(!Token::Plus.is_valid_type_name());
    assert!(!Token::Keyword(Keyword::If).is_valid_function_modifier());
}

#[test]
fn tokens_iterator_yields_positions_in_buffer_order() {
    let source = "a + b";
    let file = TokenizedFile::tokenize(source);

    let collected: Vec<_> = file.iter().collect();

    assert_eq!(collected.len(), file.buffer.len());

    for (expected_index, (position, token_data)) in collected.into_iter().enumerate() {
        assert_eq!(position.0, expected_index);
        assert_eq!(token_data, file.buffer[expected_index]);
    }
}

fn line_token_kinds_and_lexemes<'src>(
    file: &TokenizedFile<'src>,
    line_number: usize,
) -> Vec<(usize, Token, &'src str)> {
    file.line_tokens(line_number)
        .map(|(position, token_data)| (position.0, token_data.token, token_data.lexeme))
        .collect()
}

#[test]
fn line_count_counts_physical_lines_without_trailing_empty_line() {
    assert_eq!(TokenizedFile::tokenize("").line_count(), 0);
    assert_eq!(TokenizedFile::tokenize("a").line_count(), 1);
    assert_eq!(TokenizedFile::tokenize("a\n").line_count(), 1);
    assert_eq!(TokenizedFile::tokenize("a\nb\n").line_count(), 2);
}

#[test]
fn line_tokens_return_only_tokens_that_start_on_that_line() {
    let source = "a/*x\ny*/b\nc";
    let file = TokenizedFile::tokenize(source);

    assert_eq!(
        line_token_kinds_and_lexemes(&file, 0),
        vec![
            (0, Token::Identifier, "a"),
            (1, Token::BlockComment, "/*x\ny*/"),
        ]
    );

    // Important: the carried fragment "y*/" is NOT yielded here.
    assert_eq!(
        line_token_kinds_and_lexemes(&file, 1),
        vec![(2, Token::Identifier, "b"), (3, Token::Newline, "\n"),]
    );

    assert_eq!(
        line_token_kinds_and_lexemes(&file, 2),
        vec![(4, Token::Identifier, "c")]
    );
}

#[test]
fn line_tokens_are_empty_for_continuation_only_or_out_of_bounds_lines() {
    let file = TokenizedFile::tokenize("/*a\nb*/");

    assert_eq!(file.line_tokens(1).count(), 0);
    assert_eq!(file.line_tokens(999).count(), 0);
}

#[test]
fn token_at_returns_token_for_valid_position_and_none_for_invalid_one() {
    let file = TokenizedFile::tokenize("a + b");

    assert_eq!(file.token_at(TokenPosition(0)), Some(file.buffer[0]));
    assert_eq!(
        file.token_at(TokenPosition(1)).map(|t| t.token),
        Some(Token::Whitespace)
    );
    assert_eq!(
        file.token_at(TokenPosition(2)).map(|t| t.token),
        Some(Token::Plus)
    );
    assert_eq!(file.token_at(TokenPosition(file.buffer.len())), None);
}

#[test]
fn line_text_omits_line_terminators_and_handles_empty_lines() {
    let file = TokenizedFile::tokenize("left\n\nright");

    assert_eq!(file.line_text(0).as_deref(), Some("left"));
    assert_eq!(file.line_text(1).as_deref(), Some(""));
    assert_eq!(file.line_text(2).as_deref(), Some("right"));
    assert_eq!(file.line_text(999), None);
}

#[test]
fn line_text_includes_carried_fragment_on_continued_line() {
    let file = TokenizedFile::tokenize("a/*x\ny*/b");

    assert_eq!(file.line_text(1).as_deref(), Some("y*/b"));
}

#[test]
fn line_text_on_origin_line_of_multiline_token_uses_only_visible_part() {
    let file = TokenizedFile::tokenize("a/*x\ny*/b");

    assert_eq!(file.line_text(0).as_deref(), Some("a/*x"));
}
