/**
 * Unit tests for functionReturnTypeValidator.
 *
 * Documents use #HASH-OFF so that bare lines (without # prefix) are treated as
 * code by the lexer. HASH-ON mode (the default) treats prefix-less lines as
 * output text, which would mean no function/return nodes reach the validator.
 */

import { validateReturnTypes, inferReturnExpressionType } from "../../../core/src/validators/functionReturnTypeValidator.js";
import { makeDocument, alwaysError, alwaysOff, messageIds } from "./helpers.js";
import { tokenize } from "../../../core/src/lexer/lexer.js";
import { TokenType } from "../../../core/src/lexer/tokenTypes.js";

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

function makeScope({ name, startLine, endLine, params = {}, locals = {}, returnType = null }) {
    return {
        name,
        upperName: name.toUpperCase(),
        startLine,
        endLine,
        params: new Map(Object.entries(params)),
        locals: new Map(Object.entries(locals)),
        returnType
    };
}

/**
 * Wraps a raw expression string in a minimal ReturnStatement-shaped object
 * so it can be passed to inferReturnExpressionType in unit tests.
 *
 * Prefixes with '# ' so the HASH-ON tokenizer treats the expression as code,
 * then strips structural tokens (HASH_PREFIX, WHITESPACE, NEWLINE, EOF) to
 * match the TokenSpan format produced by the AST parser.
 */
const SKIP_TYPES = new Set([
    TokenType.HASH_PREFIX, TokenType.WHITESPACE, TokenType.NEWLINE, TokenType.EOF
]);
function makeReturnStmt(exprText) {
    if (!exprText) return { value: null };
    const tokens = tokenize('# ' + exprText).filter(t => !SKIP_TYPES.has(t.type));
    if (!tokens.length) return { value: null };
    const first = tokens[0];
    const last = tokens[tokens.length - 1];
    return {
        value: {
            tokens,
            start: { line: first.line, char: first.char },
            end: { line: last.line, char: last.char + last.value.length }
        }
    };
}

// ---------------------------------------------------------------------------
// inferReturnExpressionType — unit tests
// ---------------------------------------------------------------------------

