/**
 * Unit tests for documentFacts — cache hit, miss, invalidation, and LRU eviction.
 *
 * The cache is module-level state (Map). Each test uses a unique URI so that
 * entries from earlier tests do not interfere. The LRU eviction test uses
 * jest.resetModules() combined with dynamic import() to obtain a fresh module
 * instance with an empty factsCache.
 */

import { createMockDocument } from "../helpers/mockDocument.js";
import { jest } from '@jest/globals';
import { collectDocumentFacts, invalidateDocumentFacts } from "../../core/src/analysis/documentFacts.js";

const BLANK_LINES = [''];

/** Creates a minimal document with the given URI and version. */
function blankDoc(uri, version = 1) {
    const doc = createMockDocument(BLANK_LINES, uri);
    return { ...doc, version };
}

describe('documentFacts', () => {
    // -----------------------------------------------------------------------
    describe('cache hit', () => {
        test('returns the same object reference on repeated calls with unchanged (uri, version)', () => {
            const doc = blankDoc('/test/df-cache-hit.txt');
            const facts1 = collectDocumentFacts(doc);
            const facts2 = collectDocumentFacts(doc);
            expect(facts1).toBe(facts2);
        });

        test('returned facts object is frozen', () => {
            const doc = blankDoc('/test/df-frozen.txt');
            const facts = collectDocumentFacts(doc);
            expect(Object.isFrozen(facts)).toBe(true);
        });
    });

    // -----------------------------------------------------------------------
    describe('cache miss', () => {
        test('recomputes when document version increments', () => {
            const uri = '/test/df-version-bump.txt';
            const facts1 = collectDocumentFacts(blankDoc(uri, 1));
            const facts2 = collectDocumentFacts(blankDoc(uri, 2));
            expect(facts1).not.toBe(facts2);
        });

        test('cache stores one slot per URI — older version is evicted by newer', () => {
            const uri = '/test/df-one-slot.txt';
            const facts1 = collectDocumentFacts(blankDoc(uri, 1));
            collectDocumentFacts(blankDoc(uri, 2));       // overwrites the v1 slot
            const facts1b = collectDocumentFacts(blankDoc(uri, 1)); // v1 → miss
            expect(facts1b).not.toBe(facts1);
        });

        test('different URIs are cached independently', () => {
            const docA = blankDoc('/test/df-independent-a.txt');
            const docB = blankDoc('/test/df-independent-b.txt');
            const factsA = collectDocumentFacts(docA);
            const factsB = collectDocumentFacts(docB);
            expect(factsA).not.toBe(factsB);
            // Both should still be in cache
            expect(collectDocumentFacts(docA)).toBe(factsA);
            expect(collectDocumentFacts(docB)).toBe(factsB);
        });
    });

    // -----------------------------------------------------------------------
    describe('invalidateDocumentFacts', () => {
        test('forces recomputation on the next call', () => {
            const doc = blankDoc('/test/df-invalidate.txt');
            const facts1 = collectDocumentFacts(doc);
            invalidateDocumentFacts(doc.uri.toString());
            const facts2 = collectDocumentFacts(doc);
            expect(facts1).not.toBe(facts2);
        });

        test('result is stable (cache hit) immediately after re-population', () => {
            const doc = blankDoc('/test/df-stable-after-invalidate.txt');
            invalidateDocumentFacts(doc.uri.toString()); // no-op — not in cache yet
            const facts1 = collectDocumentFacts(doc);
            const facts2 = collectDocumentFacts(doc);
            expect(facts1).toBe(facts2);
        });

        test('does not affect other cached entries', () => {
            const docA = blankDoc('/test/df-invalidate-other-a.txt');
            const docB = blankDoc('/test/df-invalidate-other-b.txt');
            const factsA = collectDocumentFacts(docA);
            collectDocumentFacts(docB);
            invalidateDocumentFacts(docB.uri.toString());
            expect(collectDocumentFacts(docA)).toBe(factsA); // A untouched
        });
    });

    // -----------------------------------------------------------------------
    describe('facts shape', () => {
        test('result contains all documented keys', () => {
            const facts = collectDocumentFacts(blankDoc('/test/df-shape.txt'));
            for (const key of [
                'functionScopes',
                'executionClassificationMap',
                'outputDelimiters',
                'functionValidationTargetsByLine',
                'outputLineMap',
                'interpolationTargetsByLine',
                'userDefinedFunctionCatalog',
                'userDefinedFunctionCaseMap',
                'functionDuplicates',
                'macroCatalog',
                'macroCaseMap',
                'macroDuplicates',
                'includedVariablesByLine',
                'includedFunctionsByLine',
                'includedFunctionCaseMapByLine'
            ]) {
                expect(facts).toHaveProperty(key);
            }
        });
    });

    // -----------------------------------------------------------------------
    describe('LRU eviction', () => {
        // NOTE: This test assumes MAX_CACHE_SIZE === 50 in documentFacts.js.
        // If that constant changes, update the loop count here (50 fillers to reach 51)
        // and the test description accordingly.  The module is reloaded via
        // jest.resetModules() + dynamic import() so collectDocumentFacts starts with
        // an empty factsCache.
        test('evicts the oldest entry once the cache exceeds 50 entries', async () => {
            // Reset module registry so we get a fresh, empty factsCache.
            // In Jest ESM mode, jest.resetModules() + dynamic import gives an isolated module.
            jest.resetModules();
            const { collectDocumentFacts: collectFresh } =
                await import('../../core/src/analysis/documentFacts.js');

            const firstDoc = blankDoc('/test/df-evict-first.txt');
            const factsFirst1 = collectFresh(firstDoc); // cache size → 1

            // Add 50 more unique docs; the 50th addition brings size to 51,
            // triggering eviction of firstDoc (oldest).
            for (let i = 0; i < 50; i++) {
                collectFresh(blankDoc(`/test/df-evict-filler-${i}.txt`));
            }

            // firstDoc was evicted — recomputation produces a new object
            const factsFirst2 = collectFresh(firstDoc);
            expect(factsFirst2).not.toBe(factsFirst1);

            // Fresh entry is now stable in the cache
            const factsFirst3 = collectFresh(firstDoc);
            expect(factsFirst3).toBe(factsFirst2);
        });
    });
});
