import { describe, it, expect, afterEach } from "bun:test";
import { Effect, Layer } from "effect";
import {
  MemoryService,
  MemorySearchService,
  MemorySearchServiceLive,
  MemoryDatabase,
  MemoryDatabaseLive,
} from "../src/index.js";
import { createMemoryLayer } from "../src/runtime.js";
import type { SemanticEntry, MemoryId, MemoryLLM } from "../src/types.js";
import * as fs from "node:fs";
import * as path from "node:path";

const TEST_DB_DIR = "/tmp/test-embedding-bridge-db";
const TEST_DB = path.join(TEST_DB_DIR, "embedding-bridge.db");

const makeEntry = (
  id: string,
  content: string,
  importance = 0.5,
): SemanticEntry => ({
  id: id as MemoryId,
  agentId: "test-agent",
  content,
  summary: content.slice(0, 50),
  importance,
  verified: false,
  tags: ["test"],
  createdAt: new Date(),
  updatedAt: new Date(),
  accessCount: 0,
  lastAccessedAt: new Date(),
});

/** Mock MemoryLLM that returns deterministic embeddings based on content length. */
const createMockMemoryLLM = (): MemoryLLM & { embedCallCount: number } => {
  const mock = {
    embedCallCount: 0,
    complete: (req: { messages: readonly { role: string; content: string }[] }) =>
      Effect.succeed({
        content: "mock response",
        usage: { totalTokens: 10 },
      }),
    embed: (texts: readonly string[], _model?: string) => {
      mock.embedCallCount++;
      // Return deterministic embeddings: normalize content length into a 3D vector
      const embeddings = texts.map((text) => {
        const len = text.length;
        const norm = Math.sqrt(len * len + 1);
        return [len / norm, 1 / norm, 0] as readonly number[];
      });
      return Effect.succeed(embeddings);
    },
  };
  return mock;
};

