/**
 * Unit tests for expressionTypeInference — Phase 2 of the expression tree plan.
 *
 * Nodes are constructed directly using the node factory helpers from
 * expressionNodes.js so that tests exercise the inference logic in isolation,
 * without depending on the parser.
 *
 * A small set of end-to-end tests at the bottom round-trip through the parser
 * to verify the full pipeline still works.
 */

import {
    inferExpressionType,
    isNumericType,
    widenNumericTypes
} from "../../core/src/analysis/expressionTypeInference.js";

import {
    ExprNodeType,
    makeLiteral,
    makeDateNode,
    makeTimeNode,
    makeIdentifier,
    makeCall,
    makeUnary,
    makeBinary,
    makeParen
} from "../../core/src/ast/expressionNodes.js";

import { AcurityTypes } from "../../core/src/types/acurityTypes.js";
import { NodeType } from "../../core/src/ast/nodes.js";
import { parseExpression } from "../../core/src/ast/expressionParser.js";
import { buildDocumentAst } from "../../core/src/ast/documentAst.js";
import { makeDocument } from "./validators/helpers.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Synthetic token — enough for the node factories. */
const tok = (value = "x") => ({ value });

/** Short-hand literal node with an explicit inferredType. */
function lit(inferredType) {
    return makeLiteral(tok(inferredType), inferredType);
}

/** Identifier node for a variable whose type is in the context map. */
function varNode(name) {
    return makeIdentifier(tok(name));
}

/** Context map with one variable. */
function ctx(name, type) {
    return { variables: new Map([[name.toUpperCase(), type]]) };
}

// ---------------------------------------------------------------------------
// isNumericType
// ---------------------------------------------------------------------------

describe("isNumericType", () => {
    test.each([
        AcurityTypes.SHORT,
        AcurityTypes.LONG,
        AcurityTypes.INTEGER,
        AcurityTypes.BIGINT,
        AcurityTypes.DOUBLE
    ])("returns true for %s", (type) => {
        expect(isNumericType(type)).toBe(true);
    });

    test.each([
        AcurityTypes.STRING,
        AcurityTypes.BOOLEAN,
        AcurityTypes.DATE,
        AcurityTypes.TIME,
        null,
        undefined
    ])("returns false for %s", (type) => {
        expect(isNumericType(type)).toBe(false);
    });
});

// ---------------------------------------------------------------------------
// widenNumericTypes
// ---------------------------------------------------------------------------

