import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { LinkResolutionService } from "./link-resolution.service";
import { PrismaService } from "../../prisma/prisma.service";

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

  const workspaceId = "workspace-123";

  const mockEntries = [
    {
      id: "entry-1",
      workspaceId,
      slug: "typescript-guide",
      title: "TypeScript Guide",
      content: "...",
      contentHtml: "...",
      summary: null,
      status: "PUBLISHED",
      visibility: "WORKSPACE",
      createdAt: new Date(),
      updatedAt: new Date(),
      createdBy: "user-1",
      updatedBy: "user-1",
    },
    {
      id: "entry-2",
      workspaceId,
      slug: "react-hooks",
      title: "React Hooks",
      content: "...",
      contentHtml: "...",
      summary: null,
      status: "PUBLISHED",
      visibility: "WORKSPACE",
      createdAt: new Date(),
      updatedAt: new Date(),
      createdBy: "user-1",
      updatedBy: "user-1",
    },
    {
      id: "entry-3",
      workspaceId,
      slug: "react-hooks-advanced",
      title: "React Hooks Advanced",
      content: "...",
      contentHtml: "...",
      summary: null,
      status: "PUBLISHED",
      visibility: "WORKSPACE",
      createdAt: new Date(),
      updatedAt: new Date(),
      createdBy: "user-1",
      updatedBy: "user-1",
    },
  ];

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

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

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

    vi.clearAllMocks();
  });

  describe("resolveLink", () => {
    describe("Exact title match", () => {
      it("should resolve link by exact title match", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);

        const result = await service.resolveLink(workspaceId, "TypeScript Guide");

        expect(result).toBe("entry-1");
        expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledWith({
          where: {
            workspaceId,
            title: "TypeScript Guide",
          },
          select: {
            id: true,
          },
        });
      });

      it("should be case-sensitive for exact title match", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);

        const result = await service.resolveLink(workspaceId, "typescript guide");

        expect(result).toBeNull();
      });
    });

    describe("Slug match", () => {
      it("should resolve link by slug", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(mockEntries[0]);

        const result = await service.resolveLink(workspaceId, "typescript-guide");

        expect(result).toBe("entry-1");
        expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith({
          where: {
            workspaceId_slug: {
              workspaceId,
              slug: "typescript-guide",
            },
          },
          select: {
            id: true,
          },
        });
      });

      it("should prioritize exact title match over slug match", async () => {
        // If exact title matches, slug should not be checked
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);

        const result = await service.resolveLink(workspaceId, "TypeScript Guide");

        expect(result).toBe("entry-1");
        expect(mockPrismaService.knowledgeEntry.findUnique).not.toHaveBeenCalled();
      });
    });

    describe("Fuzzy title match", () => {
      it("should resolve link by case-insensitive fuzzy match", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([mockEntries[0]]);

        const result = await service.resolveLink(workspaceId, "typescript guide");

        expect(result).toBe("entry-1");
        expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith({
          where: {
            workspaceId,
            title: {
              mode: "insensitive",
              equals: "typescript guide",
            },
          },
          select: {
            id: true,
            title: true,
          },
        });
      });

      it("should return null when fuzzy match finds multiple results", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([
          mockEntries[1],
          mockEntries[2],
        ]);

        const result = await service.resolveLink(workspaceId, "react hooks");

        expect(result).toBeNull();
      });

      it("should return null when no match is found", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);

        const result = await service.resolveLink(workspaceId, "Non-existent Entry");

        expect(result).toBeNull();
      });
    });

    describe("Workspace scoping", () => {
      it("should only resolve links within the specified workspace", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
        mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);

        await service.resolveLink("different-workspace", "TypeScript Guide");

        expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledWith(
          expect.objectContaining({
            where: expect.objectContaining({
              workspaceId: "different-workspace",
            }),
          })
        );
      });
    });

    describe("Edge cases", () => {
      it("should handle empty string input", async () => {
        const result = await service.resolveLink(workspaceId, "");

        expect(result).toBeNull();
        expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
      });

      it("should handle null input", async () => {
        const result = await service.resolveLink(workspaceId, null as any);

        expect(result).toBeNull();
        expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
      });

      it("should handle whitespace-only input", async () => {
        const result = await service.resolveLink(workspaceId, "   ");

        expect(result).toBeNull();
        expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
      });

      it("should trim whitespace from target before resolving", async () => {
        mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);

        const result = await service.resolveLink(workspaceId, "  TypeScript Guide  ");

        expect(result).toBe("entry-1");
        expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledWith(
          expect.objectContaining({
            where: expect.objectContaining({
              title: "TypeScript Guide",
            }),
          })
        );
      });
    });
  });

  describe("resolveLinks", () => {
    it("should resolve multiple links in batch", async () => {
      // First link: "TypeScript Guide" -> exact title match
      // Second link: "react-hooks" -> slug match
      mockPrismaService.knowledgeEntry.findFirst.mockImplementation(async ({ where }: any) => {
        if (where.title === "TypeScript Guide") {
          return mockEntries[0];
        }
        return null;
      });

      mockPrismaService.knowledgeEntry.findUnique.mockImplementation(async ({ where }: any) => {
        if (where.workspaceId_slug?.slug === "react-hooks") {
          return mockEntries[1];
        }
        return null;
      });

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

      const targets = ["TypeScript Guide", "react-hooks"];
      const result = await service.resolveLinks(workspaceId, targets);

      expect(result).toEqual({
        "TypeScript Guide": "entry-1",
        "react-hooks": "entry-2",
      });
    });

    it("should handle empty array", async () => {
      const result = await service.resolveLinks(workspaceId, []);

      expect(result).toEqual({});
      expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
    });

    it("should handle unresolved links", async () => {
      mockPrismaService.knowledgeEntry.findFirst.mockResolvedValue(null);
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null);
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([]);

      const result = await service.resolveLinks(workspaceId, [
        "Non-existent",
        "Another-Non-existent",
      ]);

      expect(result).toEqual({
        "Non-existent": null,
        "Another-Non-existent": null,
      });
    });

    it("should deduplicate targets", async () => {
      mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);

      const result = await service.resolveLinks(workspaceId, [
        "TypeScript Guide",
        "TypeScript Guide",
      ]);

      expect(result).toEqual({
        "TypeScript Guide": "entry-1",
      });
      // Should only be called once for the deduplicated target
      expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledTimes(1);
    });
  });

  describe("getAmbiguousMatches", () => {
    it("should return multiple entries that match case-insensitively", async () => {
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([
        { id: "entry-2", title: "React Hooks" },
        { id: "entry-3", title: "React Hooks Advanced" },
      ]);

      const result = await service.getAmbiguousMatches(workspaceId, "react hooks");

      expect(result).toHaveLength(2);
      expect(result).toEqual([
        { id: "entry-2", title: "React Hooks" },
        { id: "entry-3", title: "React Hooks Advanced" },
      ]);
    });

    it("should return empty array when no matches found", async () => {
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);

      const result = await service.getAmbiguousMatches(workspaceId, "Non-existent");

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

    it("should return single entry if only one match", async () => {
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([
        { id: "entry-1", title: "TypeScript Guide" },
      ]);

      const result = await service.getAmbiguousMatches(workspaceId, "typescript guide");

      expect(result).toHaveLength(1);
    });
  });

  describe("resolveLinksFromContent", () => {
    it("should parse and resolve wiki links from content", async () => {
      const content = "Check out [[TypeScript Guide]] and [[React Hooks]] for more info.";

      // Mock resolveLink for each target
      mockPrismaService.knowledgeEntry.findFirst
        .mockResolvedValueOnce({ id: "entry-1" }) // TypeScript Guide
        .mockResolvedValueOnce({ id: "entry-2" }); // React Hooks

      const result = await service.resolveLinksFromContent(content, workspaceId);

      expect(result).toHaveLength(2);
      expect(result[0].link.target).toBe("TypeScript Guide");
      expect(result[0].entryId).toBe("entry-1");
      expect(result[1].link.target).toBe("React Hooks");
      expect(result[1].entryId).toBe("entry-2");
    });

    it("should return null entryId for unresolved links", async () => {
      const content = "See [[Non-existent Page]] for details.";

      mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
      mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);

      const result = await service.resolveLinksFromContent(content, workspaceId);

      expect(result).toHaveLength(1);
      expect(result[0].link.target).toBe("Non-existent Page");
      expect(result[0].entryId).toBeNull();
    });

    it("should return empty array for content with no wiki links", async () => {
      const content = "This content has no wiki links.";

      const result = await service.resolveLinksFromContent(content, workspaceId);

      expect(result).toEqual([]);
      expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
    });

    it("should handle content with display text syntax", async () => {
      const content = "Read the [[typescript-guide|TS Guide]] first.";

      mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce({
        id: "entry-1",
      });

      const result = await service.resolveLinksFromContent(content, workspaceId);

      expect(result).toHaveLength(1);
      expect(result[0].link.target).toBe("typescript-guide");
      expect(result[0].link.displayText).toBe("TS Guide");
      expect(result[0].entryId).toBe("entry-1");
    });

    it("should preserve link position information", async () => {
      const content = "Start [[Page One]] middle [[Page Two]] end.";

      mockPrismaService.knowledgeEntry.findFirst
        .mockResolvedValueOnce({ id: "entry-1" })
        .mockResolvedValueOnce({ id: "entry-2" });

      const result = await service.resolveLinksFromContent(content, workspaceId);

      expect(result).toHaveLength(2);
      expect(result[0].link.start).toBe(6);
      expect(result[0].link.end).toBe(18);
      expect(result[1].link.start).toBe(26);
      expect(result[1].link.end).toBe(38);
    });
  });

  describe("getBacklinks", () => {
    it("should return all entries that link to the target entry", async () => {
      const targetEntryId = "entry-target";
      const mockBacklinks = [
        {
          id: "link-1",
          sourceId: "entry-1",
          targetId: targetEntryId,
          linkText: "Target Page",
          displayText: "Target Page",
          positionStart: 10,
          positionEnd: 25,
          resolved: true,
          context: null,
          createdAt: new Date(),
          source: {
            id: "entry-1",
            title: "TypeScript Guide",
            slug: "typescript-guide",
          },
        },
        {
          id: "link-2",
          sourceId: "entry-2",
          targetId: targetEntryId,
          linkText: "Target Page",
          displayText: "See Target",
          positionStart: 50,
          positionEnd: 70,
          resolved: true,
          context: null,
          createdAt: new Date(),
          source: {
            id: "entry-2",
            title: "React Hooks",
            slug: "react-hooks",
          },
        },
      ];

      mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce(mockBacklinks);

      const result = await service.getBacklinks(targetEntryId);

      expect(result).toHaveLength(2);
      expect(result[0]).toEqual({
        sourceId: "entry-1",
        sourceTitle: "TypeScript Guide",
        sourceSlug: "typescript-guide",
        linkText: "Target Page",
        displayText: "Target Page",
      });
      expect(result[1]).toEqual({
        sourceId: "entry-2",
        sourceTitle: "React Hooks",
        sourceSlug: "react-hooks",
        linkText: "Target Page",
        displayText: "See Target",
      });

      expect(mockPrismaService.knowledgeLink.findMany).toHaveBeenCalledWith({
        where: {
          targetId: targetEntryId,
          resolved: true,
        },
        include: {
          source: {
            select: {
              id: true,
              title: true,
              slug: true,
            },
          },
        },
        orderBy: {
          createdAt: "desc",
        },
      });
    });

    it("should return empty array when no backlinks exist", async () => {
      mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce([]);

      const result = await service.getBacklinks("entry-with-no-backlinks");

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

    it("should only return resolved backlinks", async () => {
      const targetEntryId = "entry-target";

      mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce([]);

      await service.getBacklinks(targetEntryId);

      expect(mockPrismaService.knowledgeLink.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            resolved: true,
          }),
        })
      );
    });
  });
});
