/**
 * Unit tests for the Pratt expression parser.
 *
 * Uses a SET statement as the vehicle for getting an expression span:
 *   SET zX = <expr>   →  setNode.value is a TokenSpan over <expr>
 */

import { parseExpression } from "../../core/src/ast/expressionParser.js";
import { ExprNodeType } from "../../core/src/ast/expressionNodes.js";
import { buildDocumentAst } from "../../core/src/ast/documentAst.js";
import { makeDocument } from "./validators/helpers.js";

/**
 * Parse an expression string via a SET statement.
 */
function parseExpr(exprStr) {
    const doc = makeDocument(["#HASH-OFF", `SET zX = ${exprStr}`]);
    const { ast } = buildDocumentAst(doc);
    const setNode = ast.statements.find(s => s.type === "SetStatement");
    return parseExpression(setNode ? setNode.value : null);
}

// ---------------------------------------------------------------------------
// Null / empty
// ---------------------------------------------------------------------------

describe("parseExpression — null / empty", () => {
    test("returns null for null span", () => {
        expect(parseExpression(null)).toBeNull();
    });

    test("returns null for span with no tokens", () => {
        expect(parseExpression({ tokens: [] })).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// Literals
// ---------------------------------------------------------------------------

describe("parseExpression — literals", () => {
    test("string literal", () => {
        const node = parseExpr('"hello"');
        expect(node.type).toBe(ExprNodeType.LITERAL);
        expect(node.inferredType).toBe("STRING");
        expect(node.value).toBe('"hello"');
    });

    test("number literal in SHORT range", () => {
        const node = parseExpr("42");
        expect(node.type).toBe(ExprNodeType.LITERAL);
        expect(node.inferredType).toBe("SHORT");
    });

    test("number literal above SHORT range → LONG", () => {
        const node = parseExpr("40000");
        expect(node.type).toBe(ExprNodeType.LITERAL);
        expect(node.inferredType).toBe("LONG");
    });

    test("decimal literal → DOUBLE", () => {
        const node = parseExpr("3.14");
        expect(node.type).toBe(ExprNodeType.LITERAL);
        expect(node.inferredType).toBe("DOUBLE");
    });

    test("TRUE → BOOLEAN literal", () => {
        const node = parseExpr("TRUE");
        expect(node.type).toBe(ExprNodeType.LITERAL);
        expect(node.inferredType).toBe("BOOLEAN");
    });

    test("FALSE → BOOLEAN literal", () => {
        const node = parseExpr("FALSE");
        expect(node.type).toBe(ExprNodeType.LITERAL);
        expect(node.inferredType).toBe("BOOLEAN");
    });

    test("date literal", () => {
        const node = parseExpr("28/03/2026");
        expect(node.type).toBe(ExprNodeType.DATE);
        expect(node.inferredType).toBe("DATE");
        expect(node.value).toBe("28/03/2026");
    });

    test("date literal with 1-digit components", () => {
        const node = parseExpr("1/1/2000");
        expect(node.type).toBe(ExprNodeType.DATE);
        expect(node.inferredType).toBe("DATE");
    });

    test("time literal", () => {
        const node = parseExpr("12:30:00:00");
        expect(node.type).toBe(ExprNodeType.TIME);
        expect(node.inferredType).toBe("TIME");
        expect(node.value).toBe("12:30:00:00");
    });

    test("time literal with 1-digit components", () => {
        const node = parseExpr("12:3:0:0");
        expect(node.type).toBe(ExprNodeType.TIME);
        expect(node.inferredType).toBe("TIME");
    });
});

// ---------------------------------------------------------------------------
// Identifier and function call
// ---------------------------------------------------------------------------

describe("parseExpression — identifier and call", () => {
    test("bare identifier", () => {
        const node = parseExpr("zName");
        expect(node.type).toBe(ExprNodeType.IDENTIFIER);
        expect(node.name).toBe("zName");
    });

    test("function call with no arguments", () => {
        const node = parseExpr("NOW()");
        expect(node.type).toBe(ExprNodeType.CALL);
        expect(node.callee.name).toBe("NOW");
        expect(node.args).toHaveLength(0);
    });

    test("function call with one argument", () => {
        const node = parseExpr('FORMAT(zStr, "DD/MM/YYYY")');
        expect(node.type).toBe(ExprNodeType.CALL);
        expect(node.callee.name).toBe("FORMAT");
        expect(node.args).toHaveLength(2);
    });

    test("function call with three arguments", () => {
        const node = parseExpr('GET_TOKEN(zStr, ",", 1)');
        expect(node.type).toBe(ExprNodeType.CALL);
        expect(node.callee.name).toBe("GET_TOKEN");
        expect(node.args).toHaveLength(3);
        expect(node.args[2].type).toBe(ExprNodeType.LITERAL);
    });
});

// ---------------------------------------------------------------------------
// Unary operators
// ---------------------------------------------------------------------------

describe("parseExpression — unary operators", () => {
    test("unary minus on literal", () => {
        const node = parseExpr("-1");
        expect(node.type).toBe(ExprNodeType.UNARY);
        expect(node.op).toBe("-");
        expect(node.operand.type).toBe(ExprNodeType.LITERAL);
    });

    test("unary minus on identifier", () => {
        const node = parseExpr("-sX");
        expect(node.type).toBe(ExprNodeType.UNARY);
        expect(node.op).toBe("-");
        expect(node.operand.name).toBe("sX");
    });

    test("NOT TRUE", () => {
        const node = parseExpr("NOT TRUE");
        expect(node.type).toBe(ExprNodeType.UNARY);
        expect(node.op).toBe("NOT");
        expect(node.operand.type).toBe(ExprNodeType.LITERAL);
    });
});

// ---------------------------------------------------------------------------
// Binary operators
// ---------------------------------------------------------------------------

describe("parseExpression — binary operators", () => {
    test("addition", () => {
        const node = parseExpr("sA + sB");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("+");
        expect(node.left.name).toBe("sA");
        expect(node.right.name).toBe("sB");
    });

    test("comparison =", () => {
        const node = parseExpr("sX = 5");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("=");
    });

    test("comparison <>", () => {
        const node = parseExpr("sX <> 0");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("<>");
    });

    test("exponentiation **", () => {
        const node = parseExpr("2 ** 3");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("**");
        expect(node.left.value).toBe("2");
        expect(node.right.value).toBe("3");
    });

    test("MOD", () => {
        const node = parseExpr("9 MOD 5");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("MOD");
    });

    test("AND", () => {
        const node = parseExpr("bA AND bB");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("AND");
    });

    test("OR", () => {
        const node = parseExpr("bA OR bB");
        expect(node.type).toBe(ExprNodeType.BINARY);
        expect(node.op).toBe("OR");
    });
});

// ---------------------------------------------------------------------------
// Precedence
// ---------------------------------------------------------------------------

describe("parseExpression — precedence", () => {
    test("* binds tighter than + (left)", () => {
        // sA + sB * sC → sA + (sB * sC)
        const node = parseExpr("sA + sB * sC");
        expect(node.op).toBe("+");
        expect(node.right.op).toBe("*");
    });

    test("** binds tighter than * (confirmed: 2*3**2=18)", () => {
        // 2 * 3 ** 2 → 2 * (3 ** 2)
        const node = parseExpr("2 * 3 ** 2");
        expect(node.op).toBe("*");
        expect(node.right.op).toBe("**");
    });

    test("** is left-associative (confirmed: 2**3**2=64)", () => {
        // 2 ** 3 ** 2 → (2 ** 3) ** 2
        const node = parseExpr("2 ** 3 ** 2");
        expect(node.op).toBe("**");
        expect(node.left.op).toBe("**");
        expect(node.right.value).toBe("2");
    });

    test("AND binds tighter than OR (confirmed: TRUE OR FALSE AND FALSE = TRUE)", () => {
        // TRUE OR FALSE AND FALSE → TRUE OR (FALSE AND FALSE)
        const node = parseExpr("TRUE OR FALSE AND FALSE");
        expect(node.op).toBe("OR");
        expect(node.right.op).toBe("AND");
    });

    test("NOT binds tighter than AND (confirmed: NOT TRUE AND FALSE = FALSE)", () => {
        // NOT TRUE AND FALSE → (NOT TRUE) AND FALSE
        const node = parseExpr("NOT TRUE AND FALSE");
        expect(node.op).toBe("AND");
        expect(node.left.op).toBe("NOT");
    });

    test("MOD binds tighter than + (confirmed: 3+4 MOD 3=4)", () => {
        // 3 + 4 MOD 3 → 3 + (4 MOD 3)
        const node = parseExpr("3 + 4 MOD 3");
        expect(node.op).toBe("+");
        expect(node.right.op).toBe("MOD");
    });

    test("unary minus binds tighter than ** (confirmed: -2**2=4)", () => {
        // -2 ** 2 → (-2) ** 2
        const node = parseExpr("-2 ** 2");
        expect(node.op).toBe("**");
        expect(node.left.op).toBe("-");
    });
});

// ---------------------------------------------------------------------------
// Parentheses
// ---------------------------------------------------------------------------

describe("parseExpression — parentheses", () => {
    test("parentheses produce PAREN node", () => {
        const node = parseExpr("(sA + sB)");
        expect(node.type).toBe(ExprNodeType.PAREN);
        expect(node.expr.op).toBe("+");
    });

    test("parentheses override precedence", () => {
        // (sA + sB) * sC → BINARY(*)(PAREN(sA+sB), sC)
        const node = parseExpr("(sA + sB) * sC");
        expect(node.op).toBe("*");
        expect(node.left.type).toBe(ExprNodeType.PAREN);
    });
});
