/**
 * Unit tests for the Architect language AST parser.
 *
 * Acceptance criteria (from AST_ARCHITECTURE.md Phase 2):
 * 1. Parses all TXT fixtures to a Program without throwing.
 * 2. Key constructs are correctly nested:
 *    - FunctionDeclaration with body[]
 *    - IfStatement with consequent/alternate
 *    - DoStatement with body
 * 3. UnknownLine nodes do not fail the parse.
 */

import * as fs from "fs";
import * as path from "path";
import { tokenize } from "../../core/src/lexer/lexer.js";
import { parse } from "../../core/src/ast/parser.js";
import { NodeType } from "../../core/src/ast/nodes.js";

import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

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

function loadFixture(relativePath) {
    const fullPath = path.join(__dirname, "fixtures", relativePath);
    return fs.readFileSync(fullPath, "utf8");
}

function parseFixture(relativePath) {
    const text = loadFixture(relativePath);
    return parse(tokenize(text));
}

/** Collect all nodes of a given type anywhere in the AST (deep walk). */
function collectNodes(root, nodeType) {
    const results = [];
    function walk(node) {
        if (!node || typeof node !== "object") return;
        if (node.type === nodeType) results.push(node);
        for (const key of Object.keys(node)) {
            const val = node[key];
            if (Array.isArray(val)) {
                val.forEach(walk);
            } else if (val && typeof val === "object" && val.type) {
                walk(val);
            }
        }
    }
    walk(root);
    return results;
}

/** Count the total number of UnknownLine nodes in the AST. */
function countUnknownLines(program) {
    return collectNodes(program, NodeType.UnknownLine).length;
}

// ---------------------------------------------------------------------------
// Acceptance criterion 1: parse without throwing
// ---------------------------------------------------------------------------

const ALL_FIXTURES = [
    "hash-mode/hash-mode-explicit-off.TXT",
    "hash-mode/hash-mode-on-unprefixed-output.TXT",
    "hash-mode/hash-mode-on-note-style.TXT",
    "core/builtin-get-str.TXT",
    "core/forward-function-call-error.TXT",
    "core/type-mismatch-string-to-short.TXT",
    "core/user-defined-after-definition.TXT",
    "edge-cases/edge-case-hash-comment.TXT",
    "edge-cases/edge-case-alt-var-syntax.TXT",
    "edge-cases/edge-case-keyword-profile.TXT",
    "validators/keyword-capitalization-errors.TXT",
    "validators/keyword-capitalization-valid.TXT",
    "validators/string-literals-closed.TXT",
    "validators/naming-scope-prefix.TXT",
    "validators/naming-type-prefix.TXT",
    "validators/type-capitalization-errors.TXT",
    "control-flow/control-flow-exit.TXT",
    "control-flow/control-flow-end-of-code.TXT",
    "case/case-without-of.TXT",
    "case/case-with-of-legacy.TXT",
    "do/do-while-basic.TXT",
    "do/do-forever-break-continue.TXT",
    "function-capitalisation-mismatch/func-case-mismatch-fix-call.TXT",
    "function-capitalisation-mismatch/func-case-mismatch-fix-decl.TXT",
    "function-user-defined/func-user-defined-return-validation.TXT",
    "macro/macro-redeclare-case.TXT",
    "settings-coverage/function-redeclaration.TXT",
    "settings-coverage/unclosed-function-call.TXT",
    "block-structure/block-unclosed-at-eof.TXT",
    "block-structure/block-mismatched-closures.TXT",
    "robodoc/robodoc-valid-report.TXT",
    "output-lines/output-hash-off-interpolation.TXT",
    "included-files/include-behaviour.TXT",
    "require/require-existing-files.TXT",
    "suppression/suppression-single-rule.TXT",
];

describe("parser — no-throw on all fixtures", () => {
    for (const fixture of ALL_FIXTURES) {
        test(`parses without throwing — ${fixture}`, () => {
            expect(() => parseFixture(fixture)).not.toThrow();
        });
    }
});

