import { describe, it, expect } from "bun:test";
import { Elysia } from "elysia";
import { Database } from "bun:sqlite";
import { applySchema } from "../db/schema.js";
import { CortexStoreServiceLive } from "../services/store-service.js";
import { agentsRouter } from "../api/agents.js";
import { toolsRouter } from "../api/tools.js";
import { skillsRouter } from "../api/skills.js";
import { mcpServersRouter } from "../api/mcp-servers.js";
import { GatewayProcessManager } from "../services/gateway-process-manager.js";
import { CortexEventBridgeLive, CortexEventBridge } from "../services/event-bridge.js";
import { CortexIngestServiceLive } from "../services/ingest-service.js";
import { Effect, Layer } from "effect";

function makeTestGateway(db: Database) {
  const bridgeService = Effect.runSync(CortexEventBridge.pipe(Effect.provide(CortexEventBridgeLive)));
  const bridgeLayer = Layer.succeed(CortexEventBridge, bridgeService);
  const ingestLayer = CortexIngestServiceLive(db).pipe(Layer.provide(bridgeLayer)) as Layer.Layer<any>;
  return new GatewayProcessManager(db, ingestLayer);
}

describe("GET /api/agents", () => {
  it("returns empty array when no agents", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const gateway = makeTestGateway(db);
    const app = new Elysia().use(agentsRouter(db, gateway));
    const res = await app.handle(new Request("http://localhost/api/agents"));
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual([]);
    gateway.destroy();
  });
});

describe("GET /api/tools", () => {
  it("returns JSON array", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(toolsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(new Request("http://localhost/api/tools"));
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(Array.isArray(body)).toBe(true);
  });
});

describe("GET /api/tools/catalog", () => {
  it("returns unified catalog with built-in entries", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(toolsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(new Request("http://localhost/api/tools/catalog"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as Array<{ id: string; kind: string; name: string }>;
    expect(Array.isArray(body)).toBe(true);
    const web = body.find((t) => t.name === "web-search");
    expect(web).toBeDefined();
    expect(web!.kind).toBe("built-in");
    expect(web!.id.startsWith("bi:")).toBe(true);
  });
});

describe("POST /api/tools/lab-custom + invoke", () => {
  it("echoes args for lab custom tool", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(toolsRouter(CortexStoreServiceLive(db), db));
    const create = await app.handle(
      new Request("http://localhost/api/tools/lab-custom", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: "echo-demo",
          description: "test",
          parameters: [{ name: "msg", type: "string", required: true, description: "message" }],
        }),
      }),
    );
    expect(create.status).toBe(200);
    const { toolId } = (await create.json()) as { toolId: string };
    const inv = await app.handle(
      new Request("http://localhost/api/tools/invoke", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          toolId: `lab:${toolId}`,
          arguments: { msg: "hi" },
        }),
      }),
    );
    expect(inv.status).toBe(200);
    const out = (await inv.json()) as { result?: { received?: { msg?: string } } };
    expect(out.result?.received?.msg).toBe("hi");
  });
});

describe("GET /api/skills", () => {
  it("returns JSON array", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(new Request("http://localhost/api/skills"));
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(Array.isArray(body)).toBe(true);
  });

  it("GET /api/skills/discover returns { paths: string[] }", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(new Request("http://localhost/api/skills/discover"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { paths: string[] };
    expect(Array.isArray(body.paths)).toBe(true);
  });

  it("GET /api/skills/files returns { skills: array }", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(new Request("http://localhost/api/skills/files"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { skills: unknown[] };
    expect(Array.isArray(body.skills)).toBe(true);
  });

  it("GET /api/skills/file parses discovered SKILL.md when any exist", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const listRes = await app.handle(new Request("http://localhost/api/skills/files"));
    const { skills } = (await listRes.json()) as { skills: { relPath: string }[] };
    if (skills.length === 0) return;

    const res = await app.handle(
      new Request(
        `http://localhost/api/skills/file?path=${encodeURIComponent(skills[0]!.relPath)}`,
      ),
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { name: string; instructions: string };
    expect(typeof body.name).toBe("string");
    expect(body.name.length).toBeGreaterThan(0);
    expect(typeof body.instructions).toBe("string");
  });

  it("GET /api/skills/sqlite/:id returns parsed row", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    db.prepare(
      "INSERT INTO skills (name, description, content) VALUES (?, ?, ?)",
    ).run("row-skill", "Row desc", "---\nname: yaml-name\ndescription: Yaml desc\n---\n\n# Body\n");
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(new Request("http://localhost/api/skills/sqlite/1"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { name: string; instructions: string };
    expect(body.name).toBe("yaml-name");
    expect(body.instructions).toContain("# Body");
  });

  it("POST /api/skills creates a new skill", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(
      new Request("http://localhost/api/skills", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: "test-skill",
          instructions: "Do something useful",
          description: "A test skill",
          tags: ["test", "demo"],
        }),
      }),
    );
    expect(res.status).toBe(201);
    const body = (await res.json()) as { id: number; name: string; description: string };
    expect(body.id).toBeGreaterThan(0);
    expect(body.name).toBe("test-skill");
    expect(body.description).toBe("A test skill");

    // Verify it's in the database
    const all = await app.handle(new Request("http://localhost/api/skills"));
    const list = (await all.json()) as Array<{ id: number; name: string }>;
    expect(list.some((s) => s.id === body.id)).toBe(true);
  });

  it("POST /api/skills rejects missing name", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(
      new Request("http://localhost/api/skills", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          instructions: "Do something",
        }),
      }),
    );
    expect(res.status).toBe(422);
  });

  it("POST /api/skills rejects missing instructions", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(
      new Request("http://localhost/api/skills", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: "test-skill",
        }),
      }),
    );
    expect(res.status).toBe(422);
  });

  it("DELETE /api/skills/:id deletes a skill", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    db.prepare(
      "INSERT INTO skills (name, description, content) VALUES (?, ?, ?)",
    ).run("skill-to-delete", "Desc", "Content");
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));

    // Verify it exists
    const before = await app.handle(new Request("http://localhost/api/skills"));
    const listBefore = (await before.json()) as Array<{ id: number }>;
    const skillId = listBefore[0]?.id;
    expect(skillId).toBeDefined();

    // Delete it
    const res = await app.handle(
      new Request(`http://localhost/api/skills/${skillId}`, { method: "DELETE" }),
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { ok?: boolean };
    expect(body.ok).toBe(true);

    // Verify it's gone
    const after = await app.handle(new Request("http://localhost/api/skills"));
    const listAfter = (await after.json()) as Array<{ id: number }>;
    expect(listAfter.every((s) => s.id !== skillId)).toBe(true);
  });

  it("DELETE /api/skills/:id returns 404 for non-existent skill", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(skillsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(
      new Request("http://localhost/api/skills/999", { method: "DELETE" }),
    );
    expect(res.status).toBe(404);
    const body = (await res.json()) as { error?: string };
    expect(body.error).toContain("not found");
  });
});

