package p.studio.compiler.pbs.lexer;

import org.junit.jupiter.api.Test;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;

import java.nio.charset.StandardCharsets;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PbsLexerTest {

    @Test
    void shouldLexFunctionTokens() {
        final var source = """
            fn sum(a: int, b: int): int
            {
                return a + b;
            }
            """;
        final var diagnostics = DiagnosticSink.empty();

        final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics);

        assertEquals(PbsTokenKind.FN, tokens.get(0).kind());
        assertEquals(PbsTokenKind.IDENTIFIER, tokens.get(1).kind());
        assertEquals(PbsTokenKind.LEFT_PAREN, tokens.get(2).kind());
        assertEquals(PbsTokenKind.RETURN, tokens.stream()
                .filter(t -> t.kind() == PbsTokenKind.RETURN)
                .findFirst()
                .orElseThrow()
                .kind());
        assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
        assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input");
    }

    @Test
    void shouldLexCoreKeywordsOperatorsAndCommentToken() {
        final var source = """
            import { Item as Alias } from @core:math;
            declare const PI: float = 3.14;
            fn run(a: int) -> int {
                let const x: int = 1;
                x += 1;
                if true and false || !false {
                    return x;
                }
                return x;
            }
            // trailing comment
            """;
        final var diagnostics = DiagnosticSink.empty();

        final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics);
        final List<PbsTokenKind> kinds = tokens.stream().map(PbsToken::kind).toList();

        assertTrue(kinds.contains(PbsTokenKind.IMPORT));
        assertTrue(kinds.contains(PbsTokenKind.AS));
        assertTrue(kinds.contains(PbsTokenKind.DECLARE));
        assertTrue(kinds.contains(PbsTokenKind.CONST));
        assertTrue(kinds.contains(PbsTokenKind.ARROW));
        assertTrue(kinds.contains(PbsTokenKind.PLUS_EQUAL));
        assertTrue(kinds.contains(PbsTokenKind.AND));
        assertTrue(kinds.contains(PbsTokenKind.OR_OR));
        assertTrue(kinds.contains(PbsTokenKind.COMMENT));
        assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
        assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input");
    }

    @Test
    void shouldTokenizeReservedKeywordsAndMemberOperators() {
        final var source = """
            apply bind new implements using ctor host enum callback Self this mut const
            switch default until step while break continue return void optional result
            some none ok err handle and or not spawn yield sleep match type mod a.b a..b ?
            """;
        final var diagnostics = DiagnosticSink.empty();

        final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics);
        final List<PbsTokenKind> kinds = tokens.stream().map(PbsToken::kind).toList();

        assertTrue(kinds.contains(PbsTokenKind.APPLY));
        assertTrue(kinds.contains(PbsTokenKind.BIND));
        assertTrue(kinds.contains(PbsTokenKind.NEW));
        assertTrue(kinds.contains(PbsTokenKind.IMPLEMENTS));
        assertTrue(kinds.contains(PbsTokenKind.USING));
        assertTrue(kinds.contains(PbsTokenKind.CTOR));
        assertTrue(kinds.contains(PbsTokenKind.HOST));
        assertTrue(kinds.contains(PbsTokenKind.ENUM));
        assertTrue(kinds.contains(PbsTokenKind.CALLBACK));
        assertTrue(kinds.contains(PbsTokenKind.SELF));
        assertTrue(kinds.contains(PbsTokenKind.THIS));
        assertTrue(kinds.contains(PbsTokenKind.SPAWN));
        assertTrue(kinds.contains(PbsTokenKind.MATCH));
        assertTrue(kinds.contains(PbsTokenKind.DOT));
        assertTrue(kinds.contains(PbsTokenKind.DOT_DOT));
        assertTrue(kinds.contains(PbsTokenKind.QUESTION));
        assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
        assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input");
    }

    @Test
    void shouldReportUnterminatedString() {
        final var source = "\"unterminated";
        final var diagnostics = DiagnosticSink.empty();

        PbsLexer.lex(source, new FileId(0), diagnostics);

        assertTrue(diagnostics.hasErrors(), "Lexer should report unterminated strings");
        assertEquals(LexErrors.E_LEX_UNTERMINATED_STRING.name(),
                diagnostics.stream().findFirst().orElseThrow().getCode());
    }

    @Test
    void shouldReportInvalidStringEscape() {
        final var source = "\"bad\\q\"";
        final var diagnostics = DiagnosticSink.empty();

        PbsLexer.lex(source, new FileId(0), diagnostics);

        assertTrue(diagnostics.hasErrors(), "Lexer should report invalid string escapes");
        assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(LexErrors.E_LEX_INVALID_STRING_ESCAPE.name())));
    }

    @Test
    void shouldReportInvalidCharacter() {
        final var source = "fn a() { ~ }";
        final var diagnostics = DiagnosticSink.empty();

        PbsLexer.lex(source, new FileId(0), diagnostics);

        assertTrue(diagnostics.hasErrors(), "Lexer should report invalid characters");
        assertEquals(LexErrors.E_LEX_INVALID_CHAR.name(),
                diagnostics.stream().findFirst().orElseThrow().getCode());
    }

    @Test
    void shouldEmitUtf8ByteOffsetsForNonAsciiIdentifier() {
        final var source = "fn cafe\u00E9() -> int { return 1; }";
        final var diagnostics = DiagnosticSink.empty();

        final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics);

        assertTrue(diagnostics.isEmpty(), "Valid non-ASCII identifier should lex without diagnostics");

        final var identifier = tokens.get(1);
        final int expectedStart = "fn ".getBytes(StandardCharsets.UTF_8).length;
        final int expectedEnd = "fn cafe\u00E9".getBytes(StandardCharsets.UTF_8).length;

        assertEquals(PbsTokenKind.IDENTIFIER, identifier.kind());
        assertEquals(expectedStart, identifier.start());
        assertEquals(expectedEnd, identifier.end());
    }
}