describe("parser — Program is returned", () => {
    for (const fixture of ALL_FIXTURES) {
        test(`returns a Program node — ${fixture}`, () => {
            const program = parseFixture(fixture);
            expect(program.type).toBe(NodeType.Program);
            expect(Array.isArray(program.statements)).toBe(true);
        });
    }
});

// ---------------------------------------------------------------------------
// Acceptance criterion 2: key constructs correctly nested
// ---------------------------------------------------------------------------

describe("parser — FunctionDeclaration with body[]", () => {
    test("FunctionDeclaration node is present with correct name and params", () => {
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const funcs = collectNodes(prog, NodeType.FunctionDeclaration);
        expect(funcs.length).toBeGreaterThan(0);

        const f = funcs[0];
        expect(typeof f.name).toBe("string");
        expect(f.name.length).toBeGreaterThan(0);
        expect(Array.isArray(f.params)).toBe(true);
        expect(Array.isArray(f.body)).toBe(true);
    });

    test("FunctionDeclaration body contains nested statements", () => {
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const funcs = collectNodes(prog, NodeType.FunctionDeclaration);
        // Functions in the fixture contain RETURN and VAR statements
        const withBodies = funcs.filter(f => f.body.length > 0);
        expect(withBodies.length).toBeGreaterThan(0);
    });

    test("FunctionDeclaration params are parsed correctly", () => {
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const funcs = collectNodes(prog, NodeType.FunctionDeclaration);
        // All functions in the fixture have 2 params
        const withParams = funcs.filter(f => f.params.length === 2);
        expect(withParams.length).toBeGreaterThan(0);
        expect(withParams[0].params[0]).toHaveProperty("name");
        expect(withParams[0].params[0]).toHaveProperty("type");
    });

    test("FunctionDeclaration returnType is parsed", () => {
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const funcs = collectNodes(prog, NodeType.FunctionDeclaration);
        const typed = funcs.filter(f => f.returnType !== null);
        expect(typed.length).toBeGreaterThan(0);
        expect(["STRING", "SHORT", "LONG", "DATE", "DOUBLE", "BOOLEAN"]).toContain(typed[0].returnType);
    });

    test("statements after ENDFUNCTION are in the parent scope, not the function body", () => {
        // The fixture has 5 functions; statements between functions should be at program level.
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const funcs = collectNodes(prog, NodeType.FunctionDeclaration);
        expect(funcs.length).toBe(5);
        // EndFunctionStatement should appear at program level (between functions)
        const endFuncs = prog.statements.filter(s => s.type === NodeType.EndFunctionStatement);
        expect(endFuncs.length).toBe(5);
    });

    test("nested RETURN is inside function body", () => {
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const funcs = collectNodes(prog, NodeType.FunctionDeclaration);
        const returnsInBody = funcs.flatMap(f => f.body.filter(s => s.type === NodeType.ReturnStatement));
        expect(returnsInBody.length).toBeGreaterThan(0);
    });
});