describe('inferReturnExpressionType', () => {
    const params = new Map([['pzStr', 'STRING'], ['psIdx', 'SHORT']]);
    const locals = new Map([['fzResult', 'STRING'], ['fsCount', 'SHORT']]);

    test('returns STRING for double-quoted string literal', () => {
        expect(inferReturnExpressionType(makeReturnStmt('"hello"'), params, locals)).toBe('STRING');
    });

    test('returns STRING for single-quoted string literal', () => {
        expect(inferReturnExpressionType(makeReturnStmt("'hi'"), params, locals)).toBe('STRING');
    });

    test('returns DOUBLE for decimal literal', () => {
        expect(inferReturnExpressionType(makeReturnStmt('3.14'), params, locals)).toBe('DOUBLE');
    });

    test('returns SHORT for integer literal', () => {
        expect(inferReturnExpressionType(makeReturnStmt('42'), params, locals)).toBe('SHORT');
    });

    test('returns SHORT for negative integer literal', () => {
        expect(inferReturnExpressionType(makeReturnStmt('-1'), params, locals)).toBe('SHORT');
    });

    test('returns STRING for STRING parameter (case-insensitive)', () => {
        expect(inferReturnExpressionType(makeReturnStmt('PZSTR'), params, locals)).toBe('STRING');
    });

    test('returns SHORT for SHORT parameter', () => {
        expect(inferReturnExpressionType(makeReturnStmt('psIdx'), params, locals)).toBe('SHORT');
    });

    test('returns STRING for STRING local variable', () => {
        expect(inferReturnExpressionType(makeReturnStmt('fzResult'), params, locals)).toBe('STRING');
    });

    test('returns SHORT for SHORT local variable', () => {
        expect(inferReturnExpressionType(makeReturnStmt('fsCount'), params, locals)).toBe('SHORT');
    });

    test('returns null for unknown variable name', () => {
        expect(inferReturnExpressionType(makeReturnStmt('unknownVar'), params, locals)).toBeNull();
    });

    test('returns null for complex expression (function call)', () => {
        expect(inferReturnExpressionType(makeReturnStmt('GET_TOKEN(pzStr, ",", 1)'), params, locals)).toBeNull();
    });

    test('returns null for empty expression (bare RETURN)', () => {
        expect(inferReturnExpressionType(makeReturnStmt(''), params, locals)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// validateReturnTypes — integration
// ---------------------------------------------------------------------------

describe('validateReturnTypes', () => {
    describe('ACU-FNC-010 — return type mismatch', () => {
        test('fires when SHORT param is returned from STRING function', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO(psIdx : SHORT) : STRING',
                '    RETURN psIdx',
                'ENDFUNCTION'
            ]);
            // FUNCTION is at line 1, ENDFUNCTION at line 3
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3,
                params: { psIdx: 'SHORT' }, returnType: 'STRING'
            });
            const diags = validateReturnTypes(doc, alwaysError, { functionScopes: [scope] });
            expect(messageIds(diags)).toEqual(['ACU-FNC-010']);
            expect(diags[0].range.start.line).toBe(2);
        });

        test('fires when STRING param is returned from SHORT function', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO(pzStr : STRING) : SHORT',
                '    RETURN pzStr',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3,
                params: { pzStr: 'STRING' }, returnType: 'SHORT'
            });
            const diags = validateReturnTypes(doc, alwaysError, { functionScopes: [scope] });
            expect(messageIds(diags)).toEqual(['ACU-FNC-010']);
        });

        test('fires when SHORT local variable is returned from STRING function', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO() : STRING',
                '    SHORT fsCount',
                '    RETURN fsCount',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 4,
                locals: { fsCount: 'SHORT' }, returnType: 'STRING'
            });
            const diags = validateReturnTypes(doc, alwaysError, { functionScopes: [scope] });
            expect(messageIds(diags)).toEqual(['ACU-FNC-010']);
            expect(diags[0].range.start.line).toBe(3);
        });

        test('includes correct params in diagnostic', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO(psIdx : SHORT) : STRING',
                '    RETURN psIdx',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3,
                params: { psIdx: 'SHORT' }, returnType: 'STRING'
            });
            const [diag] = validateReturnTypes(doc, alwaysError, { functionScopes: [scope] });
            expect(diag.data.params.functionName).toBe('FOO');
            expect(diag.data.params.expectedType).toBe('STRING');
            expect(diag.data.params.actualType).toBe('SHORT');
            expect(diag.data.params.expression).toBe('psIdx');
        });

        test('does not fire when return type matches (STRING)', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO(pzStr : STRING) : STRING',
                '    RETURN pzStr',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3,
                params: { pzStr: 'STRING' }, returnType: 'STRING'
            });
            expect(validateReturnTypes(doc, alwaysError, { functionScopes: [scope] })).toEqual([]);
        });

        test('does not fire for complex/unknown expression', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO() : STRING',
                '    RETURN GET_TOKEN(x, ",", 1)',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3, returnType: 'STRING'
            });
            expect(validateReturnTypes(doc, alwaysError, { functionScopes: [scope] })).toEqual([]);
        });

        test('respects getRuleSeverity — no diagnostic when rule is off', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO(psIdx : SHORT) : STRING',
                '    RETURN psIdx',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3,
                params: { psIdx: 'SHORT' }, returnType: 'STRING'
            });
            expect(validateReturnTypes(doc, alwaysOff, { functionScopes: [scope] })).toEqual([]);
        });
    });

    describe('ACU-FNC-011 — missing RETURN in typed function', () => {
        test('fires when typed function has no RETURN', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO() : STRING',
                '    SHORT fsX',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3, returnType: 'STRING'
            });
            const diags = validateReturnTypes(doc, alwaysError, { functionScopes: [scope] });
            expect(messageIds(diags)).toEqual(['ACU-FNC-011']);
            expect(diags[0].range.start.line).toBe(3);
        });

        test('includes correct params in diagnostic', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO() : SHORT',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 2, returnType: 'SHORT'
            });
            const [diag] = validateReturnTypes(doc, alwaysError, { functionScopes: [scope] });
            expect(diag.data.params.functionName).toBe('FOO');
            expect(diag.data.params.returnType).toBe('SHORT');
        });

        test('does not fire for untyped function (returnType null)', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO()',
                '    SHORT fsX',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3, returnType: null
            });
            expect(validateReturnTypes(doc, alwaysError, { functionScopes: [scope] })).toEqual([]);
        });

        test('does not fire when function has a RETURN', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO() : STRING',
                '    RETURN "ok"',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3, returnType: 'STRING'
            });
            expect(validateReturnTypes(doc, alwaysError, { functionScopes: [scope] })).toEqual([]);
        });

        test('respects getRuleSeverity — no diagnostic when rule is off', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO() : STRING',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 2, returnType: 'STRING'
            });
            expect(validateReturnTypes(doc, alwaysOff, { functionScopes: [scope] })).toEqual([]);
        });
    });

    describe('edge cases', () => {
        test('returns empty array when functionScopes is empty', () => {
            const doc = makeDocument(['#HASH-OFF', 'RETURN "x"']);
            expect(validateReturnTypes(doc, alwaysError, { functionScopes: [] })).toEqual([]);
        });

        test('handles multiple functions, each diagnosed independently', () => {
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION A(psIdx : SHORT) : STRING',
                '    RETURN psIdx',
                'ENDFUNCTION',
                'FUNCTION B(pzStr : STRING) : SHORT',
                '    RETURN pzStr',
                'ENDFUNCTION'
            ]);
            const scopes = [
                makeScope({ name: 'A', startLine: 1, endLine: 3, params: { psIdx: 'SHORT' }, returnType: 'STRING' }),
                makeScope({ name: 'B', startLine: 4, endLine: 6, params: { pzStr: 'STRING' }, returnType: 'SHORT' })
            ];
            const diags = validateReturnTypes(doc, alwaysError, { functionScopes: scopes });
            expect(diags).toHaveLength(2);
            expect(diags[0].range.start.line).toBe(2);
            expect(diags[1].range.start.line).toBe(5);
        });

        test('RETURN inside comment on line does not create a ReturnStatement node', () => {
            // The AST parser strips trailing comments, so only the actual RETURN
            // statement in the body is captured — comment text is not parsed as RETURN.
            const doc = makeDocument([
                '#HASH-OFF',
                'FUNCTION FOO(pzStr : STRING) : STRING',
                '    RETURN pzStr // RETURN psIdx would be wrong',
                'ENDFUNCTION'
            ]);
            const scope = makeScope({
                name: 'FOO', startLine: 1, endLine: 3,
                params: { pzStr: 'STRING' }, returnType: 'STRING'
            });
            expect(validateReturnTypes(doc, alwaysError, { functionScopes: [scope] })).toEqual([]);
        });
    });
});
