import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { EntryStatus } from "@prisma/client";
import { SearchService } from "./search.service";
import { PrismaService } from "../../prisma/prisma.service";
import { KnowledgeCacheService } from "./cache.service";
import { OllamaEmbeddingService } from "./ollama-embedding.service";

describe("SearchService", () => {
  let service: SearchService;
  let prismaService: any;

  const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440000";

  beforeEach(async () => {
    const mockQueryRaw = vi.fn();
    const mockKnowledgeEntryCount = vi.fn();
    const mockKnowledgeEntryFindMany = vi.fn();
    const mockKnowledgeEntryTagFindMany = vi.fn();

    const mockPrismaService = {
      $queryRaw: mockQueryRaw,
      knowledgeEntry: {
        count: mockKnowledgeEntryCount,
        findMany: mockKnowledgeEntryFindMany,
      },
      knowledgeEntryTag: {
        findMany: mockKnowledgeEntryTagFindMany,
      },
    };

    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 mockOllamaEmbeddingService = {
      isConfigured: vi.fn().mockResolvedValue(false),
      generateEmbedding: vi.fn().mockResolvedValue([]),
      generateAndStoreEmbedding: vi.fn().mockResolvedValue(undefined),
      batchGenerateEmbeddings: vi.fn().mockResolvedValue(0),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SearchService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
        {
          provide: KnowledgeCacheService,
          useValue: mockCacheService,
        },
        {
          provide: OllamaEmbeddingService,
          useValue: mockOllamaEmbeddingService,
        },
      ],
    }).compile();

    service = module.get<SearchService>(SearchService);
    prismaService = module.get<PrismaService>(PrismaService);
  });

  describe("search", () => {
    it("should return empty results for empty query", async () => {
      const result = await service.search("", mockWorkspaceId);

      expect(result.data).toEqual([]);
      expect(result.pagination.total).toBe(0);
      expect(result.query).toBe("");
    });

    it("should return empty results for whitespace-only query", async () => {
      const result = await service.search("   ", mockWorkspaceId);

      expect(result.data).toEqual([]);
      expect(result.pagination.total).toBe(0);
    });

    it("should perform full-text search and return ranked results", async () => {
      const mockSearchResults = [
        {
          id: "entry-1",
          workspace_id: mockWorkspaceId,
          slug: "test-entry",
          title: "Test Entry",
          content: "This is test content",
          content_html: "<p>This is test content</p>",
          summary: "Test summary",
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          created_at: new Date(),
          updated_at: new Date(),
          created_by: "user-1",
          updated_by: "user-1",
          rank: 0.5,
          headline: "This is <mark>test</mark> content",
        },
      ];

      prismaService.$queryRaw
        .mockResolvedValueOnce(mockSearchResults)
        .mockResolvedValueOnce([{ count: BigInt(1) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([
        {
          entryId: "entry-1",
          tag: {
            id: "tag-1",
            name: "Documentation",
            slug: "documentation",
            color: "#blue",
          },
        },
      ]);

      const result = await service.search("test", mockWorkspaceId);

      expect(result.data).toHaveLength(1);
      expect(result.data[0].title).toBe("Test Entry");
      expect(result.data[0].rank).toBe(0.5);
      expect(result.data[0].headline).toBe("This is <mark>test</mark> content");
      expect(result.data[0].tags).toHaveLength(1);
      expect(result.pagination.total).toBe(1);
      expect(result.query).toBe("test");
    });

    it("should sanitize search query removing special characters", async () => {
      prismaService.$queryRaw
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ count: BigInt(0) }]);
      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      await service.search("test & query | !special:chars*", mockWorkspaceId);

      // Should have been called with sanitized query
      expect(prismaService.$queryRaw).toHaveBeenCalled();
    });

    it("should apply status filter when provided", async () => {
      prismaService.$queryRaw
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ count: BigInt(0) }]);
      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      await service.search("test", mockWorkspaceId, {
        status: EntryStatus.DRAFT,
      });

      expect(prismaService.$queryRaw).toHaveBeenCalled();
    });

    it("should handle pagination correctly", async () => {
      prismaService.$queryRaw
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ count: BigInt(50) }]);
      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      const result = await service.search("test", mockWorkspaceId, {
        page: 2,
        limit: 10,
      });

      expect(result.pagination.page).toBe(2);
      expect(result.pagination.limit).toBe(10);
      expect(result.pagination.total).toBe(50);
      expect(result.pagination.totalPages).toBe(5);
    });

    it("should filter by tags when provided", async () => {
      const mockSearchResults = [
        {
          id: "entry-1",
          workspace_id: mockWorkspaceId,
          slug: "tagged-entry",
          title: "Tagged Entry",
          content: "Content with search term",
          content_html: "<p>Content with search term</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          created_at: new Date(),
          updated_at: new Date(),
          created_by: "user-1",
          updated_by: "user-1",
          rank: 0.8,
          headline: "Content with <mark>search term</mark>",
        },
      ];

      prismaService.$queryRaw
        .mockResolvedValueOnce(mockSearchResults)
        .mockResolvedValueOnce([{ count: BigInt(1) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([
        {
          entryId: "entry-1",
          tag: {
            id: "tag-1",
            name: "API",
            slug: "api",
            color: "#blue",
          },
        },
      ]);

      const result = await service.search("search term", mockWorkspaceId, {
        tags: ["api", "documentation"],
      });

      expect(result.data).toHaveLength(1);
      expect(result.data[0].title).toBe("Tagged Entry");
      expect(result.data[0].tags).toHaveLength(1);
      expect(prismaService.$queryRaw).toHaveBeenCalled();
    });

    it("should combine full-text search with tag filtering", async () => {
      prismaService.$queryRaw
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ count: BigInt(0) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      await service.search("test query", mockWorkspaceId, {
        tags: ["api"],
        status: EntryStatus.PUBLISHED,
        page: 1,
        limit: 20,
      });

      // Verify the query was called (the actual SQL logic will be tested in integration tests)
      expect(prismaService.$queryRaw).toHaveBeenCalled();
    });
  });

  describe("searchByTags", () => {
    it("should return empty results for empty tags array", async () => {
      const result = await service.searchByTags([], mockWorkspaceId);

      expect(result.data).toEqual([]);
      expect(result.pagination.total).toBe(0);
    });

    it("should find entries with all specified tags", async () => {
      const mockEntries = [
        {
          id: "entry-1",
          workspaceId: mockWorkspaceId,
          slug: "tagged-entry",
          title: "Tagged Entry",
          content: "Content with tags",
          contentHtml: "<p>Content with tags</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          createdAt: new Date(),
          updatedAt: new Date(),
          createdBy: "user-1",
          updatedBy: "user-1",
          tags: [
            {
              tag: {
                id: "tag-1",
                name: "API",
                slug: "api",
                color: "#blue",
              },
            },
            {
              tag: {
                id: "tag-2",
                name: "Documentation",
                slug: "documentation",
                color: "#green",
              },
            },
          ],
        },
      ];

      prismaService.knowledgeEntry.count.mockResolvedValue(1);
      prismaService.knowledgeEntry.findMany.mockResolvedValue(mockEntries);

      const result = await service.searchByTags(["api", "documentation"], mockWorkspaceId);

      expect(result.data).toHaveLength(1);
      expect(result.data[0].title).toBe("Tagged Entry");
      expect(result.data[0].tags).toHaveLength(2);
      expect(result.pagination.total).toBe(1);
    });

    it("should apply status filter when provided", async () => {
      prismaService.knowledgeEntry.count.mockResolvedValue(0);
      prismaService.knowledgeEntry.findMany.mockResolvedValue([]);

      await service.searchByTags(["api"], mockWorkspaceId, {
        status: EntryStatus.DRAFT,
      });

      expect(prismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            status: EntryStatus.DRAFT,
          }),
        })
      );
    });

    it("should handle pagination correctly", async () => {
      prismaService.knowledgeEntry.count.mockResolvedValue(25);
      prismaService.knowledgeEntry.findMany.mockResolvedValue([]);

      const result = await service.searchByTags(["api"], mockWorkspaceId, {
        page: 2,
        limit: 10,
      });

      expect(result.pagination.page).toBe(2);
      expect(result.pagination.limit).toBe(10);
      expect(result.pagination.total).toBe(25);
      expect(result.pagination.totalPages).toBe(3);
    });
  });

  describe("recentEntries", () => {
    it("should return recently modified entries", async () => {
      const mockEntries = [
        {
          id: "entry-1",
          workspaceId: mockWorkspaceId,
          slug: "recent-entry",
          title: "Recent Entry",
          content: "Recently updated content",
          contentHtml: "<p>Recently updated content</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          createdAt: new Date(),
          updatedAt: new Date(),
          createdBy: "user-1",
          updatedBy: "user-1",
          tags: [],
        },
      ];

      prismaService.knowledgeEntry.findMany.mockResolvedValue(mockEntries);

      const result = await service.recentEntries(mockWorkspaceId);

      expect(result).toHaveLength(1);
      expect(result[0].title).toBe("Recent Entry");
      expect(prismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          orderBy: { updatedAt: "desc" },
          take: 10,
        })
      );
    });

    it("should respect the limit parameter", async () => {
      prismaService.knowledgeEntry.findMany.mockResolvedValue([]);

      await service.recentEntries(mockWorkspaceId, 5);

      expect(prismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          take: 5,
        })
      );
    });

    it("should apply status filter when provided", async () => {
      prismaService.knowledgeEntry.findMany.mockResolvedValue([]);

      await service.recentEntries(mockWorkspaceId, 10, EntryStatus.DRAFT);

      expect(prismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            status: EntryStatus.DRAFT,
          }),
        })
      );
    });

    it("should exclude archived entries by default", async () => {
      prismaService.knowledgeEntry.findMany.mockResolvedValue([]);

      await service.recentEntries(mockWorkspaceId);

      expect(prismaService.knowledgeEntry.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            status: { not: EntryStatus.ARCHIVED },
          }),
        })
      );
    });
  });

  describe("semanticSearch", () => {
    it("should throw error when OllamaEmbeddingService is not configured", async () => {
      const ollamaService = service["ollama"];
      ollamaService.isConfigured = vi.fn().mockResolvedValue(false);

      await expect(service.semanticSearch("test query", mockWorkspaceId)).rejects.toThrow(
        "Semantic search requires Ollama to be configured"
      );
    });

    it("should perform semantic search using vector similarity", async () => {
      const ollamaService = service["ollama"];
      ollamaService.isConfigured = vi.fn().mockResolvedValue(true);

      // Mock embedding generation
      const mockEmbedding = new Array(1536).fill(0.1);
      ollamaService.generateEmbedding = vi.fn().mockResolvedValue(mockEmbedding);

      const mockSearchResults = [
        {
          id: "entry-1",
          workspace_id: mockWorkspaceId,
          slug: "semantic-entry",
          title: "Semantic Entry",
          content: "This is semantically similar content",
          content_html: "<p>This is semantically similar content</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          created_at: new Date(),
          updated_at: new Date(),
          created_by: "user-1",
          updated_by: "user-1",
          rank: 0.85,
          headline: null,
        },
      ];

      prismaService.$queryRaw
        .mockResolvedValueOnce(mockSearchResults)
        .mockResolvedValueOnce([{ count: BigInt(1) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      const result = await service.semanticSearch("semantic query", mockWorkspaceId);

      expect(result.data).toHaveLength(1);
      expect(result.data[0].rank).toBe(0.85);
      expect(ollamaService.generateEmbedding).toHaveBeenCalledWith("semantic query", {});
      expect(prismaService.$queryRaw).toHaveBeenCalled();
    });

    it("should apply similarity threshold filter", async () => {
      const ollamaService = service["ollama"];
      ollamaService.isConfigured = vi.fn().mockResolvedValue(true);

      const mockEmbedding = new Array(1536).fill(0.1);
      ollamaService.generateEmbedding = vi.fn().mockResolvedValue(mockEmbedding);

      // Set environment variable for similarity threshold
      process.env.SEMANTIC_SEARCH_SIMILARITY_THRESHOLD = "0.7";

      const mockSearchResults = [
        {
          id: "entry-1",
          workspace_id: mockWorkspaceId,
          slug: "high-similarity",
          title: "High Similarity Entry",
          content: "Very similar content",
          content_html: "<p>Very similar content</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          created_at: new Date(),
          updated_at: new Date(),
          created_by: "user-1",
          updated_by: "user-1",
          rank: 0.9,
          headline: null,
        },
      ];

      prismaService.$queryRaw
        .mockResolvedValueOnce(mockSearchResults)
        .mockResolvedValueOnce([{ count: BigInt(1) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      const result = await service.semanticSearch("query", mockWorkspaceId);

      expect(result.data).toHaveLength(1);
      expect(result.data[0].rank).toBeGreaterThanOrEqual(0.7);

      // Clean up
      delete process.env.SEMANTIC_SEARCH_SIMILARITY_THRESHOLD;
    });

    it("should handle pagination correctly", async () => {
      const ollamaService = service["ollama"];
      ollamaService.isConfigured = vi.fn().mockResolvedValue(true);

      const mockEmbedding = new Array(1536).fill(0.1);
      ollamaService.generateEmbedding = vi.fn().mockResolvedValue(mockEmbedding);

      prismaService.$queryRaw
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ count: BigInt(25) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      const result = await service.semanticSearch("query", mockWorkspaceId, {
        page: 2,
        limit: 10,
      });

      expect(result.pagination.page).toBe(2);
      expect(result.pagination.limit).toBe(10);
      expect(result.pagination.total).toBe(25);
      expect(result.pagination.totalPages).toBe(3);
    });

    it("should apply status filter when provided", async () => {
      const ollamaService = service["ollama"];
      ollamaService.isConfigured = vi.fn().mockResolvedValue(true);

      const mockEmbedding = new Array(1536).fill(0.1);
      ollamaService.generateEmbedding = vi.fn().mockResolvedValue(mockEmbedding);

      prismaService.$queryRaw
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ count: BigInt(0) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      await service.semanticSearch("query", mockWorkspaceId, {
        status: EntryStatus.DRAFT,
      });

      // Verify the query was called with status filter
      expect(prismaService.$queryRaw).toHaveBeenCalled();
    });

    it("should include similarity scores in results", async () => {
      const ollamaService = service["ollama"];
      ollamaService.isConfigured = vi.fn().mockResolvedValue(true);

      const mockEmbedding = new Array(1536).fill(0.1);
      ollamaService.generateEmbedding = vi.fn().mockResolvedValue(mockEmbedding);

      const mockSearchResults = [
        {
          id: "entry-1",
          workspace_id: mockWorkspaceId,
          slug: "entry-1",
          title: "Entry 1",
          content: "Content 1",
          content_html: "<p>Content 1</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          created_at: new Date(),
          updated_at: new Date(),
          created_by: "user-1",
          updated_by: "user-1",
          rank: 0.95,
          headline: null,
        },
        {
          id: "entry-2",
          workspace_id: mockWorkspaceId,
          slug: "entry-2",
          title: "Entry 2",
          content: "Content 2",
          content_html: "<p>Content 2</p>",
          summary: null,
          status: EntryStatus.PUBLISHED,
          visibility: "WORKSPACE",
          created_at: new Date(),
          updated_at: new Date(),
          created_by: "user-1",
          updated_by: "user-1",
          rank: 0.75,
          headline: null,
        },
      ];

      prismaService.$queryRaw
        .mockResolvedValueOnce(mockSearchResults)
        .mockResolvedValueOnce([{ count: BigInt(2) }]);

      prismaService.knowledgeEntryTag.findMany.mockResolvedValue([]);

      const result = await service.semanticSearch("query", mockWorkspaceId);

      expect(result.data).toHaveLength(2);
      expect(result.data[0].rank).toBe(0.95);
      expect(result.data[1].rank).toBe(0.75);
      // Verify results are ordered by similarity (descending)
      expect(result.data[0].rank).toBeGreaterThan(result.data[1].rank);
    });
  });
});
