/**
 * Unit tests for the pattern detection logic (detectAndCreatePatterns).
 *
 * Run with: bun test packages/mcp-server/src/__tests__/pattern-detection.test.ts
 */

import { describe, expect, mock, test } from "bun:test";

// Mock config before importing
mock.module("../config.js", () => ({
  getActiveWorkspaceId: () => "ws-test",
  getActiveProjectId: () => "proj-test",
  isConfigured: () => true,
  loadConfig: () => {},
}));

// Mock graph-expansion so autoExpandGraph calls don't interfere with pattern tests
mock.module("../graph-expansion.js", () => ({
  autoExpandGraph: mock(async () => ({ relationsCreated: 0 })),
}));

const { detectAndCreatePatterns } = await import("../active-learning.js");
type SessionContext = import("../active-learning.js").SessionContext;

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

interface MockEntity {
  id: string;
  type: string;
  confidence?: number;
  tags?: string[];
}

function makeFullMockClient(opts: {
  entity?: { id: string; type: string };
  searchResults?: MockEntity[];
  existingPatterns?: MockEntity[];
}) {
  const createdEntities: Array<Record<string, unknown>> = [];
  const createdRelations: Array<Record<string, unknown>> = [];
  let nextId = 100;

  return {
    createdEntities,
    createdRelations,

    getMemoryEntity: mock(async (_id: string) => ({
      entity: opts.entity ?? { id: _id, type: "lesson" },
    })),

    searchMemoryEntities: mock(async () => ({
      entities: opts.searchResults ?? [],
      count: opts.searchResults?.length ?? 0,
    })),

    listMemoryEntities: mock(async () => ({
      entities: opts.existingPatterns ?? [],
      count: opts.existingPatterns?.length ?? 0,
    })),

    createMemoryEntity: mock(async (data: Record<string, unknown>) => {
      const entity = { id: `pattern-${nextId++}`, ...data };
      createdEntities.push(entity);
      return { entity };
    }),

    updateMemoryEntity: mock(
      async (id: string, updates: Record<string, unknown>) => ({
        entity: { id, ...updates },
      }),
    ),

    createMemoryRelation: mock(async (data: Record<string, unknown>) => {
      createdRelations.push(data);
      return { relation: { id: `rel-${nextId++}`, ...data } };
    }),
  };
}

function makeSession(overrides: Partial<SessionContext> = {}): SessionContext {
  return {
    cardId: "card-1",
    cardTitle: "Implement feature X",
    cardLabels: ["backend", "api"],
    agentIdentifier: "claude-code",
    agentName: "Claude Code",
    status: "completed",
    ...overrides,
  };
}

// ---- Tests ----

