import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { EntryStatus } from "@prisma/client";
import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";
import { SearchController } from "./search.controller";
import { SearchService } from "./services/search.service";
import { AuthGuard } from "../auth/guards/auth.guard";
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
import { SemanticSearchBodyDto, SemanticSearchQueryDto, HybridSearchBodyDto } from "./dto";

describe("SearchController", () => {
  let controller: SearchController;

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

  const mockSearchService = {
    search: vi.fn(),
    searchByTags: vi.fn(),
    recentEntries: vi.fn(),
    semanticSearch: vi.fn(),
    hybridSearch: vi.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [SearchController],
      providers: [
        {
          provide: SearchService,
          useValue: mockSearchService,
        },
      ],
    })
      .overrideGuard(AuthGuard)
      .useValue({ canActivate: () => true })
      .overrideGuard(WorkspaceGuard)
      .useValue({ canActivate: () => true })
      .overrideGuard(PermissionGuard)
      .useValue({ canActivate: () => true })
      .compile();

    controller = module.get<SearchController>(SearchController);

    vi.clearAllMocks();
  });

  describe("search", () => {
    it("should call searchService.search with correct parameters", async () => {
      const mockResult = {
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "test",
      };
      mockSearchService.search.mockResolvedValue(mockResult);

      const result = await controller.search(mockWorkspaceId, {
        q: "test",
        page: 1,
        limit: 20,
      });

      expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
        status: undefined,
        page: 1,
        limit: 20,
      });
      expect(result).toEqual(mockResult);
    });

    it("should pass status filter to service", async () => {
      mockSearchService.search.mockResolvedValue({
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "test",
      });

      await controller.search(mockWorkspaceId, {
        q: "test",
        status: EntryStatus.PUBLISHED,
      });

      expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
        status: EntryStatus.PUBLISHED,
        page: undefined,
        limit: undefined,
      });
    });

    it("should pass tags filter to service", async () => {
      mockSearchService.search.mockResolvedValue({
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "test",
      });

      await controller.search(mockWorkspaceId, {
        q: "test",
        tags: ["api", "documentation"],
      });

      expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
        status: undefined,
        page: undefined,
        limit: undefined,
        tags: ["api", "documentation"],
      });
    });

    it("should pass both status and tags filters to service", async () => {
      mockSearchService.search.mockResolvedValue({
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "test",
      });

      await controller.search(mockWorkspaceId, {
        q: "test",
        status: EntryStatus.PUBLISHED,
        tags: ["api"],
        page: 2,
        limit: 10,
      });

      expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
        status: EntryStatus.PUBLISHED,
        page: 2,
        limit: 10,
        tags: ["api"],
      });
    });
  });

  describe("searchByTags", () => {
    it("should call searchService.searchByTags with correct parameters", async () => {
      const mockResult = {
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
      };
      mockSearchService.searchByTags.mockResolvedValue(mockResult);

      const result = await controller.searchByTags(mockWorkspaceId, {
        tags: ["api", "documentation"],
        page: 1,
        limit: 20,
      });

      expect(mockSearchService.searchByTags).toHaveBeenCalledWith(
        ["api", "documentation"],
        mockWorkspaceId,
        {
          status: undefined,
          page: 1,
          limit: 20,
        }
      );
      expect(result).toEqual(mockResult);
    });

    it("should pass status filter to service", async () => {
      mockSearchService.searchByTags.mockResolvedValue({
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
      });

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

      expect(mockSearchService.searchByTags).toHaveBeenCalledWith(["api"], mockWorkspaceId, {
        status: EntryStatus.DRAFT,
        page: undefined,
        limit: undefined,
      });
    });
  });

  describe("recentEntries", () => {
    it("should call searchService.recentEntries with correct parameters", async () => {
      const mockEntries = [
        {
          id: "entry-1",
          title: "Recent Entry",
          slug: "recent-entry",
          tags: [],
        },
      ];
      mockSearchService.recentEntries.mockResolvedValue(mockEntries);

      const result = await controller.recentEntries(mockWorkspaceId, {
        limit: 10,
      });

      expect(mockSearchService.recentEntries).toHaveBeenCalledWith(mockWorkspaceId, 10, undefined);
      expect(result).toEqual({
        data: mockEntries,
        count: 1,
      });
    });

    it("should use default limit of 10", async () => {
      mockSearchService.recentEntries.mockResolvedValue([]);

      await controller.recentEntries(mockWorkspaceId, {});

      expect(mockSearchService.recentEntries).toHaveBeenCalledWith(mockWorkspaceId, 10, undefined);
    });

    it("should pass status filter to service", async () => {
      mockSearchService.recentEntries.mockResolvedValue([]);

      await controller.recentEntries(mockWorkspaceId, {
        status: EntryStatus.PUBLISHED,
        limit: 5,
      });

      expect(mockSearchService.recentEntries).toHaveBeenCalledWith(
        mockWorkspaceId,
        5,
        EntryStatus.PUBLISHED
      );
    });
  });

  describe("semanticSearch", () => {
    it("should call searchService.semanticSearch with correct parameters", async () => {
      const mockResult = {
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "machine learning",
      };
      mockSearchService.semanticSearch.mockResolvedValue(mockResult);

      const body = plainToInstance(SemanticSearchBodyDto, {
        query: "machine learning",
      });
      const query = plainToInstance(SemanticSearchQueryDto, {
        page: 1,
        limit: 20,
      });

      const result = await controller.semanticSearch(mockWorkspaceId, body, query);

      expect(mockSearchService.semanticSearch).toHaveBeenCalledWith(
        "machine learning",
        mockWorkspaceId,
        {
          status: undefined,
          page: 1,
          limit: 20,
        }
      );
      expect(result).toEqual(mockResult);
    });

    it("should pass status filter from body to service", async () => {
      mockSearchService.semanticSearch.mockResolvedValue({
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "test",
      });

      const body = plainToInstance(SemanticSearchBodyDto, {
        query: "test",
        status: EntryStatus.PUBLISHED,
      });
      const query = plainToInstance(SemanticSearchQueryDto, {});

      await controller.semanticSearch(mockWorkspaceId, body, query);

      expect(mockSearchService.semanticSearch).toHaveBeenCalledWith("test", mockWorkspaceId, {
        status: EntryStatus.PUBLISHED,
        page: undefined,
        limit: undefined,
      });
    });
  });

  describe("hybridSearch", () => {
    it("should call searchService.hybridSearch with correct parameters", async () => {
      const mockResult = {
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "deep learning",
      };
      mockSearchService.hybridSearch.mockResolvedValue(mockResult);

      const body = plainToInstance(HybridSearchBodyDto, {
        query: "deep learning",
      });
      const query = plainToInstance(SemanticSearchQueryDto, {
        page: 2,
        limit: 10,
      });

      const result = await controller.hybridSearch(mockWorkspaceId, body, query);

      expect(mockSearchService.hybridSearch).toHaveBeenCalledWith(
        "deep learning",
        mockWorkspaceId,
        {
          status: undefined,
          page: 2,
          limit: 10,
        }
      );
      expect(result).toEqual(mockResult);
    });

    it("should pass status filter from body to service", async () => {
      mockSearchService.hybridSearch.mockResolvedValue({
        data: [],
        pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
        query: "test",
      });

      const body = plainToInstance(HybridSearchBodyDto, {
        query: "test",
        status: EntryStatus.DRAFT,
      });
      const query = plainToInstance(SemanticSearchQueryDto, {});

      await controller.hybridSearch(mockWorkspaceId, body, query);

      expect(mockSearchService.hybridSearch).toHaveBeenCalledWith("test", mockWorkspaceId, {
        status: EntryStatus.DRAFT,
        page: undefined,
        limit: undefined,
      });
    });
  });
});

