import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { KnowledgeService } from "./knowledge.service";
import { PrismaService } from "../prisma/prisma.service";
import { LinkSyncService } from "./services/link-sync.service";
import { KnowledgeCacheService } from "./services/cache.service";
import { EmbeddingService } from "./services/embedding.service";
import { OllamaEmbeddingService } from "./services/ollama-embedding.service";
import { EmbeddingQueueService } from "./queues/embedding-queue.service";
import { NotFoundException } from "@nestjs/common";

describe("KnowledgeService - Version History", () => {
  let service: KnowledgeService;
  let prisma: PrismaService;
  let linkSync: LinkSyncService;

  const workspaceId = "workspace-123";
  const userId = "user-456";
  const entryId = "entry-789";
  const slug = "test-entry";

  const mockEntry = {
    id: entryId,
    workspaceId,
    slug,
    title: "Test Entry",
    content: "# Test Content",
    contentHtml: "<h1>Test Content</h1>",
    summary: "Test summary",
    status: "PUBLISHED",
    visibility: "WORKSPACE",
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date("2026-01-20"),
    createdBy: userId,
    updatedBy: userId,
  };

  const mockVersions = [
    {
      id: "version-3",
      entryId,
      version: 3,
      title: "Test Entry v3",
      content: "# Version 3",
      summary: "Summary v3",
      createdAt: new Date("2026-01-20"),
      createdBy: userId,
      changeNote: "Updated content",
      author: {
        id: userId,
        name: "Test User",
        email: "test@example.com",
      },
    },
    {
      id: "version-2",
      entryId,
      version: 2,
      title: "Test Entry v2",
      content: "# Version 2",
      summary: "Summary v2",
      createdAt: new Date("2026-01-15"),
      createdBy: userId,
      changeNote: "Second version",
      author: {
        id: userId,
        name: "Test User",
        email: "test@example.com",
      },
    },
    {
      id: "version-1",
      entryId,
      version: 1,
      title: "Test Entry v1",
      content: "# Version 1",
      summary: "Summary v1",
      createdAt: new Date("2026-01-10"),
      createdBy: userId,
      changeNote: "Initial version",
      author: {
        id: userId,
        name: "Test User",
        email: "test@example.com",
      },
    },
  ];

  const mockPrismaService = {
    knowledgeEntry: {
      findUnique: vi.fn(),
      update: vi.fn(),
    },
    knowledgeEntryVersion: {
      count: vi.fn(),
      findMany: vi.fn(),
      findUnique: vi.fn(),
      create: vi.fn(),
    },
    $transaction: vi.fn(),
  };

  const mockLinkSyncService = {
    syncLinks: vi.fn(),
  };

  const mockCacheService = {
    getEntry: vi.fn().mockResolvedValue(null),
    setEntry: vi.fn().mockResolvedValue(undefined),
    invalidateEntry: vi.fn().mockResolvedValue(undefined),
    getSearch: vi.fn().mockResolvedValue(null),
    setSearch: vi.fn().mockResolvedValue(undefined),
    invalidateSearches: vi.fn().mockResolvedValue(undefined),
    getGraph: vi.fn().mockResolvedValue(null),
    setGraph: vi.fn().mockResolvedValue(undefined),
    invalidateGraphs: vi.fn().mockResolvedValue(undefined),
    invalidateGraphsForEntry: vi.fn().mockResolvedValue(undefined),
    clearWorkspaceCache: vi.fn().mockResolvedValue(undefined),
    getStats: vi.fn().mockReturnValue({ hits: 0, misses: 0, sets: 0, deletes: 0, hitRate: 0 }),
    resetStats: vi.fn(),
    isEnabled: vi.fn().mockReturnValue(false),
  };

  const mockEmbeddingService = {
    isConfigured: vi.fn().mockReturnValue(false),
    generateEmbedding: vi.fn().mockResolvedValue(null),
    batchGenerateEmbeddings: vi.fn().mockResolvedValue([]),
  };

  const mockOllamaEmbeddingService = {
    isConfigured: vi.fn().mockResolvedValue(false),
    generateEmbedding: vi.fn().mockResolvedValue([]),
    generateAndStoreEmbedding: vi.fn().mockResolvedValue(undefined),
    batchGenerateEmbeddings: vi.fn().mockResolvedValue(0),
  };

  const mockEmbeddingQueueService = {
    enqueueEmbeddingGeneration: vi.fn().mockResolvedValue(undefined),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        KnowledgeService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
        {
          provide: LinkSyncService,
          useValue: mockLinkSyncService,
        },
        {
          provide: KnowledgeCacheService,
          useValue: mockCacheService,
        },
        {
          provide: EmbeddingService,
          useValue: mockEmbeddingService,
        },
        {
          provide: OllamaEmbeddingService,
          useValue: mockOllamaEmbeddingService,
        },
        {
          provide: EmbeddingQueueService,
          useValue: mockEmbeddingQueueService,
        },
      ],
    }).compile();

    service = module.get<KnowledgeService>(KnowledgeService);
    prisma = module.get<PrismaService>(PrismaService);
    linkSync = module.get<LinkSyncService>(LinkSyncService);

    vi.clearAllMocks();
  });

  describe("findVersions", () => {
    it("should return paginated versions for an entry", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry);
      mockPrismaService.knowledgeEntryVersion.count.mockResolvedValue(3);
      mockPrismaService.knowledgeEntryVersion.findMany.mockResolvedValue(mockVersions);

      const result = await service.findVersions(workspaceId, slug, 1, 20);

      expect(result).toEqual({
        data: mockVersions,
        pagination: {
          page: 1,
          limit: 20,
          total: 3,
          totalPages: 1,
        },
      });

      expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_slug: {
            workspaceId,
            slug,
          },
        },
      });

      expect(mockPrismaService.knowledgeEntryVersion.count).toHaveBeenCalledWith({
        where: { entryId },
      });

      expect(mockPrismaService.knowledgeEntryVersion.findMany).toHaveBeenCalledWith({
        where: { entryId },
        include: {
          author: {
            select: {
              id: true,
              name: true,
              email: true,
            },
          },
        },
        orderBy: {
          version: "desc",
        },
        skip: 0,
        take: 20,
      });
    });

    it("should handle pagination correctly", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry);
      mockPrismaService.knowledgeEntryVersion.count.mockResolvedValue(50);
      mockPrismaService.knowledgeEntryVersion.findMany.mockResolvedValue([mockVersions[0]]);

      const result = await service.findVersions(workspaceId, slug, 2, 20);

      expect(result.pagination).toEqual({
        page: 2,
        limit: 20,
        total: 50,
        totalPages: 3,
      });

      expect(mockPrismaService.knowledgeEntryVersion.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          skip: 20, // (page 2 - 1) * 20
          take: 20,
        })
      );
    });

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

      await expect(service.findVersions(workspaceId, slug)).rejects.toThrow(NotFoundException);

      expect(mockPrismaService.knowledgeEntryVersion.count).not.toHaveBeenCalled();
    });
  });

  describe("findVersion", () => {
    it("should return a specific version", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry);
      mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(mockVersions[1]);

      const result = await service.findVersion(workspaceId, slug, 2);

      expect(result).toEqual(mockVersions[1]);

      expect(mockPrismaService.knowledgeEntryVersion.findUnique).toHaveBeenCalledWith({
        where: {
          entryId_version: {
            entryId,
            version: 2,
          },
        },
        include: {
          author: {
            select: {
              id: true,
              name: true,
              email: true,
            },
          },
        },
      });
    });

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

      await expect(service.findVersion(workspaceId, slug, 2)).rejects.toThrow(NotFoundException);
    });

    it("should throw NotFoundException if version does not exist", async () => {
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry);
      mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(null);

      await expect(service.findVersion(workspaceId, slug, 99)).rejects.toThrow(NotFoundException);
    });
  });

  describe("restoreVersion", () => {
    it("should restore a previous version and create a new version", async () => {
      const entryWithVersions = {
        ...mockEntry,
        versions: [mockVersions[0]], // Latest version is v3
        tags: [],
      };

      const updatedEntry = {
        ...mockEntry,
        title: "Test Entry v2",
        content: "# Version 2",
        contentHtml: "<h1>Version 2</h1>",
        summary: "Summary v2",
        tags: [],
      };

      // Mock findVersion to return version 2
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(mockEntry);
      mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(mockVersions[1]);

      // Mock transaction
      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const tx = {
          knowledgeEntry: {
            update: vi.fn().mockResolvedValue(updatedEntry),
            findUnique: vi.fn().mockResolvedValue({
              ...updatedEntry,
              tags: [],
            }),
          },
          knowledgeEntryVersion: {
            create: vi.fn().mockResolvedValue({
              id: "version-4",
              entryId,
              version: 4,
              title: "Test Entry v2",
              content: "# Version 2",
              summary: "Summary v2",
              createdAt: new Date(),
              createdBy: userId,
              changeNote: "Restored from version 2",
            }),
          },
        };
        return callback(tx);
      });

      // Mock for findVersion call
      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(entryWithVersions);

      const result = await service.restoreVersion(
        workspaceId,
        slug,
        2,
        userId,
        "Custom restore note"
      );

      expect(result.title).toBe("Test Entry v2");
      expect(result.content).toBe("# Version 2");

      expect(mockLinkSyncService.syncLinks).toHaveBeenCalledWith(
        workspaceId,
        entryId,
        "# Version 2"
      );
    });

    it("should use default change note if not provided", async () => {
      const entryWithVersions = {
        ...mockEntry,
        versions: [mockVersions[0]],
        tags: [],
      };

      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(mockEntry);
      mockPrismaService.knowledgeEntryVersion.findUnique.mockResolvedValue(mockVersions[1]);

      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const createMock = vi.fn();
        const tx = {
          knowledgeEntry: {
            update: vi.fn().mockResolvedValue(mockEntry),
            findUnique: vi.fn().mockResolvedValue({ ...mockEntry, tags: [] }),
          },
          knowledgeEntryVersion: {
            create: createMock,
          },
        };
        await callback(tx);
        return { ...mockEntry, tags: [] };
      });

      mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(entryWithVersions);

      await service.restoreVersion(workspaceId, slug, 2, userId);

      // Verify transaction was called
      expect(mockPrismaService.$transaction).toHaveBeenCalled();
    });

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

      await expect(service.restoreVersion(workspaceId, slug, 2, userId)).rejects.toThrow(
        NotFoundException
      );
    });
  });
});