describe("widenNumericTypes", () => {
    test("SHORT + SHORT → SHORT", () => {
        expect(widenNumericTypes(AcurityTypes.SHORT, AcurityTypes.SHORT)).toBe(AcurityTypes.SHORT);
    });

    test("SHORT + LONG → LONG", () => {
        expect(widenNumericTypes(AcurityTypes.SHORT, AcurityTypes.LONG)).toBe(AcurityTypes.LONG);
    });

    test("LONG + BIGINT → BIGINT", () => {
        expect(widenNumericTypes(AcurityTypes.LONG, AcurityTypes.BIGINT)).toBe(AcurityTypes.BIGINT);
    });

    test("BIGINT + DOUBLE → DOUBLE", () => {
        expect(widenNumericTypes(AcurityTypes.BIGINT, AcurityTypes.DOUBLE)).toBe(AcurityTypes.DOUBLE);
    });

    test("INTEGER treated as LONG — INTEGER + SHORT → LONG", () => {
        expect(widenNumericTypes(AcurityTypes.INTEGER, AcurityTypes.SHORT)).toBe(AcurityTypes.LONG);
    });

    test("INTEGER + DOUBLE → DOUBLE", () => {
        expect(widenNumericTypes(AcurityTypes.INTEGER, AcurityTypes.DOUBLE)).toBe(AcurityTypes.DOUBLE);
    });

    test("non-numeric left → null", () => {
        expect(widenNumericTypes(AcurityTypes.STRING, AcurityTypes.LONG)).toBeNull();
    });

    test("non-numeric right → null", () => {
        expect(widenNumericTypes(AcurityTypes.LONG, AcurityTypes.BOOLEAN)).toBeNull();
    });

    test("both null → null", () => {
        expect(widenNumericTypes(null, null)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// inferExpressionType — null / missing node
// ---------------------------------------------------------------------------

describe("inferExpressionType — null / missing", () => {
    test("returns null for null node", () => {
        expect(inferExpressionType(null)).toBeNull();
    });

    test("returns null for undefined node", () => {
        expect(inferExpressionType(undefined)).toBeNull();
    });

    test("returns null for unknown node type", () => {
        expect(inferExpressionType({ type: "Unknown" })).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// LITERAL, DATE, TIME — passthrough of inferredType
// ---------------------------------------------------------------------------

describe("inferExpressionType — literals", () => {
    test.each([
        [ExprNodeType.LITERAL, AcurityTypes.STRING],
        [ExprNodeType.LITERAL, AcurityTypes.SHORT],
        [ExprNodeType.LITERAL, AcurityTypes.LONG],
        [ExprNodeType.LITERAL, AcurityTypes.DOUBLE],
        [ExprNodeType.LITERAL, AcurityTypes.BOOLEAN]
    ])("%s with inferredType %s", (nodeType, inferredType) => {
        const node = { type: nodeType, inferredType };
        expect(inferExpressionType(node)).toBe(inferredType);
    });

    test("DATE node → DATE", () => {
        expect(inferExpressionType(makeDateNode(tok("01/01/2024")))).toBe(AcurityTypes.DATE);
    });

    test("TIME node → TIME", () => {
        expect(inferExpressionType(makeTimeNode(tok("12:00:00:00")))).toBe(AcurityTypes.TIME);
    });

    test("LITERAL with no inferredType → null", () => {
        const node = { type: ExprNodeType.LITERAL };
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// PAREN — passthrough
// ---------------------------------------------------------------------------

describe("inferExpressionType — PAREN", () => {
    test("delegates to inner expression", () => {
        const inner = lit(AcurityTypes.LONG);
        const node = makeParen(inner, tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });

    test("nested parens", () => {
        const node = makeParen(makeParen(lit(AcurityTypes.STRING), tok()), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.STRING);
    });
});

// ---------------------------------------------------------------------------
// IDENTIFIER — variable lookup
// ---------------------------------------------------------------------------

describe("inferExpressionType — IDENTIFIER (variable)", () => {
    test("resolves variable from context map", () => {
        const node = varNode("lTotal");
        expect(inferExpressionType(node, ctx("lTotal", AcurityTypes.LONG))).toBe(AcurityTypes.LONG);
    });

    test("lookup is case-insensitive", () => {
        const node = varNode("zName");
        const context = { variables: new Map([["ZNAME", AcurityTypes.STRING]]) };
        expect(inferExpressionType(node, context)).toBe(AcurityTypes.STRING);
    });

    test("returns null when variable not in context", () => {
        const node = varNode("lUnknown");
        expect(inferExpressionType(node, ctx("lOther", AcurityTypes.LONG))).toBeNull();
    });

    test("returns null with empty context", () => {
        const node = varNode("lX");
        expect(inferExpressionType(node)).toBeNull();
    });

    test("variables is not a Map — falls through to constant check", () => {
        const node = varNode("MAX_SHORT");
        // MAX_SHORT is a builtin constant → SHORT
        expect(inferExpressionType(node, { variables: {} })).toBe(AcurityTypes.SHORT);
    });
});

// ---------------------------------------------------------------------------
// IDENTIFIER — built-in constants
// ---------------------------------------------------------------------------

describe("inferExpressionType — IDENTIFIER (builtin constant)", () => {
    test("MAX_SHORT → SHORT", () => {
        expect(inferExpressionType(varNode("MAX_SHORT"))).toBe(AcurityTypes.SHORT);
    });

    test("MIN_SHORT → SHORT", () => {
        expect(inferExpressionType(varNode("MIN_SHORT"))).toBe(AcurityTypes.SHORT);
    });

    test("MAX_DATE → DATE", () => {
        expect(inferExpressionType(varNode("MAX_DATE"))).toBe(AcurityTypes.DATE);
    });

    test("MIN_DATE → DATE", () => {
        expect(inferExpressionType(varNode("MIN_DATE"))).toBe(AcurityTypes.DATE);
    });

    test("constant name is case-insensitive", () => {
        expect(inferExpressionType(varNode("max_short"))).toBe(AcurityTypes.SHORT);
    });

    test("unknown identifier with no context → null", () => {
        expect(inferExpressionType(varNode("COMPLETELY_UNKNOWN"))).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// CALL — built-in function return types
// ---------------------------------------------------------------------------

describe("inferExpressionType — CALL (builtin)", () => {
    function callNode(name) {
        return makeCall(makeIdentifier(tok(name)), [], tok(), tok());
    }

    test("STR(…) → STRING", () => {
        expect(inferExpressionType(callNode("STR"))).toBe(AcurityTypes.STRING);
    });

    test("TO_LONG(…) → LONG", () => {
        expect(inferExpressionType(callNode("TO_LONG"))).toBe(AcurityTypes.LONG);
    });

    test("TO_SHORT(…) → SHORT", () => {
        expect(inferExpressionType(callNode("TO_SHORT"))).toBe(AcurityTypes.SHORT);
    });

    test("TO_DOUBLE(…) → DOUBLE", () => {
        expect(inferExpressionType(callNode("TO_DOUBLE"))).toBe(AcurityTypes.DOUBLE);
    });

    test("case-insensitive function lookup", () => {
        expect(inferExpressionType(callNode("str"))).toBe(AcurityTypes.STRING);
    });

    test("unknown function → null", () => {
        expect(inferExpressionType(callNode("TOTALLY_UNKNOWN_FUNC"))).toBeNull();
    });

    test("null callee → null", () => {
        const node = { type: ExprNodeType.CALL, callee: null, args: [] };
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// CALL — user-defined function catalog
// ---------------------------------------------------------------------------

describe("inferExpressionType — CALL (user-defined)", () => {
    const udCatalog = {
        CALC_TOTAL: { declaredName: "CALC_TOTAL", params: [], returnType: AcurityTypes.DOUBLE },
        GET_NAME:   { declaredName: "GET_NAME",   params: [], returnType: AcurityTypes.STRING },
        NO_RETURN:  { declaredName: "NO_RETURN",  params: [], returnType: null }
    };

    function callNode(name) {
        return makeCall(makeIdentifier(tok(name)), [], tok(), tok());
    }

    test("user-defined function returning DOUBLE", () => {
        const context = { userDefinedFunctionCatalog: udCatalog };
        expect(inferExpressionType(callNode("CALC_TOTAL"), context)).toBe(AcurityTypes.DOUBLE);
    });

    test("user-defined function returning STRING", () => {
        const context = { userDefinedFunctionCatalog: udCatalog };
        expect(inferExpressionType(callNode("GET_NAME"), context)).toBe(AcurityTypes.STRING);
    });

    test("user-defined function with null returnType → null", () => {
        const context = { userDefinedFunctionCatalog: udCatalog };
        expect(inferExpressionType(callNode("NO_RETURN"), context)).toBeNull();
    });

    test("lookup is case-insensitive", () => {
        const context = { userDefinedFunctionCatalog: udCatalog };
        expect(inferExpressionType(callNode("calc_total"), context)).toBe(AcurityTypes.DOUBLE);
    });
});

// ---------------------------------------------------------------------------
// UNARY
// ---------------------------------------------------------------------------

describe("inferExpressionType — UNARY", () => {
    test("NOT → BOOLEAN", () => {
        const node = makeUnary("NOT", lit(AcurityTypes.BOOLEAN), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.BOOLEAN);
    });

    test("NOT (lowercase) → BOOLEAN", () => {
        const node = makeUnary("not", lit(AcurityTypes.BOOLEAN), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.BOOLEAN);
    });

    test("unary minus preserves SHORT", () => {
        const node = makeUnary("-", lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.SHORT);
    });

    test("unary minus preserves DOUBLE", () => {
        const node = makeUnary("-", lit(AcurityTypes.DOUBLE), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DOUBLE);
    });

    test("unary minus on non-numeric → null", () => {
        const node = makeUnary("-", lit(AcurityTypes.STRING), tok());
        expect(inferExpressionType(node)).toBeNull();
    });

    test("unknown unary op → null", () => {
        const node = makeUnary("+", lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// BINARY — comparison operators → BOOLEAN
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY comparisons", () => {
    test.each(["=", "<>", "<", ">", "<=", ">="])("'%s' → BOOLEAN", (op) => {
        const node = makeBinary(op, lit(AcurityTypes.LONG), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.BOOLEAN);
    });
});

// ---------------------------------------------------------------------------
// BINARY — logical operators → BOOLEAN
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY logical", () => {
    test("AND → BOOLEAN", () => {
        const node = makeBinary("AND", lit(AcurityTypes.BOOLEAN), lit(AcurityTypes.BOOLEAN), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.BOOLEAN);
    });

    test("OR → BOOLEAN", () => {
        const node = makeBinary("OR", lit(AcurityTypes.BOOLEAN), lit(AcurityTypes.BOOLEAN), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.BOOLEAN);
    });

    test("AND case-insensitive", () => {
        const node = makeBinary("and", lit(AcurityTypes.BOOLEAN), lit(AcurityTypes.BOOLEAN), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.BOOLEAN);
    });
});

// ---------------------------------------------------------------------------
// BINARY — exponentiation
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY exponentiation", () => {
    test("** always yields DOUBLE", () => {
        const node = makeBinary("**", lit(AcurityTypes.SHORT), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DOUBLE);
    });
});

// ---------------------------------------------------------------------------
// BINARY — MOD
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY MOD", () => {
    test("numeric MOD → left type (SHORT)", () => {
        const node = makeBinary("MOD", lit(AcurityTypes.SHORT), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.SHORT);
    });

    test("numeric MOD → left type (DOUBLE)", () => {
        const node = makeBinary("MOD", lit(AcurityTypes.DOUBLE), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DOUBLE);
    });

    test("INTEGER MOD → normalized to LONG", () => {
        const node = makeBinary("MOD", lit(AcurityTypes.INTEGER), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });

    test("non-numeric left MOD → null", () => {
        const node = makeBinary("MOD", lit(AcurityTypes.STRING), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBeNull();
    });

    test("MOD case-insensitive", () => {
        const node = makeBinary("mod", lit(AcurityTypes.LONG), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });
});

// ---------------------------------------------------------------------------
// BINARY — addition
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY +", () => {
    test("numeric + numeric → widened type", () => {
        const node = makeBinary("+", lit(AcurityTypes.SHORT), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });

    test("SHORT + SHORT → SHORT", () => {
        const node = makeBinary("+", lit(AcurityTypes.SHORT), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.SHORT);
    });

    test("LONG + DOUBLE → DOUBLE", () => {
        const node = makeBinary("+", lit(AcurityTypes.LONG), lit(AcurityTypes.DOUBLE), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DOUBLE);
    });

    test("STRING + STRING → STRING", () => {
        const node = makeBinary("+", lit(AcurityTypes.STRING), lit(AcurityTypes.STRING), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.STRING);
    });

    test("DATE + numeric → DATE", () => {
        const node = makeBinary("+", lit(AcurityTypes.DATE), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DATE);
    });

    test("numeric + DATE → DATE", () => {
        const node = makeBinary("+", lit(AcurityTypes.SHORT), lit(AcurityTypes.DATE), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DATE);
    });

    test("TIME + numeric → TIME", () => {
        const node = makeBinary("+", lit(AcurityTypes.TIME), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.TIME);
    });

    test("numeric + TIME → TIME", () => {
        const node = makeBinary("+", lit(AcurityTypes.SHORT), lit(AcurityTypes.TIME), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.TIME);
    });

    test("incompatible types → null", () => {
        const node = makeBinary("+", lit(AcurityTypes.STRING), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// BINARY — subtraction
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY -", () => {
    test("numeric - numeric → widened type", () => {
        const node = makeBinary("-", lit(AcurityTypes.LONG), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });

    test("DATE - numeric → DATE", () => {
        const node = makeBinary("-", lit(AcurityTypes.DATE), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DATE);
    });

    test("DATE - DATE → LONG (days between)", () => {
        const node = makeBinary("-", lit(AcurityTypes.DATE), lit(AcurityTypes.DATE), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });

    test("TIME - numeric → TIME", () => {
        const node = makeBinary("-", lit(AcurityTypes.TIME), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.TIME);
    });

    test("incompatible types → null", () => {
        const node = makeBinary("-", lit(AcurityTypes.STRING), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// BINARY — multiplication and division
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY * and /", () => {
    test("SHORT * LONG → LONG", () => {
        const node = makeBinary("*", lit(AcurityTypes.SHORT), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.LONG);
    });

    test("DOUBLE / SHORT → DOUBLE", () => {
        const node = makeBinary("/", lit(AcurityTypes.DOUBLE), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBe(AcurityTypes.DOUBLE);
    });

    test("non-numeric * numeric → null", () => {
        const node = makeBinary("*", lit(AcurityTypes.STRING), lit(AcurityTypes.LONG), tok());
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// BINARY — variable operands resolved from context
// ---------------------------------------------------------------------------

describe("inferExpressionType — BINARY with variable operands", () => {
    test("variable + literal resolved through context", () => {
        const varCtx = { variables: new Map([["LTOTAL", AcurityTypes.LONG]]) };
        const node = makeBinary("+", varNode("lTotal"), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node, varCtx)).toBe(AcurityTypes.LONG);
    });

    test("unknown variable operand → null", () => {
        const node = makeBinary("+", varNode("lUnknown"), lit(AcurityTypes.SHORT), tok());
        expect(inferExpressionType(node)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// End-to-end: parser → inference
// ---------------------------------------------------------------------------

describe("inferExpressionType — end-to-end via parser", () => {

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

    test("42 (SHORT literal) → SHORT", () => {
        expect(inferExpressionType(parseExpr("42"))).toBe(AcurityTypes.SHORT);
    });

    test("3.14 (DOUBLE literal) → DOUBLE", () => {
        expect(inferExpressionType(parseExpr("3.14"))).toBe(AcurityTypes.DOUBLE);
    });

    test('"hello" (STRING literal) → STRING', () => {
        expect(inferExpressionType(parseExpr('"hello"'))).toBe(AcurityTypes.STRING);
    });

    test("TRUE (BOOLEAN literal) → BOOLEAN", () => {
        expect(inferExpressionType(parseExpr("TRUE"))).toBe(AcurityTypes.BOOLEAN);
    });

    test("1 + 2 → SHORT (both SHORT, widens to SHORT)", () => {
        expect(inferExpressionType(parseExpr("1 + 2"))).toBe(AcurityTypes.SHORT);
    });

    test("1 + 40000 → LONG (SHORT + LONG → LONG)", () => {
        expect(inferExpressionType(parseExpr("1 + 40000"))).toBe(AcurityTypes.LONG);
    });

    test("1 = 1 → BOOLEAN", () => {
        expect(inferExpressionType(parseExpr("1 = 1"))).toBe(AcurityTypes.BOOLEAN);
    });

    test("2 ** 8 → DOUBLE", () => {
        expect(inferExpressionType(parseExpr("2 ** 8"))).toBe(AcurityTypes.DOUBLE);
    });

    test("NOT TRUE → BOOLEAN", () => {
        expect(inferExpressionType(parseExpr("NOT TRUE"))).toBe(AcurityTypes.BOOLEAN);
    });

    test("STR(42) → STRING", () => {
        expect(inferExpressionType(parseExpr("STR(42)"))).toBe(AcurityTypes.STRING);
    });

    test("(42) → SHORT (paren passthrough)", () => {
        expect(inferExpressionType(parseExpr("(42)"))).toBe(AcurityTypes.SHORT);
    });

    test("MAX_SHORT (builtin constant) → SHORT", () => {
        expect(inferExpressionType(parseExpr("MAX_SHORT"))).toBe(AcurityTypes.SHORT);
    });
});
