import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { NotFoundException } from "@nestjs/common";
import { GraphService } from "./graph.service";
import { PrismaService } from "../../prisma/prisma.service";
import { KnowledgeCacheService } from "./cache.service";

describe("GraphService", () => {
  let service: GraphService;
  let prisma: PrismaService;

  const mockEntry = {
    id: "entry-1",
    workspaceId: "workspace-1",
    slug: "test-entry",
    title: "Test Entry",
    content: "Test content",
    contentHtml: "<p>Test content</p>",
    summary: "Test summary",
    status: "PUBLISHED",
    visibility: "WORKSPACE",
    createdAt: new Date(),
    updatedAt: new Date(),
    createdBy: "user-1",
    updatedBy: "user-1",
    tags: [],
    outgoingLinks: [],
    incomingLinks: [],
  };

  const mockPrismaService = {
    knowledgeEntry: {
      findUnique: vi.fn(),
    },
  };

  const mockCacheService = {
    isEnabled: vi.fn().mockReturnValue(false),
    getEntry: vi.fn().mockResolvedValue(null),
    setEntry: vi.fn(),
    invalidateEntry: vi.fn(),
    getGraph: vi.fn().mockResolvedValue(null),
    setGraph: vi.fn(),
    invalidateGraph: vi.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        GraphService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
        {
          provide: KnowledgeCacheService,
          useValue: mockCacheService,
        },
      ],
    }).compile();

    service = module.get<GraphService>(GraphService);
    prisma = module.get<PrismaService>(PrismaService);

    vi.clearAllMocks();
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("getEntryGraphBySlug", () => {
    it("should throw NotFoundException if entry does not exist", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null);

      await expect(service.getEntryGraphBySlug("workspace-1", "non-existent", 1)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should call getEntryGraph with entry ID", async () => {
      const mockEntry = {
        id: "entry-1",
        workspaceId: "workspace-1",
        slug: "test-entry",
        tags: [],
        outgoingLinks: [],
        incomingLinks: [],
      };

      mockPrismaService.knowledgeEntry.findUnique
        .mockResolvedValueOnce(mockEntry) // First call in getEntryGraphBySlug
        .mockResolvedValueOnce(mockEntry) // Second call in getEntryGraph validation
        .mockResolvedValueOnce(mockEntry); // Third call in getEntryGraph BFS

      await service.getEntryGraphBySlug("workspace-1", "test-entry", 1);

      expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_slug: {
            workspaceId: "workspace-1",
            slug: "test-entry",
          },
        },
      });
    });
  });

  describe("getEntryGraph", () => {
    it("should throw NotFoundException if entry does not exist", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null);

      await expect(service.getEntryGraph("workspace-1", "non-existent", 1)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should throw NotFoundException if entry belongs to different workspace", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue({
        ...mockEntry,
        workspaceId: "different-workspace",
      });

      await expect(service.getEntryGraph("workspace-1", "entry-1", 1)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should return graph with center node when depth is 0", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry);

      const result = await service.getEntryGraph("workspace-1", "entry-1", 0);

      expect(result.centerNode.id).toBe("entry-1");
      expect(result.nodes).toHaveLength(1);
      expect(result.edges).toHaveLength(0);
      expect(result.stats.totalNodes).toBe(1);
      expect(result.stats.totalEdges).toBe(0);
    });

    it("should build graph with connected nodes at depth 1", async () => {
      const linkedEntry = {
        id: "entry-2",
        workspaceId: "workspace-1",
        slug: "linked-entry",
        title: "Linked Entry",
        content: "Linked content",
        contentHtml: "<p>Linked content</p>",
        summary: null,
        status: "PUBLISHED",
        visibility: "WORKSPACE",
        createdAt: new Date(),
        updatedAt: new Date(),
        createdBy: "user-1",
        updatedBy: "user-1",
        tags: [],
        outgoingLinks: [],
        incomingLinks: [],
      };

      mockPrismaService.knowledgeEntry.findUnique
        // First call: initial validation (with tags only)
        .mockResolvedValueOnce(mockEntry)
        // Second call: BFS for center entry (with tags and links)
        .mockResolvedValueOnce({
          ...mockEntry,
          outgoingLinks: [
            {
              id: "link-1",
              sourceId: "entry-1",
              targetId: "entry-2",
              linkText: "link to entry 2",
              resolved: true,
              target: linkedEntry,
            },
          ],
          incomingLinks: [],
        })
        // Third call: BFS for linked entry
        .mockResolvedValueOnce(linkedEntry);

      const result = await service.getEntryGraph("workspace-1", "entry-1", 1);

      expect(result.nodes).toHaveLength(2);
      expect(result.edges).toHaveLength(1);
      expect(result.stats.totalNodes).toBe(2);
      expect(result.stats.totalEdges).toBe(1);
    });
  });

  describe("getFullGraph", () => {
    beforeEach(() => {
      // Add findMany mock
      mockPrismaService.knowledgeEntry.findMany = vi.fn();
      mockPrismaService.knowledgeLink = {
        findMany: vi.fn(),
      };
    });

    it("should return full graph with all entries and links", async () => {
      const entries = [
        { ...mockEntry, id: "entry-1", slug: "entry-1", tags: [] },
        {
          ...mockEntry,
          id: "entry-2",
          slug: "entry-2",
          title: "Entry 2",
          tags: [],
        },
      ];

      const links = [
        {
          id: "link-1",
          sourceId: "entry-1",
          targetId: "entry-2",
          linkText: "link text",
          resolved: true,
        },
      ];

      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue(entries);
      mockPrismaService.knowledgeLink.findMany.mockResolvedValue(links);

      const result = await service.getFullGraph("workspace-1");

      expect(result.nodes).toHaveLength(2);
      expect(result.edges).toHaveLength(1);
      expect(result.stats.totalNodes).toBe(2);
      expect(result.stats.totalEdges).toBe(1);
      expect(result.stats.orphanCount).toBe(0);
    });

    it("should detect orphan entries (entries with no links)", async () => {
      const entries = [
        { ...mockEntry, id: "entry-1", slug: "entry-1", tags: [] },
        {
          ...mockEntry,
          id: "entry-2",
          slug: "entry-2",
          title: "Entry 2",
          tags: [],
        },
        {
          ...mockEntry,
          id: "entry-3",
          slug: "entry-3",
          title: "Entry 3 (orphan)",
          tags: [],
        },
      ];

      const links = [
        {
          id: "link-1",
          sourceId: "entry-1",
          targetId: "entry-2",
          linkText: "link text",
          resolved: true,
        },
      ];

      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue(entries);
      mockPrismaService.knowledgeLink.findMany.mockResolvedValue(links);

      const result = await service.getFullGraph("workspace-1");

      expect(result.stats.orphanCount).toBe(1);
      const orphanNode = result.nodes.find((n) => n.id === "entry-3");
      expect(orphanNode?.isOrphan).toBe(true);
    });

    it("should filter by status", async () => {
      const entries = [{ ...mockEntry, id: "entry-1", status: "PUBLISHED", tags: [] }];

      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue(entries);
      mockPrismaService.knowledgeLink.findMany.mockResolvedValue([]);

      await service.getFullGraph("workspace-1", { status: "PUBLISHED" });

      expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            status: "PUBLISHED",
          }),
        })
      );
    });

    it("should filter by tags", async () => {
      const entries = [{ ...mockEntry, id: "entry-1", tags: [] }];

      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue(entries);
      mockPrismaService.knowledgeLink.findMany.mockResolvedValue([]);

      await service.getFullGraph("workspace-1", { tags: ["tag-1", "tag-2"] });

      expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            tags: {
              some: {
                tag: {
                  slug: {
                    in: ["tag-1", "tag-2"],
                  },
                },
              },
            },
          }),
        })
      );
    });

    it("should limit number of nodes", async () => {
      const entries = [
        { ...mockEntry, id: "entry-1", slug: "entry-1", tags: [] },
        { ...mockEntry, id: "entry-2", slug: "entry-2", tags: [] },
      ];

      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue(entries);
      mockPrismaService.knowledgeLink.findMany.mockResolvedValue([]);

      await service.getFullGraph("workspace-1", { limit: 1 });

      expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          take: 1,
        })
      );
    });
  });

  describe("getGraphStats", () => {
    beforeEach(() => {
      mockPrismaService.knowledgeEntry.count = vi.fn();
      mockPrismaService.knowledgeEntry.findMany = vi.fn();
      mockPrismaService.knowledgeLink = {
        count: vi.fn(),
        groupBy: vi.fn(),
      };
      mockPrismaService.$queryRaw = vi.fn();
    });

    it("should return graph statistics", async () => {
      mockPrismaService.knowledgeEntry.count.mockResolvedValue(10);
      mockPrismaService.knowledgeLink.count.mockResolvedValue(15);
      mockPrismaService.$queryRaw.mockResolvedValue([
        { id: "entry-1", slug: "entry-1", title: "Entry 1", link_count: "5" },
        { id: "entry-2", slug: "entry-2", title: "Entry 2", link_count: "3" },
      ]);
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([{ id: "orphan-1" }]);

      const result = await service.getGraphStats("workspace-1");

      expect(result.totalEntries).toBe(10);
      expect(result.totalLinks).toBe(15);
      expect(result.averageLinks).toBe(1.5);
      expect(result.mostConnectedEntries).toHaveLength(2);
      expect(result.mostConnectedEntries[0].linkCount).toBe(5);
    });

    it("should calculate orphan entries correctly", async () => {
      mockPrismaService.knowledgeEntry.count.mockResolvedValue(5);
      mockPrismaService.knowledgeLink.count.mockResolvedValue(2);
      mockPrismaService.$queryRaw.mockResolvedValue([]);
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([
        { id: "orphan-1" },
        { id: "orphan-2" },
      ]);

      const result = await service.getGraphStats("workspace-1");

      expect(result.orphanEntries).toBe(2);
    });
  });
});
