/**
 * Unit tests for variableValidator.
 *
 * Covers:
 *   ACU-VAR-001  Undefined variable (target or expression reference)
 *   ACU-VAR-002  Variable used before being assigned
 *   ACU-VAR-003  Unclosed string literal
 *   ACU-VAR-004  Pseudo-constant assigned more than once
 *   ACU-INT-002  Variable name used with incorrect capitalisation
 *
 * Tests run in global scope only (no function scopes) unless stated otherwise.
 */

import { validateVariableUsage, isPseudoConstant } from "../../../core/src/validators/variableValidator.js";
import { makeDocument, alwaysWarn, alwaysOff, messageIds } from "./helpers.js";

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

/** Builds the minimal facts object used by validateVariableUsage. */
function makeFacts(lineCount, overrides = {}) {
    return {
        functionScopes: [],
        executionClassificationMap: Array.from({ length: lineCount }, () => ({
            isOutput: false,
            isExecutable: true
        })),
        includedVariablesByLine: new Map(),
        macroCatalog: {},
        ...overrides
    };
}

/** Builds a globalVariables Map from an array of variable names. */
function makeGlobals(...names) {
    return new Map(names.map((n) => [n, { lineIndex: 0 }]));
}

// ---------------------------------------------------------------------------
// isPseudoConstant
// ---------------------------------------------------------------------------

describe('isPseudoConstant', () => {
    test('returns true for ALL_CAPS name containing CONST', () => {
        expect(isPseudoConstant('MY_CONST_VALUE')).toBe(true);
    });

    test('returns true for name with single lowercase prefix + CONST', () => {
        expect(isPseudoConstant('gMY_CONST_VALUE')).toBe(true);
    });

    test('returns false when CONST not in name', () => {
        expect(isPseudoConstant('MY_VAR_VALUE')).toBe(false);
    });

    test('returns false for mixed-case names even with CONST', () => {
        expect(isPseudoConstant('MyConstant')).toBe(false);
    });

    test('returns false for null/empty input', () => {
        expect(isPseudoConstant(null)).toBe(false);
        expect(isPseudoConstant('')).toBe(false);
    });
});

// ---------------------------------------------------------------------------
// Happy path — no diagnostics
// ---------------------------------------------------------------------------

describe('validateVariableUsage — happy path', () => {
    test('returns empty array for document with no assignments', () => {
        const doc = makeDocument(['IF x = 1', 'ENDIF']);
        const facts = makeFacts(2);
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() });
        expect(diags).toHaveLength(0);
    });

    test('returns empty array for empty document', () => {
        const doc = makeDocument(['']);
        const facts = makeFacts(1);
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() })).toHaveLength(0);
    });

    test('no diagnostic when variable is declared and assigned', () => {
        const doc = makeDocument(['SET myVar = "hello"']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals })).toHaveLength(0);
    });

    test('no diagnostic when expression references an already-assigned variable', () => {
        const doc = makeDocument([
            'SET myVar = "hello"',
            'SET otherVar = myVar'
        ]);
        const facts = makeFacts(2);
        const globals = makeGlobals('myVar', 'otherVar');
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals })).toHaveLength(0);
    });

    test('skips output lines', () => {
        const doc = makeDocument(['OUTPUT undeclaredVar']);
        const facts = makeFacts(1, {
            executionClassificationMap: [{ isOutput: true, isExecutable: false }]
        });
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() })).toHaveLength(0);
    });

    test('skips non-executable lines', () => {
        const doc = makeDocument(['// just a comment']);
        const facts = makeFacts(1, {
            executionClassificationMap: [{ isOutput: false, isExecutable: false }]
        });
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() })).toHaveLength(0);
    });

    test('does not flag built-in constants', () => {
        // TRUE is a built-in constant
        const doc = makeDocument(['SET myVar = TRUE']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals })).toHaveLength(0);
    });

    test('does not flag known database field handles (correct casing)', () => {
        const doc = makeDocument(['SET myVar = hACc_Clean_Output']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals })).toHaveLength(0);
    });

    test('does not flag variables that appear in the macroCatalog', () => {
        const doc = makeDocument(['SET MYMACRO = "x"']);
        const facts = makeFacts(1, { macroCatalog: { MYMACRO: true } });
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() })).toHaveLength(0);
    });

    test('variable in string literal is not treated as a reference', () => {
        const doc = makeDocument(['SET myVar = "undeclared"']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        expect(validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals })).toHaveLength(0);
    });
});

// ---------------------------------------------------------------------------
// ACU-VAR-001 — Undefined variable
// ---------------------------------------------------------------------------

describe('ACU-VAR-001 — undefined variable', () => {
    test('flags when assignment target is not declared', () => {
        const doc = makeDocument(['SET undeclaredVar = "hello"']);
        const facts = makeFacts(1);
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() });
        expect(messageIds(diags)).toContain('ACU-VAR-001');
    });

    test('flags when expression references an undeclared variable', () => {
        const doc = makeDocument([
            'SET myVar = "hello"',
            'SET otherVar = undeclaredRef'
        ]);
        const facts = makeFacts(2);
        const globals = makeGlobals('myVar', 'otherVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-VAR-001');
    });

    test('produces only one diagnostic per undefined target (not both VAR-001 and VAR-002)', () => {
        const doc = makeDocument(['SET undeclaredVar = "hello"']);
        const facts = makeFacts(1);
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: makeGlobals() });
        // Should flag ACU-VAR-001 and skip further checks for this line
        expect(diags).toHaveLength(1);
        expect(messageIds(diags)).toContain('ACU-VAR-001');
    });
});

