import { describe, expect, test } from "bun:test";
import { cacheManifest, computeRelevanceScore, getCachedManifest, getSessionAssemblyId, mapToContextEntity, trackSessionAssembly, } from "../context-assembly.js";
function makeEntity(overrides = {}) {
    return {
        id: "test-id",
        type: "pattern",
        title: "Test Pattern",
        content: "Some test content about React hooks and performance optimization.",
        confidence: 0.9,
        tags: ["react", "performance"],
        memory_tier: "reference",
        access_count: 5,
        last_accessed_at: new Date().toISOString(),
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        ...overrides,
    };
}
// ─── computeRelevanceScore ──────────────────────────────────────────
describe("computeRelevanceScore", () => {
    test("text match contributes to score", () => {
        const entity = makeEntity({
            title: "React hooks patterns",
            content: "How to use React hooks effectively for state management",
        });
        const { score, reasons } = computeRelevanceScore(entity, "React hooks state management", []);
        expect(score).toBeGreaterThan(0);
        expect(reasons).toContainEqual(expect.stringContaining("text_match"));
    });
    test("no text match gives lower score", () => {
        const entity = makeEntity({
            title: "Database migration guide",
            content: "How to run PostgreSQL migrations",
        });
        const { score: matchScore } = computeRelevanceScore(entity, "Database migration PostgreSQL", []);
        const entity2 = makeEntity({
            title: "Unrelated topic",
            content: "Nothing matching here at all xyz abc",
        });
        const { score: noMatchScore } = computeRelevanceScore(entity2, "Database migration PostgreSQL", []);
        expect(matchScore).toBeGreaterThan(noMatchScore);
    });
    test("tag overlap boosts score", () => {
        const entity = makeEntity({ tags: ["react", "hooks"] });
        const withLabels = computeRelevanceScore(entity, "test", [
            "react",
            "hooks",
        ]);
        const withoutLabels = computeRelevanceScore(entity, "test", []);
        expect(withLabels.score).toBeGreaterThan(withoutLabels.score);
        expect(withLabels.reasons).toContainEqual(expect.stringContaining("tag_match"));
    });
    test("high confidence gives higher score", () => {
        const highConf = makeEntity({ confidence: 1.0 });
        const lowConf = makeEntity({ confidence: 0.3 });
        const high = computeRelevanceScore(highConf, "test", []);
        const low = computeRelevanceScore(lowConf, "test", []);
        expect(high.score).toBeGreaterThan(low.score);
    });
    test("recently accessed gets recency bonus", () => {
        const recent = makeEntity({
            last_accessed_at: new Date().toISOString(),
        });
        const old = makeEntity({
            last_accessed_at: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
        });
        const recentScore = computeRelevanceScore(recent, "test", []);
        const oldScore = computeRelevanceScore(old, "test", []);
        expect(recentScore.score).toBeGreaterThan(oldScore.score);
    });
    test("tier weight applied: reference > episode > draft", () => {
        const base = {
            title: "Test",
            content: "matching content here",
            confidence: 0.9,
            tags: [],
            access_count: 5,
            last_accessed_at: new Date().toISOString(),
        };
        const ref = computeRelevanceScore(makeEntity({ ...base, memory_tier: "reference" }), "matching content", []);
        const ep = computeRelevanceScore(makeEntity({ ...base, memory_tier: "episode" }), "matching content", []);
        const dr = computeRelevanceScore(makeEntity({ ...base, memory_tier: "draft" }), "matching content", []);
        expect(ref.score).toBeGreaterThan(ep.score);
        expect(ep.score).toBeGreaterThan(dr.score);
    });
    // --- RRF / hybrid search signal ---
    test("RRF score contributes when present", () => {
        // Use content with no text overlap to isolate RRF signal
        const withRrf = makeEntity({
            rrf_score: 0.03,
            title: "Unrelated xyz",
            content: "Unrelated abc def",
            tags: [],
        });
        const withoutRrf = makeEntity({
            rrf_score: undefined,
            title: "Unrelated xyz",
            content: "Unrelated abc def",
            tags: [],
        });
        const rrf = computeRelevanceScore(withRrf, "something else entirely", []);
        const noRrf = computeRelevanceScore(withoutRrf, "something else entirely", []);
        expect(rrf.score).toBeGreaterThan(noRrf.score);
        expect(rrf.reasons).toContainEqual(expect.stringContaining("hybrid_search"));
    });
    test("RRF score reduces text match weight", () => {
        // With RRF, text_match weight drops from 0.4 to 0.15
        // So text overlap contributes less when RRF is present
        const entity = makeEntity({
            title: "React hooks patterns",
            content: "How to use React hooks",
            rrf_score: 0.04,
        });
        const { reasons } = computeRelevanceScore(entity, "React hooks patterns", []);
        // Should still have text_match but hybrid_search should be present too
        expect(reasons).toContainEqual(expect.stringContaining("hybrid_search"));
        expect(reasons).toContainEqual(expect.stringContaining("text_match"));
    });
    test("RRF score of 0 is treated as absent", () => {
        const withZeroRrf = makeEntity({ rrf_score: 0 });
        const withoutRrf = makeEntity({ rrf_score: undefined });
        const zero = computeRelevanceScore(withZeroRrf, "test", []);
        const none = computeRelevanceScore(withoutRrf, "test", []);
        // Zero RRF should not contribute, so scores should be equal
        expect(zero.score).toBe(none.score);
    });
    // --- Procedure boost ---
    test("procedure type gets a boost", () => {
        const procedure = makeEntity({ type: "procedure" });
        const pattern = makeEntity({ type: "pattern" });
        const procScore = computeRelevanceScore(procedure, "test", []);
        const patScore = computeRelevanceScore(pattern, "test", []);
        expect(procScore.score).toBeGreaterThan(patScore.score);
        expect(procScore.reasons).toContainEqual(expect.stringContaining("procedure_boost"));
    });
    // --- Usefulness feedback ---
    test("high usefulness score boosts relevance", () => {
        const useful = makeEntity({
            metadata: { usefulness_score: 10 },
        });
        const notUseful = makeEntity({
            metadata: { usefulness_score: 0 },
        });
        const usefulResult = computeRelevanceScore(useful, "test", []);
        const notUsefulResult = computeRelevanceScore(notUseful, "test", []);
        expect(usefulResult.score).toBeGreaterThan(notUsefulResult.score);
        expect(usefulResult.reasons).toContainEqual(expect.stringContaining("useful"));
    });
    test("low usefulness with high access gives penalty", () => {
        const lowUsefulness = makeEntity({
            access_count: 10,
            metadata: { usefulness_score: 0 },
        });
        const noMetadata = makeEntity({
            access_count: 10,
            metadata: { usefulness_score: 5 },
        });
        const low = computeRelevanceScore(lowUsefulness, "test", []);
        const normal = computeRelevanceScore(noMetadata, "test", []);
        expect(low.reasons).toContainEqual(expect.stringContaining("low_usefulness"));
        expect(low.score).toBeLessThan(normal.score);
    });
    // --- Access frequency ---
    test("frequently accessed entities get a bonus", () => {
        const frequent = makeEntity({ access_count: 20 });
        const rare = makeEntity({ access_count: 0 });
        const freqResult = computeRelevanceScore(frequent, "test", []);
        const rareResult = computeRelevanceScore(rare, "test", []);
        expect(freqResult.score).toBeGreaterThan(rareResult.score);
        expect(freqResult.reasons).toContainEqual(expect.stringContaining("frequently_used"));
    });
    // --- Score clamping ---
    test("score is clamped to 0-1 range before tier weight", () => {
        // Stack everything possible to try to exceed 1.0
        const entity = makeEntity({
            title: "React hooks state management patterns best practices",
            content: "React hooks state management patterns best practices guide",
            confidence: 1.0,
            tags: ["react", "hooks", "state"],
            memory_tier: "reference",
            access_count: 100,
            rrf_score: 0.04,
            last_accessed_at: new Date().toISOString(),
            metadata: { usefulness_score: 20 },
            type: "procedure",
        });
        const { score } = computeRelevanceScore(entity, "React hooks state management patterns best practices", ["react", "hooks", "state"]);
        // Reference tier weight is 1.0, so max score should be 1.0
        expect(score).toBeLessThanOrEqual(1.0);
        expect(score).toBeGreaterThan(0);
    });
    // --- Tag matching is case-insensitive ---
    test("tag matching is case-insensitive", () => {
        const entity = makeEntity({ tags: ["React", "HOOKS"] });
        const { reasons } = computeRelevanceScore(entity, "test", [
            "react",
            "hooks",
        ]);
        expect(reasons).toContainEqual(expect.stringContaining("tag_match"));
    });
    // --- No last_accessed_at means no recency bonus ---
    test("null last_accessed_at skips recency scoring", () => {
        const noAccess = makeEntity({ last_accessed_at: null });
        const { reasons } = computeRelevanceScore(noAccess, "test", []);
        expect(reasons).not.toContainEqual(expect.stringContaining("recently_accessed"));
    });
    // --- Words shorter than 3 chars are filtered ---
    test("short words (<3 chars) are ignored in text matching", () => {
        const entity = makeEntity({
            title: "A B C",
            content: "to be or not to be",
        });
        const { reasons } = computeRelevanceScore(entity, "to be or not", []);
        // "to", "be", "or" are all ≤2 chars, "not" is 3 chars
        // Only "not" should be considered
        expect(reasons).toContainEqual(expect.stringContaining("text_match"));
    });
});
// ─── mapToContextEntity ─────────────────────────────────────────────
describe("mapToContextEntity", () => {
    test("maps raw API response to ContextEntity", () => {
        const raw = {
            id: "abc-123",
            type: "pattern",
            title: "Test",
            content: "Content",
            confidence: 0.8,
            tags: ["tag1"],
            memory_tier: "episode",
            access_count: 3,
            last_accessed_at: "2025-01-01T00:00:00Z",
            updated_at: "2025-01-01T00:00:00Z",
        };
        const entity = mapToContextEntity(raw);
        expect(entity.id).toBe("abc-123");
        expect(entity.memory_tier).toBe("episode");
        expect(entity.access_count).toBe(3);
    });
    test("defaults missing fields", () => {
        const raw = {
            id: "abc",
            type: "context",
            title: "Test",
            content: "Content",
        };
        const entity = mapToContextEntity(raw);
        expect(entity.memory_tier).toBe("reference");
        expect(entity.access_count).toBe(0);
        expect(entity.confidence).toBe(1.0);
        expect(entity.tags).toEqual([]);
        expect(entity.last_accessed_at).toBeNull();
    });
    test("preserves hybrid search signals when present", () => {
        const raw = {
            id: "abc",
            type: "pattern",
            title: "Test",
            content: "Content",
            rrf_score: 0.025,
            fts_rank: 3,
            semantic_rank: 5,
        };
        const entity = mapToContextEntity(raw);
        expect(entity.rrf_score).toBe(0.025);
        expect(entity.fts_rank).toBe(3);
        expect(entity.semantic_rank).toBe(5);
    });
    test("metadata is preserved when present", () => {
        const raw = {
            id: "abc",
            type: "pattern",
            title: "Test",
            content: "Content",
            metadata: { usefulness_score: 5, custom_field: "value" },
        };
        const entity = mapToContextEntity(raw);
        expect(entity.metadata).toEqual({
            usefulness_score: 5,
            custom_field: "value",
        });
    });
});
// ─── Manifest caching ──────────────────────────────────────────────
describe("manifest caching", () => {
    test("cacheManifest stores and getCachedManifest retrieves", () => {
        const manifest = {
            assemblyId: "ctx_test_cache1",
            timestamp: new Date().toISOString(),
            included: [],
            excluded: [],
            budgetUsed: 0,
            budgetTotal: 4000,
            tierBreakdown: {
                draft: { count: 0, tokens: 0 },
                episode: { count: 0, tokens: 0 },
                reference: { count: 0, tokens: 0 },
            },
        };
        cacheManifest(manifest);
        const retrieved = getCachedManifest("ctx_test_cache1");
        expect(retrieved).toBeDefined();
        expect(retrieved?.assemblyId).toBe("ctx_test_cache1");
    });
    test("getCachedManifest returns undefined for unknown ID", () => {
        const result = getCachedManifest("ctx_nonexistent_12345");
        expect(result).toBeUndefined();
    });
    test("cacheManifest evicts oldest when exceeding MAX_CACHE_SIZE", () => {
        // Fill cache with 50+ manifests (MAX_CACHE_SIZE = 50)
        const firstId = `ctx_evict_first_${Date.now()}`;
        cacheManifest({
            assemblyId: firstId,
            timestamp: new Date().toISOString(),
            included: [],
            excluded: [],
            budgetUsed: 0,
            budgetTotal: 4000,
            tierBreakdown: {
                draft: { count: 0, tokens: 0 },
                episode: { count: 0, tokens: 0 },
                reference: { count: 0, tokens: 0 },
            },
        });
        // Add 50 more to push the first one out
        for (let i = 0; i < 50; i++) {
            cacheManifest({
                assemblyId: `ctx_evict_fill_${i}_${Date.now()}`,
                timestamp: new Date().toISOString(),
                included: [],
                excluded: [],
                budgetUsed: 0,
                budgetTotal: 4000,
                tierBreakdown: {
                    draft: { count: 0, tokens: 0 },
                    episode: { count: 0, tokens: 0 },
                    reference: { count: 0, tokens: 0 },
                },
            });
        }
        // The first entry should have been evicted
        const firstResult = getCachedManifest(firstId);
        expect(firstResult).toBeUndefined();
    });
});
// ─── Session assembly tracking ──────────────────────────────────────
describe("session assembly tracking", () => {
    test("trackSessionAssembly and getSessionAssemblyId round-trip", () => {
        const cardId = "card-track-test-1";
        const assemblyId = "ctx_track_test_1";
        trackSessionAssembly(cardId, assemblyId);
        expect(getSessionAssemblyId(cardId)).toBe(assemblyId);
    });
    test("getSessionAssemblyId returns undefined for untracked card", () => {
        expect(getSessionAssemblyId("card-nonexistent-xyz")).toBeUndefined();
    });
    test("trackSessionAssembly overwrites previous value for same card", () => {
        const cardId = "card-overwrite-test";
        trackSessionAssembly(cardId, "ctx_old");
        trackSessionAssembly(cardId, "ctx_new");
        expect(getSessionAssemblyId(cardId)).toBe("ctx_new");
    });
});
