/**
 * memory-consolidation-discipline.test.ts
 *
 * Verify the add/prune-only discipline on the consolidation (extractor) path:
 *   - applyConsolidationActions accepts "add" and "prune" actions.
 *   - applyConsolidationActions rejects "update", "supersede", and other kinds.
 *   - applyMemoryActions still accepts the full action surface (unaffected).
 *   - swarm-dispatch passes a formatted memory snapshot in the result when
 *     the parent has active memories.
 */

import { beforeEach, describe, expect, it, vi } from "vitest";

// ─── Top-level mocks (hoisted by vitest) ─────────────────────────────────────

vi.mock("../sf-db.js", () => ({
	isDbAvailable: vi.fn().mockReturnValue(true),
	_getAdapter: vi.fn().mockReturnValue(null),
	transaction: vi.fn((fn: () => void) => fn()),
	insertMemoryRow: vi.fn(),
	rewriteMemoryId: vi.fn(),
	updateMemoryContentRow: vi.fn(),
	incrementMemoryHitCount: vi.fn(),
	supersedeMemoryRow: vi.fn(),
	decayMemoriesBefore: vi.fn(),
	supersedeLowestRankedMemories: vi.fn(),
	markMemoryUnitProcessed: vi.fn(),
	deleteMemoryEmbedding: vi.fn(),
}));

vi.mock("../memory-relations.js", () => ({
	createMemoryRelation: vi.fn(),
}));

vi.mock("../sync-scheduler.js", () => ({
	queueMemorySync: vi.fn(),
}));

vi.mock("../sm-client.js", () => ({
	querySmMemories: vi.fn().mockResolvedValue([]),
}));

// memory-store mock used by swarm-dispatch tests — only overrides the two
// functions touched at the dispatch boundary; the real implementations are
// used everywhere else (including the consolidation-action tests).
vi.mock("../memory-store.js", async (importOriginal) => {
	const original = await importOriginal<typeof import("../memory-store.js")>();
	return {
		...original,
		getActiveMemoriesRanked: vi.fn().mockReturnValue([]),
		formatMemoriesForPrompt: vi.fn().mockReturnValue(""),
	};
});

vi.mock("../uok/agent-swarm.js", () => ({
	AgentSwarm: {
		load: vi.fn().mockReturnValue({
			getAll: vi.fn().mockReturnValue([{ identity: { name: "worker-1" } }]),
			route: vi.fn().mockReturnValue({ identity: { name: "worker-1" } }),
			getTopology: vi.fn(),
		}),
	},
}));

vi.mock("../uok/message-bus.js", () => {
	const MockMessageBus = vi.fn().mockImplementation(function () {
		this.send = vi.fn().mockReturnValue("msg-abc");
	});
	return { MessageBus: MockMessageBus };
});

vi.mock("../uok/swarm-roles.js", () => ({
	createDefaultSwarm: vi.fn().mockResolvedValue({
		swarm: {
			getAll: vi.fn().mockReturnValue([{ identity: { name: "worker-1" } }]),
			route: vi.fn().mockReturnValue({ identity: { name: "worker-1" } }),
		},
	}),
}));

// ─── Imports (after mocks) ────────────────────────────────────────────────────

import * as memoryStore from "../memory-store.js";
import {
	applyConsolidationActions,
	applyMemoryActions,
} from "../memory-store.js";
import * as sfDb from "../sf-db.js";
import { SwarmDispatchLayer } from "../uok/swarm-dispatch.js";

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeAddAction(overrides: Record<string, unknown> = {}) {
	return {
		action: "add",
		category: "convention",
		content: "Use vitest for all unit tests.",
		confidence: 0.8,
		...overrides,
	};
}

function makePruneAction(id = "MEM001") {
	return { action: "prune", id };
}

/** Shared DB adapter that simulates a successful memory insert. */
function makeMockAdapter() {
	return {
		prepare: vi.fn().mockReturnValue({
			get: vi.fn().mockReturnValue({ seq: 1 }),
			all: vi.fn().mockReturnValue([]),
			run: vi.fn(),
		}),
	};
}

const testEnvelope = {
	unitId: "T001",
	unitType: "task" as const,
	workMode: "build" as const,
	payload: { goal: "write tests" },
	priority: 5,
	scope: "project",
};

// ─── applyConsolidationActions ────────────────────────────────────────────────