// ---------------------------------------------------------------------------
// ACU-VAR-002 — Variable used before assigned
// ---------------------------------------------------------------------------

describe('ACU-VAR-002 — variable used before assigned', () => {
    test('flags when a global variable is read before it has been assigned', () => {
        // myVar is declared but not yet SET; referenced immediately
        const doc = makeDocument(['SET result = myVar']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar', 'result');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-VAR-002');
    });

    test('no diagnostic once variable has been assigned on a prior line', () => {
        const doc = makeDocument([
            'SET myVar = "hello"',
            'SET result = myVar'
        ]);
        const facts = makeFacts(2);
        const globals = makeGlobals('myVar', 'result');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).not.toContain('ACU-VAR-002');
    });
});

// ---------------------------------------------------------------------------
// ACU-VAR-003 — Unclosed string literal
// ---------------------------------------------------------------------------

describe('ACU-VAR-003 — unclosed string literal', () => {
    test('flags an unclosed double-quote string in expression', () => {
        const doc = makeDocument(['SET myVar = "hello']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-VAR-003');
    });

    test('flags an unclosed single-quote string in expression', () => {
        const doc = makeDocument(["SET myVar = 'hello"]);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-VAR-003');
    });

    test('does not flag a properly closed string', () => {
        const doc = makeDocument(['SET myVar = "hello"']);
        const facts = makeFacts(1);
        const globals = makeGlobals('myVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).not.toContain('ACU-VAR-003');
    });
});

// ---------------------------------------------------------------------------
// ACU-VAR-004 — Pseudo-constant assigned more than once
// ---------------------------------------------------------------------------

describe('ACU-VAR-004 — pseudo-constant multiple assignment', () => {
    test('flags when a pseudo-constant is assigned a second time', () => {
        const doc = makeDocument([
            'SET MY_CONST_VALUE = "first"',
            'SET MY_CONST_VALUE = "second"'
        ]);
        const facts = makeFacts(2);
        const globals = makeGlobals('MY_CONST_VALUE');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-VAR-004');
    });

    test('no diagnostic on first assignment of a pseudo-constant', () => {
        const doc = makeDocument(['SET MY_CONST_VALUE = "first"']);
        const facts = makeFacts(1);
        const globals = makeGlobals('MY_CONST_VALUE');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).not.toContain('ACU-VAR-004');
    });

    test('no diagnostic for regular (non-pseudo-constant) variables assigned multiple times', () => {
        const doc = makeDocument([
            'SET myVar = "first"',
            'SET myVar = "second"'
        ]);
        const facts = makeFacts(2);
        const globals = makeGlobals('myVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).not.toContain('ACU-VAR-004');
    });

    test('flags on second and subsequent assignments, not the first', () => {
        const doc = makeDocument([
            'SET MY_CONST_VALUE = "a"',
            'SET MY_CONST_VALUE = "b"',
            'SET MY_CONST_VALUE = "c"'
        ]);
        const facts = makeFacts(3);
        const globals = makeGlobals('MY_CONST_VALUE');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        const var004 = diags.filter((d) => d.data.messageId === 'ACU-VAR-004');
        expect(var004).toHaveLength(2);
    });
});

// ---------------------------------------------------------------------------
// ACU-INT-002 — Wrong capitalisation
// ---------------------------------------------------------------------------

describe('ACU-INT-002 — incorrect capitalisation', () => {
    test('flags when assignment target uses wrong casing', () => {
        // Declared as 'MyVar', used as 'myvar'
        const doc = makeDocument(['SET myvar = "hello"']);
        const facts = makeFacts(1);
        const globals = makeGlobals('MyVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-INT-002');
    });

    test('flags when expression reference uses wrong casing', () => {
        const doc = makeDocument([
            'SET MyVar = "hello"',
            'SET result = myvar'
        ]);
        const facts = makeFacts(2);
        const globals = makeGlobals('MyVar', 'result');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).toContain('ACU-INT-002');
    });

    test('does not emit ACU-VAR-001 when case mismatch is detected', () => {
        const doc = makeDocument(['SET myvar = "hello"']);
        const facts = makeFacts(1);
        const globals = makeGlobals('MyVar');
        const diags = validateVariableUsage(doc, alwaysWarn, { facts, globalVariables: globals });
        expect(messageIds(diags)).not.toContain('ACU-VAR-001');
    });
});

// ---------------------------------------------------------------------------
// Severity
// ---------------------------------------------------------------------------

describe('severity', () => {
    test('produces no diagnostics when severity is off', () => {
        const doc = makeDocument([
            'SET undeclaredVar = "hello"',
            'SET MY_CONST = "a"',
            'SET MY_CONST = "b"'
        ]);
        const facts = makeFacts(3, { macroCatalog: {} });
        const globals = makeGlobals('MY_CONST');
        expect(validateVariableUsage(doc, alwaysOff, { facts, globalVariables: globals })).toHaveLength(0);
    });
});
