/**
 * Unit tests for graph-expansion.ts (autoExpandGraph).
 *
 * Run with: bun test packages/mcp-server/src/__tests__/graph-expansion.test.ts
 */

import { describe, expect, mock, test } from "bun:test";
import { autoExpandGraph } from "../graph-expansion.js";

// ---- Mock helpers ----

interface MockEntity {
  id: string;
  confidence?: number;
}

function makeMockClient(
  entities: MockEntity[] = [],
  relationError?: { status?: number } | Error,
) {
  const createdRelations: Array<Record<string, unknown>> = [];

  return {
    createdRelations,
    searchMemoryEntities: mock(async () => ({
      entities,
      count: entities.length,
    })),
    createMemoryRelation: mock(async (data: Record<string, unknown>) => {
      if (relationError) throw relationError;
      createdRelations.push(data);
      return { relation: { id: "rel-1", ...data } };
    }),
  };
}

// ---- Tests ----

describe("autoExpandGraph", () => {
  describe("basic relation creation", () => {
    test("creates relations for each matching entity", async () => {
      const client = makeMockClient([
        { id: "other-1", confidence: 0.9 },
        { id: "other-2", confidence: 0.8 },
      ]);

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "React hooks guide",
        "How to use useState and useEffect effectively.",
        ["react"],
        "ws-1",
      );

      expect(result.relationsCreated).toBe(2);
      expect(client.createdRelations).toHaveLength(2);
      expect(client.createdRelations[0]).toMatchObject({
        source_id: "new-entity",
        target_id: "other-1",
        relation_type: "relates_to",
        confidence: 0.6,
      });
    });

    test("excludes self from relations", async () => {
      const client = makeMockClient([
        { id: "new-entity", confidence: 1.0 }, // same as entityId
        { id: "other-1", confidence: 0.9 },
      ]);

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "Title",
        "Content",
        [],
        "ws-1",
      );

      expect(result.relationsCreated).toBe(1);
      expect(client.createdRelations[0].target_id).toBe("other-1");
    });

    test("excludes entities with confidence below 0.4", async () => {
      const client = makeMockClient([
        { id: "low-conf", confidence: 0.3 },
        { id: "ok-conf", confidence: 0.4 },
        { id: "no-conf" }, // undefined confidence → treated as 1
      ]);

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "Title",
        "Content",
        [],
        "ws-1",
      );

      expect(result.relationsCreated).toBe(2);
      const targetIds = client.createdRelations.map((r) => r.target_id);
      expect(targetIds).toContain("ok-conf");
      expect(targetIds).toContain("no-conf");
      expect(targetIds).not.toContain("low-conf");
    });

    test("respects maxRelations cap", async () => {
      const entities: MockEntity[] = Array.from({ length: 10 }, (_, i) => ({
        id: `entity-${i}`,
        confidence: 0.9,
      }));
      const client = makeMockClient(entities);

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "Title",
        "Content",
        [],
        "ws-1",
        undefined,
        3, // maxRelations = 3
      );

      expect(result.relationsCreated).toBe(3);
      expect(client.createdRelations).toHaveLength(3);
    });

    test("returns 0 when no entities returned by search", async () => {
      const client = makeMockClient([]);

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "Unique topic",
        "Nothing similar exists yet.",
        [],
        "ws-1",
      );

      expect(result.relationsCreated).toBe(0);
      expect(client.createMemoryRelation).not.toHaveBeenCalled();
    });
  });

  describe("search query construction", () => {
    test("builds query from title + content snippet", async () => {
      const client = makeMockClient([]);

      await autoExpandGraph(
        client as never,
        "eid",
        "Authentication guide",
        "How to implement JWT-based authentication in Next.js applications using Supabase.",
        ["auth"],
        "ws-1",
      );

      const [calledWorkspaceId, calledQuery] = client.searchMemoryEntities.mock
        .calls[0] as [string, string];
      expect(calledWorkspaceId).toBe("ws-1");
      expect(calledQuery).toContain("Authentication guide");
      expect(calledQuery).toContain("JWT");
    });

    test("content snippet is limited to 200 characters", async () => {
      const longContent = "X".repeat(500);
      const client = makeMockClient([]);

      await autoExpandGraph(
        client as never,
        "eid",
        "Title",
        longContent,
        [],
        "ws-1",
      );

      const [, calledQuery] = client.searchMemoryEntities.mock.calls[0] as [
        string,
        string,
      ];
      // Query = "Title " + 200 chars max from content
      expect(calledQuery.length).toBeLessThanOrEqual("Title".length + 1 + 200);
    });

    test("passes projectId to search", async () => {
      const client = makeMockClient([]);

      await autoExpandGraph(
        client as never,
        "eid",
        "Title",
        "Content",
        [],
        "ws-1",
        "proj-42",
      );

      const [, , opts] = client.searchMemoryEntities.mock.calls[0] as [
        string,
        string,
        { project_id?: string },
      ];
      expect(opts?.project_id).toBe("proj-42");
    });
  });

  describe("error handling", () => {
    test("returns 0 when searchMemoryEntities throws", async () => {
      const client = {
        searchMemoryEntities: mock(async () => {
          throw new Error("Network error");
        }),
        createMemoryRelation: mock(async () => ({ relation: {} })),
      };

      const result = await autoExpandGraph(
        client as never,
        "eid",
        "Title",
        "Content",
        [],
        "ws-1",
      );

      expect(result.relationsCreated).toBe(0);
      expect(client.createMemoryRelation).not.toHaveBeenCalled();
    });

    test("skips individual relation failures, counts successes", async () => {
      let callCount = 0;
      const client = {
        searchMemoryEntities: mock(async () => ({
          entities: [
            { id: "a", confidence: 0.9 },
            { id: "b", confidence: 0.9 },
            { id: "c", confidence: 0.9 },
          ],
          count: 3,
        })),
        createMemoryRelation: mock(async () => {
          callCount++;
          if (callCount === 2) throw new Error("Relation failed");
          return { relation: {} };
        }),
      };

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "Title",
        "Content",
        [],
        "ws-1",
      );

      // a succeeds, b fails, c succeeds → 2 created
      expect(result.relationsCreated).toBe(2);
    });

    test("silently handles 409 conflict (duplicate relation)", async () => {
      const conflictError = Object.assign(new Error("Conflict"), {
        status: 409,
      });
      const client = makeMockClient(
        [{ id: "existing", confidence: 0.9 }],
        conflictError,
      );

      const result = await autoExpandGraph(
        client as never,
        "new-entity",
        "Title",
        "Content",
        [],
        "ws-1",
      );

      // 409 is swallowed, count stays 0
      expect(result.relationsCreated).toBe(0);
    });
  });
});