describe("POST /api/mcp-servers/import-json", () => {
  it("rejects invalid JSON", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(mcpServersRouter(db));
    const res = await app.handle(
      new Request("http://localhost/api/mcp-servers/import-json", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ json: "not-json" }),
      }),
    );
    expect(res.status).toBe(400);
  });

  it("rejects empty expansion", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(mcpServersRouter(db));
    const res = await app.handle(
      new Request("http://localhost/api/mcp-servers/import-json", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ json: "{}" }),
      }),
    );
    expect(res.status).toBe(400);
  });

  it("imports Cursor-style mcpServers map", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(mcpServersRouter(db));
    const payload = {
      mcpServers: {
        alpha: { command: "echo", args: ["mcp"] },
      },
    };
    const res = await app.handle(
      new Request("http://localhost/api/mcp-servers/import-json", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ json: JSON.stringify(payload) }),
      }),
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { ok: boolean; count: number; created: { name: string }[] };
    expect(body.ok).toBe(true);
    expect(body.count).toBe(1);
    expect(body.created[0]!.name).toBe("alpha");

    const list = await app.handle(new Request("http://localhost/api/mcp-servers"));
    expect(list.status).toBe(200);
    const rows = (await list.json()) as { name: string }[];
    expect(rows.some((r) => r.name === "alpha")).toBe(true);
  });

  it("upserts on duplicate name — re-importing same config updates instead of failing", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(mcpServersRouter(db));

    // First import: two distinct servers
    const first = await app.handle(
      new Request("http://localhost/api/mcp-servers/import-json", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          json: JSON.stringify([
            { name: "one", command: "x" },
            { name: "two", command: "y" },
          ]),
        }),
      }),
    );
    expect(first.status).toBe(200);
    const firstBody = (await first.json()) as { count: number; created: { serverId: string; name: string }[] };
    expect(firstBody.count).toBe(2);
    const originalServerId = firstBody.created.find((c) => c.name === "one")!.serverId;

    // Second import: same name "one", updated config — should succeed as upsert, not 400
    const second = await app.handle(
      new Request("http://localhost/api/mcp-servers/import-json", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          json: JSON.stringify([{ name: "one", command: "x-updated" }]),
        }),
      }),
    );
    expect(second.status).toBe(200);
    const secondBody = (await second.json()) as { count: number; created: { serverId: string; name: string }[] };
    expect(secondBody.count).toBe(1);
    // server_id must be preserved (agent configs referencing it stay valid)
    expect(secondBody.created[0]!.serverId).toBe(originalServerId);

    const list = await app.handle(new Request("http://localhost/api/mcp-servers"));
    const rows = (await list.json()) as { name: string; config: { command?: string } }[];
    expect(rows.length).toBe(2);
    const updated = rows.find((r) => r.name === "one");
    expect(updated?.config.command).toBe("x-updated");
  });
});

describe("POST /api/tools/mcp-import-json", () => {
  it("imports MCP configs (same logic as /api/mcp-servers/import-json)", async () => {
    const db = new Database(":memory:");
    applySchema(db);
    const app = new Elysia().use(toolsRouter(CortexStoreServiceLive(db), db));
    const res = await app.handle(
      new Request("http://localhost/api/tools/mcp-import-json", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          json: JSON.stringify({ mcpServers: { viaTools: { command: "true", args: [] } } }),
        }),
      }),
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { ok: boolean; count: number };
    expect(body.ok).toBe(true);
    expect(body.count).toBe(1);
  });
});