describe("parser — IfStatement with consequent/alternate", () => {
    test("IfStatement is present in do-forever fixture", () => {
        const prog = parseFixture("do/do-forever-break-continue.TXT");
        const ifs = collectNodes(prog, NodeType.IfStatement);
        expect(ifs.length).toBeGreaterThan(0);
    });

    test("IfStatement.consequent is a non-empty array", () => {
        const prog = parseFixture("do/do-forever-break-continue.TXT");
        const ifs = collectNodes(prog, NodeType.IfStatement);
        const withConsequent = ifs.filter(n => n.consequent.length > 0);
        expect(withConsequent.length).toBeGreaterThan(0);
    });

    test("IfStatement.condition is a TokenSpan", () => {
        const prog = parseFixture("do/do-forever-break-continue.TXT");
        const ifs = collectNodes(prog, NodeType.IfStatement);
        expect(ifs[0].condition).not.toBeNull();
        expect(Array.isArray(ifs[0].condition.tokens)).toBe(true);
        expect(ifs[0].condition.tokens.length).toBeGreaterThan(0);
    });

    test("IfStatement.alternate receives statements after ELSE", () => {
        // Build a synthetic document with ELSE
        const text = [
            "#HASH-OFF",
            "IF x = 1",
            "  SET y = 1",
            "ELSE",
            "  SET y = 2",
            "ENDIF",
        ].join("\n");
        const prog = parse(tokenize(text));
        const ifs = collectNodes(prog, NodeType.IfStatement);
        expect(ifs.length).toBe(1);
        expect(ifs[0].consequent.length).toBeGreaterThan(0);
        expect(ifs[0].alternate.length).toBeGreaterThan(0);
    });


    test("EndIfStatement is in parent scope, not inside IfStatement", () => {
        const text = [
            "#HASH-OFF",
            "IF x = 1",
            "  SET y = 1",
            "ENDIF",
        ].join("\n");
        const prog = parse(tokenize(text));
        const endIfs = prog.statements.filter(s => s.type === NodeType.EndIfStatement);
        expect(endIfs.length).toBe(1);
        // Should not be inside the IfStatement
        const ifNode = prog.statements.find(s => s.type === NodeType.IfStatement);
        expect(ifNode.consequent.every(s => s.type !== NodeType.EndIfStatement)).toBe(true);
    });
});

describe("parser — DoStatement with body", () => {
    test("DoStatement is present in do-while fixture", () => {
        const prog = parseFixture("do/do-while-basic.TXT");
        const dos = collectNodes(prog, NodeType.DoStatement);
        expect(dos.length).toBeGreaterThan(0);
    });

    test("DoStatement.kind is set correctly", () => {
        const prog = parseFixture("do/do-while-basic.TXT");
        const dos = collectNodes(prog, NodeType.DoStatement);
        expect(dos[0].kind).toBe("while");
    });

    test("DoStatement.body contains nested statements", () => {
        const prog = parseFixture("do/do-while-basic.TXT");
        const dos = collectNodes(prog, NodeType.DoStatement);
        const withBodies = dos.filter(d => d.body.length > 0);
        expect(withBodies.length).toBeGreaterThan(0);
    });

    test("DoStatement kind=forever for DO FOREVER", () => {
        const prog = parseFixture("do/do-forever-break-continue.TXT");
        const dos = collectNodes(prog, NodeType.DoStatement);
        const forever = dos.filter(d => d.kind === "forever");
        expect(forever.length).toBeGreaterThan(0);
    });

    test("BREAK and CONTINUE are nested inside DoStatement body", () => {
        const prog = parseFixture("do/do-forever-break-continue.TXT");
        const dos = collectNodes(prog, NodeType.DoStatement);
        const breaks = dos.flatMap(d => collectNodes(d, NodeType.BreakStatement));
        const continues = dos.flatMap(d => collectNodes(d, NodeType.ContinueStatement));
        expect(breaks.length).toBeGreaterThan(0);
        expect(continues.length).toBeGreaterThan(0);
    });

    test("standalone UNTIL closes the DO block", () => {
        const text = [
            "#HASH-OFF",
            "DO FOREVER",
            "  SET x = 1",
            "UNTIL (x = 5)",
        ].join("\n");
        const prog = parse(tokenize(text));
        const dos = collectNodes(prog, NodeType.DoStatement);
        expect(dos.length).toBe(1);
        expect(dos[0].body.length).toBeGreaterThan(0);
        // EndDoStatement should be in parent scope
        const endDos = prog.statements.filter(s => s.type === NodeType.EndDoStatement);
        expect(endDos.length).toBe(1);
        expect(endDos[0].condition).not.toBeNull();
    });

    test("DO iterator with TO and BY produces kind=iterator", () => {
        const text = [
            "#HASH-OFF",
            "DO sCounter = 0 TO 9 BY 1",
            "  SET x = 1",
            "ENDDO",
        ].join("\n");
        const prog = parse(tokenize(text));
        const dos = collectNodes(prog, NodeType.DoStatement);
        expect(dos.length).toBe(1);
        expect(dos[0].kind).toBe("iterator");
        expect(dos[0].iterator.variable).toBe("sCounter");
        expect(dos[0].iterator.to).not.toBeNull();
        expect(dos[0].iterator.by).not.toBeNull();
    });

    test("DO iterator with TO only — by is null", () => {
        const text = [
            "#HASH-OFF",
            "DO sCounter = 0 TO 9",
            "  SET x = 1",
            "ENDDO",
        ].join("\n");
        const prog = parse(tokenize(text));
        const dos = collectNodes(prog, NodeType.DoStatement);
        expect(dos[0].kind).toBe("iterator");
        expect(dos[0].iterator.to).not.toBeNull();
        expect(dos[0].iterator.by).toBeNull();
    });

    test("DO iterator with neither TO nor BY — both null", () => {
        const text = [
            "#HASH-OFF",
            "DO sCounter = 0",
            "  SET x = 1",
            "ENDDO",
        ].join("\n");
        const prog = parse(tokenize(text));
        const dos = collectNodes(prog, NodeType.DoStatement);
        expect(dos[0].kind).toBe("iterator");
        expect(dos[0].iterator.to).toBeNull();
        expect(dos[0].iterator.by).toBeNull();
    });

    test("DO iterator start expression is captured as TokenSpan", () => {
        const text = [
            "#HASH-OFF",
            "DO sCounter = nStart TO nEnd BY 2",
            "ENDDO",
        ].join("\n");
        const prog = parse(tokenize(text));
        const dos = collectNodes(prog, NodeType.DoStatement);
        const { iterator } = dos[0];
        expect(iterator.start).not.toBeNull();
        expect(iterator.start.tokens.map(t => t.value).join("")).toContain("nStart");
        expect(iterator.to.tokens.map(t => t.value).join("")).toContain("nEnd");
        expect(iterator.by.tokens.map(t => t.value).join("")).toContain("2");
    });
});

