/**
 * SM (Singularity Memory) Client Tests
 *
 * Validates that SM client gracefully handles:
 * - SM unavailable (offline)
 * - SM available (connected)
 * - Fire-and-forget sync
 * - Timeout behavior
 * - Environment variable controls
 */

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

describe("SM Client", () => {
	let originalEnv: Record<string, string | undefined>;
	let originalFetch: typeof globalThis.fetch;

	beforeEach(() => {
		originalEnv = { ...process.env };
		originalFetch = globalThis.fetch;
	});

	afterEach(() => {
		globalThis.fetch = originalFetch;
		vi.restoreAllMocks();
		// Restore env
		for (const key of Object.keys(process.env)) {
			if (!(key in originalEnv)) {
				delete process.env[key];
			}
		}
		for (const [key, value] of Object.entries(originalEnv)) {
			process.env[key] = value;
		}
	});

	describe("initializeSmClient", () => {
		it("when_sm_enabled_false_returns_disconnected", async () => {
			process.env.SM_ENABLED = "false";

			// Dynamically import to pick up env var
			const { initializeSmClient } = await import("../sm-client.js");
			const result = await initializeSmClient();

			expect(result).toEqual(
				expect.objectContaining({
					connected: false,
				}),
			);
		});

		it("when_sm_unavailable_returns_disconnected_with_reason", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://localhost:19999"; // Unlikely port

			const { initializeSmClient } = await import("../sm-client.js");
			const result = await initializeSmClient();

			expect(result).toEqual(
				expect.objectContaining({
					connected: false,
					reason: expect.any(String),
				}),
			);
		});

		it("when_sm_addr_invalid_returns_disconnected", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "invalid://not-a-url";

			const { initializeSmClient } = await import("../sm-client.js");
			const result = await initializeSmClient();

			expect(result).toEqual(
				expect.objectContaining({
					connected: false,
				}),
			);
		});

		it("stores_client_state_in_global", async () => {
			process.env.SM_ENABLED = "false";

			const { initializeSmClient } = await import("../sm-client.js");
			await initializeSmClient();

			// Client should store state in globalThis (internal implementation detail)
			// Just verify it doesn't throw
			expect(true).toBe(true);
		});
	});

	describe("syncMemoryToSm", () => {
		it("when_sm_disabled_does_not_throw", async () => {
			process.env.SM_ENABLED = "false";

			const { syncMemoryToSm } = await import("../sm-client.js");

			// Should fire-and-forget without throwing
			syncMemoryToSm("test-memory", {
				type: "note",
				content: "test",
				projectId: "test",
			});

			// Wait for async operations to settle
			await new Promise((resolve) => setTimeout(resolve, 50));

			expect(true).toBe(true);
		});

		it("when_sm_available_queues_sync", async () => {
			process.env.SM_ENABLED = "false"; // Disable for this test (no real SM available)

			const { syncMemoryToSm } = await import("../sm-client.js");

			// Should queue sync without throwing, even if SM unavailable
			syncMemoryToSm("test-memory", {
				type: "note",
				content: "test",
				projectId: "test",
			});

			await new Promise((resolve) => setTimeout(resolve, 50));

			expect(true).toBe(true);
		});

		it("accepts_memory_object_with_required_fields", async () => {
			process.env.SM_ENABLED = "false";

			const { syncMemoryToSm } = await import("../sm-client.js");

			const memory = {
				type: "note",
				content: "Important observation",
				projectId: "project-123",
			};

			// Should not throw
			syncMemoryToSm("mem-1", memory);

			await new Promise((resolve) => setTimeout(resolve, 50));

			expect(true).toBe(true);
		});
	});

	describe("querySmMemories", () => {
		it("when_sm_disabled_returns_empty_array", async () => {
			process.env.SM_ENABLED = "false";

			const { querySmMemories } = await import("../sm-client.js");

			const result = await querySmMemories("test", {});

			expect(Array.isArray(result)).toBe(true);
			expect(result).toEqual([]);
		});

		it("when_sm_unavailable_returns_empty_array", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://localhost:19999";

			const { querySmMemories } = await import("../sm-client.js");

			const result = await querySmMemories("test", { maxResults: 5 });

			expect(Array.isArray(result)).toBe(true);
			expect(result).toEqual([]);
		});

		it("does_not_throw_on_query_failure", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://invalid-host:9999";

			const { querySmMemories } = await import("../sm-client.js");

			// Should gracefully return empty array, not throw
			const result = await querySmMemories("search", {});

			expect(Array.isArray(result)).toBe(true);
			expect(result).toEqual([]);
		});

		it("accepts_optional_filters", async () => {
			process.env.SM_ENABLED = "false";

			const { querySmMemories } = await import("../sm-client.js");

			// Should accept various filter options
			const result = await querySmMemories("cross-project", {
				projectId: "proj-123",
				maxResults: 10,
				types: ["note", "decision"],
			});

			expect(Array.isArray(result)).toBe(true);
		});

		it("when_sm_enabled_without_tenant_scope_returns_empty_without_fetch", async () => {
			process.env.SM_ENABLED = "true";
			delete process.env.SM_TENANT_ID;
			delete process.env.SINGULARITY_MEMORY_TENANT_ID;
			const fetchMock = vi.fn();
			globalThis.fetch = fetchMock;

			const { querySmMemories } = await import("../sm-client.js");

			const result = await querySmMemories("cross-project", {
				smConnected: true,
				limit: 3,
			});

			expect(result).toEqual([]);
			expect(fetchMock).not.toHaveBeenCalled();
		});

		it("when_tenant_scope_configured_sends_tenant_and_project_filters", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SM_TENANT_ID = "tenant-a";
			process.env.SM_PROJECT_ID = "project-b";
			const fetchMock = vi.fn().mockResolvedValue({
				ok: true,
				json: async () => ({ memories: [{ id: "mem-1", content: "hit" }] }),
			});
			globalThis.fetch = fetchMock;

			const { querySmMemories } = await import("../sm-client.js");

			const result = await querySmMemories("cross-project", {
				smConnected: true,
				limit: 3,
			});

			expect(result).toEqual([{ id: "mem-1", content: "hit" }]);
			expect(fetchMock).toHaveBeenCalledTimes(1);
			const body = JSON.parse(fetchMock.mock.calls[0][1].body);
			// Both tenant (repoIdentity key) and tenantId (legacy SM API compat) are sent
			expect(body).toMatchObject({
				query: "cross-project",
				limit: 3,
				tenant: "tenant-a",
				tenantId: "tenant-a",
				projectId: "project-b",
			});
		});
	});

	describe("getSmStatus", () => {
		it("when_sm_disabled_returns_disconnected_status", async () => {
			delete process.env.SM_ENABLED; // SM is opt-in — absence means disabled

			const { getSmStatus } = await import("../sm-client.js");

			const status = getSmStatus();

			expect(status).toEqual({
				connected: false,
				reason: "disabled (set SM_ENABLED=true to enable)",
			});
		});

		it("when_sm_enabled_returns_configured_status", async () => {
			process.env.SM_ENABLED = "true";
			delete process.env.SINGULARITY_MEMORY_ADDR; // Use default

			const { getSmStatus } = await import("../sm-client.js");

			const status = getSmStatus();

			expect(status).toEqual(
				expect.objectContaining({
					configured: true,
					tenantConfigured: false,
					status: "unknown", // Actual connection status requires initializeSmClient
				}),
			);
		});

		it("includes_address_when_configured", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://custom-host:9999";

			const { getSmStatus } = await import("../sm-client.js");

			const status = getSmStatus();

			expect(status.addr).toBe("http://custom-host:9999");
		});
	});

	describe("Environment Variables", () => {
		it("respects_sm_enabled_flag", async () => {
			process.env.SM_ENABLED = "false";

			const { initializeSmClient } = await import("../sm-client.js");

			const result = await initializeSmClient();

			// Should be disconnected when disabled
			expect(result.connected).toBe(false);
		});

		it("uses_custom_singularity_memory_addr", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://custom-host:9999";

			const { getSmStatus } = await import("../sm-client.js");

			const status = getSmStatus();

			// Should reflect the custom address
			expect(status.addr).toBe("http://custom-host:9999");
		});

		it("defaults_to_localhost_8080", async () => {
			// Clear custom addr
			delete process.env.SINGULARITY_MEMORY_ADDR;
			process.env.SM_ENABLED = "true";

			const { getSmStatus } = await import("../sm-client.js");

			const status = getSmStatus();

			// Should default to localhost:8080
			expect(status.addr).toBe("http://localhost:8080");
		});
	});

	describe("Fire-and-Forget Behavior", () => {
		it("sync_does_not_await_network", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://localhost:19999";

			const { syncMemoryToSm } = await import("../sm-client.js");

			// Should return immediately, not wait for network timeout
			const start = Date.now();

			syncMemoryToSm("test", {
				type: "note",
				content: "test",
				projectId: "test",
			});

			const elapsed = Date.now() - start;

			// Should be nearly instant (< 10ms), not waiting for 5s timeout
			expect(elapsed).toBeLessThan(100);
		});

		it("multiple_syncs_queue_independently", async () => {
			process.env.SM_ENABLED = "false";

			const { syncMemoryToSm } = await import("../sm-client.js");

			// Fire multiple syncs
			for (let i = 0; i < 5; i++) {
				syncMemoryToSm(`mem-${i}`, {
					type: "note",
					content: `Note ${i}`,
					projectId: "test",
				});
			}

			// Should all be queued without blocking
			expect(true).toBe(true);
		});
	});

	describe("Graceful Degradation", () => {
		it("sf_continues_offline_when_sm_unavailable", async () => {
			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://localhost:19999";

			const { querySmMemories } = await import("../sm-client.js");

			// SF should get empty results, not crash
			const result = await querySmMemories("test", {});

			expect(result).toEqual([]);
		});

		it("local_memory_unaffected_by_sm_failure", async () => {
			// This is a contract test — ensure SM failures don't break local memory
			// Actual local memory tests are in memory-store.test.ts
			// This just validates that SM client doesn't throw

			process.env.SM_ENABLED = "true";
			process.env.SINGULARITY_MEMORY_ADDR = "http://invalid:9999";

			const { syncMemoryToSm, querySmMemories } = await import(
				"../sm-client.js"
			);

			// Both operations should complete without throwing
			syncMemoryToSm("test", {
				type: "note",
				content: "test",
				projectId: "test",
			});

			const result = await querySmMemories("test", {});

			expect(Array.isArray(result)).toBe(true);
		});
	});
});