describe("applyConsolidationActions", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		(sfDb.isDbAvailable as ReturnType<typeof vi.fn>).mockReturnValue(true);
		(sfDb.transaction as ReturnType<typeof vi.fn>).mockImplementation(
			(fn: () => void) => fn(),
		);
		(sfDb._getAdapter as ReturnType<typeof vi.fn>).mockReturnValue(
			makeMockAdapter(),
		);
	});

	it('accepts an "add" action without throwing', () => {
		expect(() =>
			applyConsolidationActions([makeAddAction()], "task", "T001"),
		).not.toThrow();
	});

	it('accepts a "prune" action without throwing', () => {
		expect(() =>
			applyConsolidationActions([makePruneAction("MEM007")], "task", "T001"),
		).not.toThrow();
	});

	it('accepts a mixed batch of "add" and "prune" without throwing', () => {
		expect(() =>
			applyConsolidationActions(
				[makeAddAction(), makePruneAction("MEM003")],
				"slice",
				"S01",
			),
		).not.toThrow();
	});

	it('rejects an "update" action with a descriptive error', () => {
		expect(() =>
			applyConsolidationActions(
				[{ action: "update", id: "MEM001", content: "new text" }],
				"task",
				"T001",
			),
		).toThrow(/applyConsolidationActions.*update.*not allowed/i);
	});

	it('rejects an "UPDATE" (uppercase) action with a descriptive error', () => {
		expect(() =>
			applyConsolidationActions(
				[{ action: "UPDATE", id: "MEM001", content: "new text" }],
				"task",
				"T001",
			),
		).toThrow(/applyConsolidationActions.*UPDATE.*not allowed/i);
	});

	it('rejects a "supersede" action with a descriptive error', () => {
		expect(() =>
			applyConsolidationActions(
				[{ action: "supersede", id: "MEM001", superseded_by: "MEM002" }],
				"task",
				"T001",
			),
		).toThrow(/applyConsolidationActions.*supersede.*not allowed/i);
	});

	it('rejects a "SUPERSEDE" (uppercase) action with a descriptive error', () => {
		expect(() =>
			applyConsolidationActions(
				[{ action: "SUPERSEDE", id: "MEM001", superseded_by: "MEM002" }],
				"task",
				"T001",
			),
		).toThrow(/applyConsolidationActions.*SUPERSEDE.*not allowed/i);
	});

	it('rejects a "reinforce" action with a descriptive error', () => {
		expect(() =>
			applyConsolidationActions(
				[{ action: "reinforce", id: "MEM001" }],
				"task",
				"T001",
			),
		).toThrow(/applyConsolidationActions.*reinforce.*not allowed/i);
	});

	it("error message instructs the model to prune-then-add instead of updating", () => {
		let errorMsg = "";
		try {
			applyConsolidationActions(
				[{ action: "update", id: "MEM001", content: "revised" }],
				"task",
				"T001",
			);
		} catch (err) {
			errorMsg = (err as Error).message;
		}
		expect(errorMsg).toMatch(/prune.*add/i);
	});

	it("does nothing and does not throw for an empty actions array", () => {
		expect(() => applyConsolidationActions([], "task", "T001")).not.toThrow();
	});

	it("does nothing and does not throw when DB is unavailable", () => {
		(sfDb.isDbAvailable as ReturnType<typeof vi.fn>).mockReturnValue(false);
		expect(() =>
			applyConsolidationActions([makeAddAction()], "task", "T001"),
		).not.toThrow();
	});
});

// ─── applyMemoryActions (full surface, unaffected) ────────────────────────────

describe("applyMemoryActions — full action surface remains intact", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		(sfDb.isDbAvailable as ReturnType<typeof vi.fn>).mockReturnValue(true);
		(sfDb.transaction as ReturnType<typeof vi.fn>).mockImplementation(
			(fn: () => void) => fn(),
		);
		(sfDb._getAdapter as ReturnType<typeof vi.fn>).mockReturnValue(
			makeMockAdapter(),
		);
	});

	it('accepts "CREATE" without throwing', () => {
		expect(() =>
			applyMemoryActions(
				[
					{
						action: "CREATE",
						category: "convention",
						content: "Use npm only.",
						confidence: 0.8,
					},
				],
				"task",
				"T002",
			),
		).not.toThrow();
	});

	it('accepts "UPDATE" without throwing', () => {
		expect(() =>
			applyMemoryActions(
				[{ action: "UPDATE", id: "MEM001", content: "revised text" }],
				"task",
				"T002",
			),
		).not.toThrow();
	});

	it('accepts "REINFORCE" without throwing', () => {
		expect(() =>
			applyMemoryActions(
				[{ action: "REINFORCE", id: "MEM001" }],
				"task",
				"T002",
			),
		).not.toThrow();
	});

	it('accepts "SUPERSEDE" without throwing', () => {
		expect(() =>
			applyMemoryActions(
				[{ action: "SUPERSEDE", id: "MEM001", superseded_by: "MEM002" }],
				"task",
				"T002",
			),
		).not.toThrow();
	});
});