describe("SemanticSearchBodyDto validation", () => {
  it("should pass with valid query", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, { query: "test search" });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should pass with query and valid status", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, {
      query: "test search",
      status: EntryStatus.PUBLISHED,
    });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should fail when query is missing", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, {});
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const queryError = errors.find((e) => e.property === "query");
    expect(queryError).toBeDefined();
  });

  it("should fail when query is not a string", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, { query: 12345 });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const queryError = errors.find((e) => e.property === "query");
    expect(queryError).toBeDefined();
  });

  it("should fail when query exceeds 500 characters", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, {
      query: "a".repeat(501),
    });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const queryError = errors.find((e) => e.property === "query");
    expect(queryError).toBeDefined();
  });

  it("should pass when query is exactly 500 characters", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, {
      query: "a".repeat(500),
    });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should fail with invalid status value", async () => {
    const dto = plainToInstance(SemanticSearchBodyDto, {
      query: "test",
      status: "INVALID_STATUS",
    });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const statusError = errors.find((e) => e.property === "status");
    expect(statusError).toBeDefined();
  });
});

describe("HybridSearchBodyDto validation", () => {
  it("should pass with valid query", async () => {
    const dto = plainToInstance(HybridSearchBodyDto, { query: "test search" });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should pass with query and valid status", async () => {
    const dto = plainToInstance(HybridSearchBodyDto, {
      query: "hybrid search",
      status: EntryStatus.DRAFT,
    });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should fail when query is missing", async () => {
    const dto = plainToInstance(HybridSearchBodyDto, {});
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const queryError = errors.find((e) => e.property === "query");
    expect(queryError).toBeDefined();
  });

  it("should fail when query exceeds 500 characters", async () => {
    const dto = plainToInstance(HybridSearchBodyDto, {
      query: "a".repeat(501),
    });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const queryError = errors.find((e) => e.property === "query");
    expect(queryError).toBeDefined();
  });

  it("should fail with invalid status value", async () => {
    const dto = plainToInstance(HybridSearchBodyDto, {
      query: "test",
      status: "NOT_A_STATUS",
    });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const statusError = errors.find((e) => e.property === "status");
    expect(statusError).toBeDefined();
  });
});

describe("SemanticSearchQueryDto validation", () => {
  it("should pass with valid page and limit", async () => {
    const dto = plainToInstance(SemanticSearchQueryDto, { page: 1, limit: 20 });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should pass with no parameters (all optional)", async () => {
    const dto = plainToInstance(SemanticSearchQueryDto, {});
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it("should fail when page is less than 1", async () => {
    const dto = plainToInstance(SemanticSearchQueryDto, { page: 0 });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const pageError = errors.find((e) => e.property === "page");
    expect(pageError).toBeDefined();
  });

  it("should fail when limit exceeds 100", async () => {
    const dto = plainToInstance(SemanticSearchQueryDto, { limit: 101 });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const limitError = errors.find((e) => e.property === "limit");
    expect(limitError).toBeDefined();
  });

  it("should fail when limit is less than 1", async () => {
    const dto = plainToInstance(SemanticSearchQueryDto, { limit: 0 });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const limitError = errors.find((e) => e.property === "limit");
    expect(limitError).toBeDefined();
  });

  it("should fail when page is not an integer", async () => {
    const dto = plainToInstance(SemanticSearchQueryDto, { page: 1.5 });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    const pageError = errors.find((e) => e.property === "page");
    expect(pageError).toBeDefined();
  });
});
