import { Test, TestingModule } from "@nestjs/testing";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { LinkSyncService } from "./link-sync.service";
import { LinkResolutionService } from "./link-resolution.service";
import { PrismaService } from "../../prisma/prisma.service";
import * as wikiLinkParser from "../utils/wiki-link-parser";

// Mock the wiki-link parser
vi.mock("../utils/wiki-link-parser");
const mockParseWikiLinks = vi.mocked(wikiLinkParser.parseWikiLinks);

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

  const mockWorkspaceId = "workspace-1";
  const mockEntryId = "entry-1";
  const mockTargetId = "entry-2";

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        LinkSyncService,
        {
          provide: PrismaService,
          useValue: {
            knowledgeLink: {
              findMany: vi.fn(),
              create: vi.fn(),
              update: vi.fn(),
              delete: vi.fn(),
              deleteMany: vi.fn(),
            },
            $transaction: vi.fn((fn) => fn(prisma)),
          },
        },
        {
          provide: LinkResolutionService,
          useValue: {
            resolveLink: vi.fn(),
            resolveLinks: vi.fn(),
          },
        },
      ],
    }).compile();

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

  afterEach(() => {
    vi.clearAllMocks();
  });

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

    it("should parse wiki links from content", async () => {
      const content = "This is a [[Test Link]] in content";
      mockParseWikiLinks.mockReturnValue([
        {
          raw: "[[Test Link]]",
          target: "Test Link",
          displayText: "Test Link",
          start: 10,
          end: 25,
        },
      ]);

      vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
      vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      expect(mockParseWikiLinks).toHaveBeenCalledWith(content);
    });

    it("should create new links when parsing finds wiki links", async () => {
      const content = "This is a [[Test Link]] in content";
      mockParseWikiLinks.mockReturnValue([
        {
          raw: "[[Test Link]]",
          target: "Test Link",
          displayText: "Test Link",
          start: 10,
          end: 25,
        },
      ]);

      vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
      vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({
        id: "link-1",
        sourceId: mockEntryId,
        targetId: mockTargetId,
        linkText: "Test Link",
        displayText: "Test Link",
        positionStart: 10,
        positionEnd: 25,
        resolved: true,
        context: null,
        createdAt: new Date(),
      });

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
        data: {
          sourceId: mockEntryId,
          targetId: mockTargetId,
          linkText: "Test Link",
          displayText: "Test Link",
          positionStart: 10,
          positionEnd: 25,
          resolved: true,
        },
      });
    });

    it("should skip unresolved links when target cannot be found", async () => {
      const content = "This is a [[Nonexistent Link]] in content";
      mockParseWikiLinks.mockReturnValue([
        {
          raw: "[[Nonexistent Link]]",
          target: "Nonexistent Link",
          displayText: "Nonexistent Link",
          start: 10,
          end: 32,
        },
      ]);

      vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(null);
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
      const transactionSpy = vi.spyOn(prisma, "$transaction").mockResolvedValue(undefined);

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      // Should not create any links when target cannot be resolved
      // (schema requires targetId to be non-null)
      expect(transactionSpy).toHaveBeenCalled();
      const transactionFn = transactionSpy.mock.calls[0][0];
      expect(typeof transactionFn).toBe("function");
    });

    it("should handle custom display text in links", async () => {
      const content = "This is a [[Target|Custom Display]] in content";
      mockParseWikiLinks.mockReturnValue([
        {
          raw: "[[Target|Custom Display]]",
          target: "Target",
          displayText: "Custom Display",
          start: 10,
          end: 35,
        },
      ]);

      vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
      vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
        data: {
          sourceId: mockEntryId,
          targetId: mockTargetId,
          linkText: "Target",
          displayText: "Custom Display",
          positionStart: 10,
          positionEnd: 35,
          resolved: true,
        },
      });
    });

    it("should delete orphaned links not present in updated content", async () => {
      const content = "This is a [[New Link]] in content";
      mockParseWikiLinks.mockReturnValue([
        {
          raw: "[[New Link]]",
          target: "New Link",
          displayText: "New Link",
          start: 10,
          end: 22,
        },
      ]);

      // Mock existing link that should be removed
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([
        {
          id: "old-link-1",
          sourceId: mockEntryId,
          targetId: "old-target",
          linkText: "Old Link",
          displayText: "Old Link",
          positionStart: 5,
          positionEnd: 17,
          resolved: true,
          context: null,
          createdAt: new Date(),
        },
      ] as any);

      vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
      vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
      vi.spyOn(prisma.knowledgeLink, "deleteMany").mockResolvedValue({ count: 1 });

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      expect(prisma.knowledgeLink.deleteMany).toHaveBeenCalledWith({
        where: {
          sourceId: mockEntryId,
          id: {
            in: ["old-link-1"],
          },
        },
      });
    });

    it("should handle empty content by removing all links", async () => {
      const content = "";
      mockParseWikiLinks.mockReturnValue([]);

      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([
        {
          id: "link-1",
          sourceId: mockEntryId,
          targetId: mockTargetId,
          linkText: "Link",
          displayText: "Link",
          positionStart: 10,
          positionEnd: 18,
          resolved: true,
          context: null,
          createdAt: new Date(),
        },
      ] as any);

      vi.spyOn(prisma.knowledgeLink, "deleteMany").mockResolvedValue({ count: 1 });

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      expect(prisma.knowledgeLink.deleteMany).toHaveBeenCalledWith({
        where: {
          sourceId: mockEntryId,
          id: {
            in: ["link-1"],
          },
        },
      });
    });

    it("should handle multiple links in content", async () => {
      const content = "Links: [[Link 1]] and [[Link 2]] and [[Link 3]]";
      mockParseWikiLinks.mockReturnValue([
        {
          raw: "[[Link 1]]",
          target: "Link 1",
          displayText: "Link 1",
          start: 7,
          end: 17,
        },
        {
          raw: "[[Link 2]]",
          target: "Link 2",
          displayText: "Link 2",
          start: 22,
          end: 32,
        },
        {
          raw: "[[Link 3]]",
          target: "Link 3",
          displayText: "Link 3",
          start: 37,
          end: 47,
        },
      ]);

      vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
      vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);

      await service.syncLinks(mockWorkspaceId, mockEntryId, content);

      expect(prisma.knowledgeLink.create).toHaveBeenCalledTimes(3);
    });
  });

  describe("getBacklinks", () => {
    it("should return all backlinks for an entry", async () => {
      const mockBacklinks = [
        {
          id: "link-1",
          sourceId: "source-1",
          targetId: mockEntryId,
          linkText: "Link Text",
          displayText: "Link Text",
          positionStart: 10,
          positionEnd: 25,
          resolved: true,
          context: null,
          createdAt: new Date(),
          source: {
            id: "source-1",
            title: "Source Entry",
            slug: "source-entry",
          },
        },
      ];

      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue(mockBacklinks as any);

      const result = await service.getBacklinks(mockEntryId);

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

      expect(result).toEqual(mockBacklinks);
    });

    it("should return empty array when no backlinks exist", async () => {
      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);

      const result = await service.getBacklinks(mockEntryId);

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

  describe("getUnresolvedLinks", () => {
    it("should return all unresolved links for a workspace", async () => {
      const mockUnresolvedLinks = [
        {
          id: "link-1",
          sourceId: mockEntryId,
          targetId: null,
          linkText: "Unresolved Link",
          displayText: "Unresolved Link",
          positionStart: 10,
          positionEnd: 29,
          resolved: false,
          context: null,
          createdAt: new Date(),
        },
      ];

      vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue(mockUnresolvedLinks as any);

      const result = await service.getUnresolvedLinks(mockWorkspaceId);

      expect(prisma.knowledgeLink.findMany).toHaveBeenCalledWith({
        where: {
          source: {
            workspaceId: mockWorkspaceId,
          },
          resolved: false,
        },
        include: {
          source: {
            select: {
              id: true,
              title: true,
              slug: true,
            },
          },
        },
      });

      expect(result).toEqual(mockUnresolvedLinks);
    });
  });
});