describe("Embedding Bridge — MemoryLLM.embed integration", () => {
  afterEach(() => {
    try {
      fs.unlinkSync(TEST_DB);
      fs.unlinkSync(TEST_DB + "-wal");
      fs.unlinkSync(TEST_DB + "-shm");
    } catch {
      /* ignore */
    }
    try {
      fs.rmSync(TEST_DB_DIR, { recursive: true });
    } catch {
      /* ignore */
    }
  });

  it("should auto-generate embeddings when storing semantic entries", async () => {
    const mockLLM = createMockMemoryLLM();
    const layer = createMemoryLayer(
      "1",
      { agentId: "test-agent", dbPath: TEST_DB },
      mockLLM,
    );

    const result = await Effect.runPromise(
      Effect.scoped(
        Effect.gen(function* () {
          const svc = yield* MemoryService;
          const entry = makeEntry("emb-1", "TypeScript is great for large codebases");
          yield* svc.storeSemantic(entry);

          // Verify the embedding was generated by checking the DB directly
          const db = yield* MemoryDatabase;
          const rows = yield* db.query<{ embedding: ArrayBuffer | null }>(
            `SELECT embedding FROM semantic_memory WHERE id = ?`,
            ["emb-1"],
          );
          return rows[0]?.embedding;
        }).pipe(Effect.provide(layer)),
      ),
    );

    expect(result).not.toBeNull();
    expect(result).toBeInstanceOf(Uint8Array);
    // Verify embed was called
    expect(mockLLM.embedCallCount).toBe(1);
  });

  it("should not overwrite existing embeddings", async () => {
    const mockLLM = createMockMemoryLLM();
    const layer = createMemoryLayer(
      "1",
      { agentId: "test-agent", dbPath: TEST_DB },
      mockLLM,
    );

    await Effect.runPromise(
      Effect.scoped(
        Effect.gen(function* () {
          const svc = yield* MemoryService;
          // Store entry WITH an embedding already set
          const existingEmbedding = [0.1, 0.2, 0.3];
          const entry: SemanticEntry = {
            ...makeEntry("emb-2", "Pre-embedded content"),
            embedding: existingEmbedding,
          };
          yield* svc.storeSemantic(entry);

          // Verify the original embedding was preserved (embed should NOT be called)
          const db = yield* MemoryDatabase;
          const rows = yield* db.query<{ embedding: ArrayBuffer | null }>(
            `SELECT embedding FROM semantic_memory WHERE id = ?`,
            ["emb-2"],
          );
          const stored = rows[0]?.embedding;
          expect(stored).not.toBeNull();
          const storedArray = Array.from(
            new Float32Array(
              stored instanceof ArrayBuffer
                ? stored
                : (stored as unknown as Uint8Array).buffer,
            ),
          );
          // Should be the original embedding, not the mock-generated one
          expect(storedArray.length).toBe(3);
          expect(storedArray[0]).toBeCloseTo(0.1, 4);
        }).pipe(Effect.provide(layer)),
      ),
    );

    // embed() should NOT have been called since entry already had an embedding
    expect(mockLLM.embedCallCount).toBe(0);
  });

  it("should work without MemoryLLM (backward compat)", async () => {
    // No memoryLLM passed — should store without embedding
    const layer = createMemoryLayer("1", {
      agentId: "test-agent",
      dbPath: TEST_DB,
    });

    const result = await Effect.runPromise(
      Effect.scoped(
        Effect.gen(function* () {
          const svc = yield* MemoryService;
          const entry = makeEntry("emb-3", "No LLM available");
          yield* svc.storeSemantic(entry);

          const db = yield* MemoryDatabase;
          const rows = yield* db.query<{ embedding: ArrayBuffer | null }>(
            `SELECT embedding FROM semantic_memory WHERE id = ?`,
            ["emb-3"],
          );
          return rows[0]?.embedding;
        }).pipe(Effect.provide(layer)),
      ),
    );

    // No embedding should be stored
    expect(result).toBeNull();
  });

  it("should enable vector search when embeddings are auto-generated", async () => {
    const mockLLM = createMockMemoryLLM();
    const layer = createMemoryLayer(
      "1",
      { agentId: "test-agent", dbPath: TEST_DB },
      mockLLM,
    );

    const results = await Effect.runPromise(
      Effect.scoped(
        Effect.gen(function* () {
          const svc = yield* MemoryService;
          const search = yield* MemorySearchService;

          // Store entries — embeddings will be auto-generated
          yield* svc.storeSemantic(
            makeEntry("vec-1", "Short text", 0.8),
          );
          yield* svc.storeSemantic(
            makeEntry("vec-2", "A much longer piece of text that has different embedding characteristics", 0.6),
          );
          yield* svc.storeSemantic(
            makeEntry("vec-3", "Tiny", 0.7),
          );

          // Search using a query embedding similar to "Short text" (length 10)
          // Our mock generates [len/norm, 1/norm, 0] so short text ~= [0.995, 0.099, 0]
          const queryEmbedding = [0.995, 0.099, 0];
          return yield* search.searchVector(queryEmbedding, "test-agent", 2);
        }).pipe(Effect.provide(layer)),
      ),
    );

    // Should return results (previously would always return 0 with no embeddings)
    expect(results.length).toBe(2);
    // The most similar entry should be "Short text" (closest embedding to query)
    expect(results[0]!.id).toBe("vec-1");
  });

  it("should gracefully handle embed() failure", async () => {
    const failingLLM: MemoryLLM = {
      complete: () =>
        Effect.succeed({ content: "ok", usage: { totalTokens: 1 } }),
      embed: () => Effect.fail(new Error("Embedding service unavailable")),
    };

    const layer = createMemoryLayer(
      "1",
      { agentId: "test-agent", dbPath: TEST_DB },
      failingLLM,
    );

    // Should still store the entry, just without an embedding
    const result = await Effect.runPromise(
      Effect.scoped(
        Effect.gen(function* () {
          const svc = yield* MemoryService;
          const entry = makeEntry("emb-fail", "This should still store");
          const id = yield* svc.storeSemantic(entry);

          const db = yield* MemoryDatabase;
          const rows = yield* db.query<{ content: string; embedding: ArrayBuffer | null }>(
            `SELECT content, embedding FROM semantic_memory WHERE id = ?`,
            ["emb-fail"],
          );
          return { id, content: rows[0]?.content, embedding: rows[0]?.embedding };
        }).pipe(Effect.provide(layer)),
      ),
    );

    expect(result.id).toBe("emb-fail");
    expect(result.content).toBe("This should still store");
    // Embedding should be null because embed() failed
    expect(result.embedding).toBeNull();
  });
});
