/**
 * Unit tests for semanticTokenClassifier.
 *
 * Tests the core classification logic (no vscode dependency).
 * All token positions are 0-based (line, char).
 */

import { classifySemanticTokens, SEMANTIC_TOKEN_TYPES, SEMANTIC_TOKEN_MODIFIERS } from "../../core/src/analysis/semanticTokenClassifier.js";
import { makeDocument } from "./validators/helpers.js";

describe("classifySemanticTokens", () => {
    describe("builtin global variables → VARIABLE + READONLY", () => {
        test("classifies a recognised builtin global variable", () => {
            // SYSTEM_DATE is a well-known BuiltInGlobalVariable
            const doc = makeDocument(["#HASH-OFF", "SET zDate = SYSTEM_DATE"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.VARIABLE);
            expect(t).toBeDefined();
            expect(t.tokenModifiers).toBe(SEMANTIC_TOKEN_MODIFIERS.READONLY);
        });

        test("global variable column is accurate", () => {
            // "SET zDate = SYSTEM_DATE"
            //  0         1         2
            //  0123456789012345678901234
            //             ^12 = 'SYSTEM_DATE'
            const doc = makeDocument(["#HASH-OFF", "SET zDate = SYSTEM_DATE"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.VARIABLE);
            expect(t.char).toBe(12);
            expect(t.length).toBe("SYSTEM_DATE".length);
        });

        test("global variable lookup is case-insensitive", () => {
            const doc = makeDocument(["#HASH-OFF", "SET zDate = system_date"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.some((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.VARIABLE)).toBe(true);
        });

        test("returns empty array for document with no constants or functions", () => {
            const doc = makeDocument(["#HASH-OFF", "SET zName = \"hello\""]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens).toHaveLength(0);
        });
    });

    describe("builtin functions → FUNCTION + DEFAULT_LIBRARY (only when followed by LPAREN)", () => {
        test("classifies a builtin function call", () => {
            // GET_TOKEN is a well-known BuiltInFunction
            const doc = makeDocument(["#HASH-OFF", "SET zX = GET_TOKEN(zStr, 1, \",\")"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.FUNCTION);
            expect(t).toBeDefined();
            expect(t.tokenModifiers).toBe(SEMANTIC_TOKEN_MODIFIERS.DEFAULT_LIBRARY);
        });

        test("function column is accurate", () => {
            // "SET zX = GET_TOKEN(zStr, 1, \",\")"
            //  0         1
            //  0123456789012345678
            //           ^9 = 'GET_TOKEN'
            const doc = makeDocument(["#HASH-OFF", "SET zX = GET_TOKEN(zStr, 1, \",\")"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.FUNCTION);
            expect(t.char).toBe(9);
            expect(t.length).toBe("GET_TOKEN".length);
        });

        test("does NOT classify builtin function name without LPAREN", () => {
            // e.g. a variable named FORMAT (unusual but possible)
            // Without parens it should not be emitted as a function token
            const doc = makeDocument(["#HASH-OFF", "SET FORMAT = 1"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.FUNCTION)).toHaveLength(0);
        });

        test("function token preceded by whitespace before LPAREN is still classified", () => {
            // "SET zX = FORMAT (zStr)"  — space before paren
            const doc = makeDocument(["#HASH-OFF", "SET zX = FORMAT (zStr)"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.some((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.FUNCTION)).toBe(true);
        });
    });

    describe("multiple tokens on one line", () => {
        test("classifies both constant and function on same line", () => {
            const doc = makeDocument(["#HASH-OFF", "SET zX = FORMAT(SYSTEM_DATE, \"DD/MM/YYYY\")"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.some((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.FUNCTION)).toBe(true);
            expect(tokens.some((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.VARIABLE)).toBe(true);
        });
    });

    describe("output lines are excluded", () => {
        test("IDENTIFIER tokens on HASH-ON output lines are not classified", () => {
            // HASH-ON mode: "SYSTEM_DATE" without # is an output line — no IDENTIFIER token
            const doc = makeDocument(["SYSTEM_DATE"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens).toHaveLength(0);
        });
    });

    describe("token line numbers", () => {
        test("reports correct line for token on second code line", () => {
            const doc = makeDocument([
                "#HASH-OFF",
                "SET zA = 1",
                "SET zDate = SYSTEM_DATE"
            ]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.VARIABLE);
            expect(t.line).toBe(2);
        });
    });

    describe("annotation tokens — @mode:*", () => {
        test("classifies @mode:strict as ANNOTATION_KEYWORD", () => {
            const doc = makeDocument(["// @mode:strict"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD);
            expect(t).toBeDefined();
        });

        test("@mode:strict char offset skips the comment prefix", () => {
            // "// @mode:strict"
            //  0123
            //     ^ char 3 = '@mode:strict'
            const doc = makeDocument(["// @mode:strict"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD);
            expect(t.char).toBe(3);
            expect(t.length).toBe("@mode:strict".length);
        });

        test("classifies @mode:legacy as ANNOTATION_KEYWORD", () => {
            const doc = makeDocument(["// @mode:legacy"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.some((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD)).toBe(true);
        });

        test("classifies @mode:default as ANNOTATION_KEYWORD", () => {
            const doc = makeDocument(["// @mode:default"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.some((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD)).toBe(true);
        });

        test("@mode annotation with leading whitespace is still classified", () => {
            // "   // @mode:strict"
            const doc = makeDocument(["   // @mode:strict"]);
            const tokens = classifySemanticTokens(doc);
            const t = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD);
            expect(t).toBeDefined();
            expect(t.char).toBe(6); // 3 spaces + "//" + " " = index 6
        });

        test("plain comment is NOT classified as annotation", () => {
            const doc = makeDocument(["// This is just a regular comment"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD)).toHaveLength(0);
        });

        test("@mode annotation emits no ANNOTATION_RULE_ID tokens", () => {
            const doc = makeDocument(["// @mode:strict"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_RULE_ID)).toHaveLength(0);
        });
    });

    describe("annotation tokens — @rule-ignore", () => {
        test("classifies @rule-ignore as ANNOTATION_KEYWORD", () => {
            const doc = makeDocument(["// @rule-ignore ACU-VAR-001"]);
            const tokens = classifySemanticTokens(doc);
            const kw = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD);
            expect(kw).toBeDefined();
            expect(kw.char).toBe(3);
            expect(kw.length).toBe("@rule-ignore".length);
        });

        test("classifies ACU rule ID as ANNOTATION_RULE_ID", () => {
            const doc = makeDocument(["// @rule-ignore ACU-VAR-001"]);
            const tokens = classifySemanticTokens(doc);
            const id = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_RULE_ID);
            expect(id).toBeDefined();
            expect(id.length).toBe("ACU-VAR-001".length);
        });

        test("ACU-VAR-001 char offset is accurate", () => {
            // "// @rule-ignore ACU-VAR-001"
            //  0123456789012345678
            //                  ^16 = 'ACU-VAR-001'
            const doc = makeDocument(["// @rule-ignore ACU-VAR-001"]);
            const tokens = classifySemanticTokens(doc);
            const id = tokens.find((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_RULE_ID);
            expect(id.char).toBe(16);
        });

        test("multiple rule IDs each get an ANNOTATION_RULE_ID token", () => {
            const doc = makeDocument(["// @rule-ignore ACU-VAR-001 ACU-FNC-001"]);
            const tokens = classifySemanticTokens(doc);
            const ids = tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_RULE_ID);
            expect(ids).toHaveLength(2);
        });

        test("REASON clause text is NOT classified as annotation tokens", () => {
            const doc = makeDocument(["// @rule-ignore ACU-VAR-001 REASON: intentional legacy code"]);
            const tokens = classifySemanticTokens(doc);
            const ids = tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_RULE_ID);
            expect(ids).toHaveLength(1);
        });

        test("@rule-ignore without rule IDs emits only the keyword token", () => {
            const doc = makeDocument(["// @rule-ignore"]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD)).toHaveLength(1);
            expect(tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_RULE_ID)).toHaveLength(0);
        });
    });

    describe("annotations do not fire on non-comment lines", () => {
        test("executable line containing @mode text is not annotated", () => {
            const doc = makeDocument(["#HASH-OFF", "zMode = \"@mode:strict\""]);
            const tokens = classifySemanticTokens(doc);
            expect(tokens.filter((t) => t.tokenType === SEMANTIC_TOKEN_TYPES.ANNOTATION_KEYWORD)).toHaveLength(0);
        });
    });
});