// ─── swarm-dispatch memory inheritance ───────────────────────────────────────

describe("swarm-dispatch — memory snapshot inheritance", () => {
	const threeMemories = [
		{
			seq: 1,
			id: "MEM001",
			category: "convention",
			content: "Use vitest.",
			confidence: 0.9,
			hit_count: 2,
			tags: [],
			source_unit_type: null,
			source_unit_id: null,
			created_at: "2026-01-01",
			updated_at: "2026-01-01",
			superseded_by: null,
		},
		{
			seq: 2,
			id: "MEM002",
			category: "architecture",
			content: "SF uses SQLite.",
			confidence: 0.85,
			hit_count: 1,
			tags: [],
			source_unit_type: null,
			source_unit_id: null,
			created_at: "2026-01-01",
			updated_at: "2026-01-01",
			superseded_by: null,
		},
		{
			seq: 3,
			id: "MEM003",
			category: "gotcha",
			content: "Never use bun.",
			confidence: 0.95,
			hit_count: 3,
			tags: [],
			source_unit_type: null,
			source_unit_id: null,
			created_at: "2026-01-01",
			updated_at: "2026-01-01",
			superseded_by: null,
		},
	];

	beforeEach(() => {
		vi.clearAllMocks();
		// Reset to empty by default; individual tests override as needed.
		(
			memoryStore.getActiveMemoriesRanked as ReturnType<typeof vi.fn>
		).mockReturnValue([]);
		(
			memoryStore.formatMemoriesForPrompt as ReturnType<typeof vi.fn>
		).mockReturnValue("");
	});

	it("includes memoryContext in dispatch result when parent has 3 active memories", async () => {
		(
			memoryStore.getActiveMemoriesRanked as ReturnType<typeof vi.fn>
		).mockReturnValue(threeMemories);
		(
			memoryStore.formatMemoriesForPrompt as ReturnType<typeof vi.fn>
		).mockReturnValue(
			"## Project Memory (auto-learned)\n- Use vitest.\n- SF uses SQLite.\n- Never use bun.",
		);

		const layer = new SwarmDispatchLayer("/tmp/fake-project", {
			autoCreate: false,
		});
		const result = await layer.dispatch(testEnvelope);

		// Result should carry the memoryContext field
		expect(result.memoryContext).toBeDefined();
		expect(result.memoryContext).toContain("Project Memory");

		// getActiveMemoriesRanked should have been called with limit 30
		expect(memoryStore.getActiveMemoriesRanked).toHaveBeenCalledWith(30);

		// formatMemoriesForPrompt should have received the three memories
		expect(memoryStore.formatMemoriesForPrompt).toHaveBeenCalledWith(
			threeMemories,
			2000,
			false,
		);
	});

	it("omits memoryContext from result when no memories exist", async () => {
		// Already set to [] by beforeEach
		const layer = new SwarmDispatchLayer("/tmp/fake-project-2", {
			autoCreate: false,
		});
		const result = await layer.dispatch(testEnvelope);

		expect(result.memoryContext).toBeUndefined();
	});

	it("dispatch succeeds even when getActiveMemoriesRanked throws (fail-open)", async () => {
		(
			memoryStore.getActiveMemoriesRanked as ReturnType<typeof vi.fn>
		).mockImplementation(() => {
			throw new Error("DB offline");
		});

		const layer = new SwarmDispatchLayer("/tmp/fake-project-3", {
			autoCreate: false,
		});

		// Should not throw — memory context is fail-open
		const result = await layer.dispatch(testEnvelope);
		expect(result.messageId).toBe("msg-abc");
		expect(result.memoryContext).toBeUndefined();
	});
});