describe("parser — CaseStatement with branches", () => {
    test("CaseStatement is present in case fixture", () => {
        const prog = parseFixture("case/case-without-of.TXT");
        const cases = collectNodes(prog, NodeType.CaseStatement);
        expect(cases.length).toBe(1);
    });

    test("CaseStatement.branches contains the VALUE branches", () => {
        const prog = parseFixture("case/case-without-of.TXT");
        const cases = collectNodes(prog, NodeType.CaseStatement);
        expect(cases[0].branches.length).toBeGreaterThan(0);
    });

    test("CaseStatement.otherwise is populated by OTHERWISE clause", () => {
        const prog = parseFixture("case/case-without-of.TXT");
        const cases = collectNodes(prog, NodeType.CaseStatement);
        expect(cases[0].otherwise).not.toBeNull();
        expect(Array.isArray(cases[0].otherwise)).toBe(true);
    });

    test("CASE ... OF legacy style also parses (subject excludes OF)", () => {
        const prog = parseFixture("case/case-with-of-legacy.TXT");
        const cases = collectNodes(prog, NodeType.CaseStatement);
        expect(cases.length).toBe(1);
        expect(cases[0].branches.length).toBeGreaterThan(0);
        // 'OF' keyword should not be part of the subject span
        if (cases[0].subject) {
            const hasOf = cases[0].subject.tokens.some(
                t => t.type === "KEYWORD" && t.value.toUpperCase() === "OF"
            );
            expect(hasOf).toBe(false);
        }
    });
});

// ---------------------------------------------------------------------------
// Acceptance criterion 3: UnknownLine does not fail the parse
// ---------------------------------------------------------------------------