describe("detectAndCreatePatterns", () => {
  describe("threshold gating", () => {
    test("does nothing when fewer than 3 similar entities exist", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: [
          { id: "old-1", type: "lesson" },
          { id: "old-2", type: "lesson" },
        ],
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      expect(client.createMemoryEntity).not.toHaveBeenCalled();
      expect(client.createMemoryRelation).not.toHaveBeenCalled();
    });

    test("creates pattern when exactly 3 similar entities exist", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: [
          { id: "old-1", type: "lesson" },
          { id: "old-2", type: "lesson" },
          { id: "old-3", type: "lesson" },
        ],
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      expect(client.createMemoryEntity).toHaveBeenCalledTimes(1);
      const created = client.createdEntities[0];
      expect(created.type).toBe("pattern");
      expect(created.memory_tier).toBe("reference");
      expect(created.confidence).toBe(0.75);
    });

    test("creates pattern when more than 3 similar entities exist", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "error" },
        searchResults: Array.from({ length: 10 }, (_, i) => ({
          id: `old-${i}`,
          type: "error",
        })),
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      expect(client.createMemoryEntity).toHaveBeenCalledTimes(1);
    });
  });

  describe("newly-created entities are excluded from match count", () => {
    test("newEntityIds are excluded from the existing count", async () => {
      // 3 search results but 2 of them are in newEntityIds → only 1 truly existing
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: [
          { id: "new-2", type: "lesson" }, // also new - should be excluded
          { id: "new-3", type: "lesson" }, // also new - should be excluded
          { id: "old-1", type: "lesson" }, // truly existing
        ],
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1", "new-2", "new-3"],
        makeSession(),
        "ws-test",
      );

      // Only 1 truly existing → below threshold → no pattern
      expect(client.createMemoryEntity).not.toHaveBeenCalled();
    });
  });

  describe("existing pattern update", () => {
    test("updates existing pattern entity instead of creating a new one", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: Array.from({ length: 5 }, (_, i) => ({
          id: `old-${i}`,
          type: "lesson",
        })),
        existingPatterns: [{ id: "existing-pattern", type: "pattern" }],
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      // No new pattern entity created
      expect(client.createMemoryEntity).not.toHaveBeenCalled();
      // Existing one was updated
      expect(client.updateMemoryEntity).toHaveBeenCalledWith(
        "existing-pattern",
        expect.objectContaining({
          content: expect.stringContaining("Recurring pattern"),
          metadata: expect.objectContaining({
            pattern_count: expect.any(Number),
          }),
        }),
      );
    });

    test("creates relation from existing pattern to new entity", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: Array.from({ length: 4 }, (_, i) => ({
          id: `old-${i}`,
          type: "lesson",
        })),
        existingPatterns: [{ id: "existing-pattern", type: "pattern" }],
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      const relationsFromPattern = client.createdRelations.filter(
        (r) => r.source_id === "existing-pattern",
      );
      expect(relationsFromPattern.length).toBeGreaterThan(0);
      const targetIds = relationsFromPattern.map((r) => r.target_id);
      expect(targetIds).toContain("new-1");
    });
  });

  describe("new pattern creation", () => {
    test("pattern entity has correct metadata", async () => {
      const session = makeSession({ cardLabels: ["backend", "api"] });
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "error" },
        searchResults: Array.from({ length: 4 }, (_, i) => ({
          id: `old-${i}`,
          type: "error",
        })),
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        session,
        "ws-test",
        "proj-test",
      );

      const pattern = client.createdEntities[0];
      expect(pattern.type).toBe("pattern");
      expect(pattern.memory_tier).toBe("reference");
      expect(pattern.confidence).toBe(0.75);
      expect(pattern.workspace_id).toBe("ws-test");
      expect(pattern.project_id).toBe("proj-test");
      const meta = pattern.metadata as Record<string, unknown>;
      expect(meta.source).toBe("pattern_detection");
      expect(meta.pattern_type).toBe("error");
    });

    test("pattern title references the entity type", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "solution" },
        searchResults: Array.from({ length: 3 }, (_, i) => ({
          id: `old-${i}`,
          type: "solution",
        })),
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession({ cardLabels: ["backend", "api"] }),
        "ws-test",
      );

      const pattern = client.createdEntities[0];
      expect(pattern.title as string).toContain("Pattern:");
      expect(pattern.title as string).toContain("solution");
    });

    test("creates relates_to relations from pattern to source entities", async () => {
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: [
          { id: "old-1", type: "lesson" },
          { id: "old-2", type: "lesson" },
          { id: "old-3", type: "lesson" },
        ],
      });

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      // Relations should link pattern → new entity + up to 4 existing
      const relations = client.createdRelations;
      expect(relations.length).toBeGreaterThan(0);
      for (const rel of relations) {
        expect(rel.relation_type).toBe("relates_to");
        expect(rel.confidence).toBe(0.75);
      }
      const targetIds = relations.map((r) => r.target_id);
      expect(targetIds).toContain("new-1");
    });
  });

  describe("error handling", () => {
    test("is non-fatal when getMemoryEntity throws", async () => {
      const client = {
        getMemoryEntity: mock(async () => {
          throw new Error("Not found");
        }),
        searchMemoryEntities: mock(async () => ({ entities: [], count: 0 })),
        listMemoryEntities: mock(async () => ({ entities: [], count: 0 })),
        createMemoryEntity: mock(async () => ({ entity: { id: "x" } })),
        updateMemoryEntity: mock(async () => ({ entity: {} })),
        createMemoryRelation: mock(async () => ({ relation: {} })),
      };

      // Should not throw
      const result = await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

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

    test("skips entity if type is missing", async () => {
      const client = makeFullMockClient({
        // entity with no type
        entity: undefined,
        searchResults: [],
      });
      // Override getMemoryEntity to return entity without type
      client.getMemoryEntity = mock(async () => ({
        entity: { id: "new-1" } as { id: string; type: string },
      }));

      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      expect(client.createMemoryEntity).not.toHaveBeenCalled();
    });

    test("duplicate relation failures do not abort remaining relations", async () => {
      let relCount = 0;
      const client = makeFullMockClient({
        entity: { id: "new-1", type: "lesson" },
        searchResults: [
          { id: "old-1", type: "lesson" },
          { id: "old-2", type: "lesson" },
          { id: "old-3", type: "lesson" },
        ],
      });
      // First relation call throws 409, rest succeed
      client.createMemoryRelation = mock(
        async (data: Record<string, unknown>) => {
          relCount++;
          if (relCount === 1)
            throw Object.assign(new Error("Conflict"), { status: 409 });
          client.createdRelations.push(data);
          return { relation: {} };
        },
      );

      // Should not throw
      await detectAndCreatePatterns(
        client as never,
        ["new-1"],
        makeSession(),
        "ws-test",
      );

      // Pattern was still created despite relation error
      expect(client.createdEntities).toHaveLength(1);
    });
  });

  describe("multiple new entities in one session", () => {
    test("processes each new entity independently", async () => {
      let callCount = 0;
      const entityTypes = ["lesson", "error"];

      const client = {
        getMemoryEntity: mock(async (id: string) => ({
          entity: { id, type: entityTypes[callCount++ % 2] ?? "lesson" },
        })),
        searchMemoryEntities: mock(async () => ({
          entities: Array.from({ length: 4 }, (_, i) => ({
            id: `old-${i}`,
            type: "lesson",
          })),
          count: 4,
        })),
        listMemoryEntities: mock(async () => ({ entities: [], count: 0 })),
        createMemoryEntity: mock(async (data: Record<string, unknown>) => ({
          entity: { id: `pat-${Math.random()}`, ...data },
        })),
        updateMemoryEntity: mock(async () => ({ entity: {} })),
        createMemoryRelation: mock(async () => ({ relation: {} })),
        createdEntities: [] as Array<Record<string, unknown>>,
        createdRelations: [] as Array<Record<string, unknown>>,
      };

      await detectAndCreatePatterns(
        client as never,
        ["new-1", "new-2"],
        makeSession(),
        "ws-test",
      );

      // Both new entities processed → 2 pattern entities potentially created
      expect(client.getMemoryEntity).toHaveBeenCalledTimes(2);
    });
  });
});