describe("parser — UnknownLine handling", () => {
    test("UnknownLine nodes do not cause exceptions", () => {
        // A document with gibberish lines
        const text = "#HASH-OFF\n??? completely invalid !!!\n#SET x = 1\n";
        expect(() => parse(tokenize(text))).not.toThrow();
    });

    test("UnknownLine count does not explode on a valid document", () => {
        // A well-formed document should have very few (ideally zero) UnknownLine nodes.
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const count = countUnknownLines(prog);
        // Allow a small number for edge cases not yet handled, but not the majority.
        expect(count).toBeLessThan(5);
    });

    test("parse continues after UnknownLine — subsequent statements are present", () => {
        const text = [
            "#HASH-OFF",
            "??? bad line ???",
            "VAR x : SHORT",
            "SET x = 1",
        ].join("\n");
        const prog = parse(tokenize(text));
        const vars = collectNodes(prog, NodeType.VarStatement);
        const sets = collectNodes(prog, NodeType.SetStatement);
        expect(vars.length).toBe(1);
        expect(sets.length).toBe(1);
    });
});

// ---------------------------------------------------------------------------
// Additional structural checks
// ---------------------------------------------------------------------------

describe("parser — node properties", () => {
    test("VarStatement has names[] and varType", () => {
        const text = "#HASH-OFF\nVAR x, y : SHORT\n";
        const prog = parse(tokenize(text));
        const vars = collectNodes(prog, NodeType.VarStatement);
        expect(vars.length).toBe(1);
        expect(vars[0].names).toContain("x");
        expect(vars[0].names).toContain("y");
        expect(vars[0].varType).toBe("SHORT");
    });

    test("VarStatement supports alternate type-first syntax", () => {
        const text = "#HASH-OFF\nSHORT myVar\n";
        const prog = parse(tokenize(text));
        const vars = collectNodes(prog, NodeType.VarStatement);
        expect(vars.length).toBe(1);
        expect(vars[0].names).toContain("myVar");
        expect(vars[0].varType).toBe("SHORT");
    });

    test("SetStatement has target string and value TokenSpan", () => {
        const text = "#HASH-OFF\nSET myVar = GET_TOKEN(x, \",\", 1)\n";
        const prog = parse(tokenize(text));
        const sets = collectNodes(prog, NodeType.SetStatement);
        expect(sets.length).toBe(1);
        expect(sets[0].target).toBe("myVar");
        expect(sets[0].value).not.toBeNull();
        expect(Array.isArray(sets[0].value.tokens)).toBe(true);
    });

    test("HashOffDirective is emitted for #HASH-OFF", () => {
        const prog = parseFixture("hash-mode/hash-mode-explicit-off.TXT");
        const directives = collectNodes(prog, NodeType.HashOffDirective);
        expect(directives.length).toBe(1);
    });

    test("IncludeDirective carries filename", () => {
        const prog = parseFixture("included-files/include-behaviour.TXT");
        const includes = collectNodes(prog, NodeType.IncludeDirective);
        expect(includes.length).toBeGreaterThan(0);
        expect(typeof includes[0].filename).toBe("string");
        expect(includes[0].filename.length).toBeGreaterThan(0);
    });

    test("MacroDeclaration has name and body span", () => {
        const prog = parseFixture("macro/macro-redeclare-case.TXT");
        const macros = collectNodes(prog, NodeType.MacroDeclaration);
        expect(macros.length).toBeGreaterThan(0);
        expect(typeof macros[0].name).toBe("string");
    });

    test("OutputLine is emitted for output text lines", () => {
        const prog = parseFixture("hash-mode/hash-mode-on-unprefixed-output.TXT");
        const outputs = collectNodes(prog, NodeType.OutputLine);
        expect(outputs.length).toBeGreaterThan(0);
    });

    test("ReturnStatement is present inside function bodies", () => {
        const prog = parseFixture("function-user-defined/func-user-defined-return-validation.TXT");
        const returns = collectNodes(prog, NodeType.ReturnStatement);
        expect(returns.length).toBeGreaterThan(0);
    });

    test("NewInterpreterDirective is emitted for NEWINTERPRETER", () => {
        const prog = parseFixture("db-field-names/db-field-handles-known.TXT");
        const ni = collectNodes(prog, NodeType.NewInterpreterDirective);
        expect(ni.length).toBeGreaterThan(0);
    });
});
