import { Test, TestingModule } from "@nestjs/testing";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { FindingsService } from "./findings.service";
import { PrismaService } from "../prisma/prisma.service";
import { EmbeddingService } from "../knowledge/services/embedding.service";

describe("FindingsService", () => {
  let service: FindingsService;
  let prisma: PrismaService;
  let embeddingService: EmbeddingService;

  const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
  const mockFindingId = "550e8400-e29b-41d4-a716-446655440002";
  const mockTaskId = "550e8400-e29b-41d4-a716-446655440003";

  const mockPrismaService = {
    finding: {
      create: vi.fn(),
      findMany: vi.fn(),
      findUnique: vi.fn(),
      count: vi.fn(),
      delete: vi.fn(),
    },
    agentTask: {
      findUnique: vi.fn(),
    },
    $queryRaw: vi.fn(),
    $executeRaw: vi.fn(),
  };

  const mockEmbeddingService = {
    isConfigured: vi.fn(),
    generateEmbedding: vi.fn(),
  };

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

    service = module.get<FindingsService>(FindingsService);
    prisma = module.get<PrismaService>(PrismaService);
    embeddingService = module.get<EmbeddingService>(EmbeddingService);

    vi.clearAllMocks();
  });

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

  describe("create", () => {
    it("should create a finding and store embedding when configured", async () => {
      const createDto = {
        taskId: mockTaskId,
        agentId: "research-agent",
        type: "security",
        title: "SQL injection risk",
        data: { severity: "high" },
        summary: "Potential SQL injection in search endpoint.",
      };

      const createdFinding = {
        id: mockFindingId,
        workspaceId: mockWorkspaceId,
        ...createDto,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrismaService.agentTask.findUnique.mockResolvedValue({
        id: mockTaskId,
        workspaceId: mockWorkspaceId,
      });
      mockPrismaService.finding.create.mockResolvedValue(createdFinding);
      mockPrismaService.finding.findUnique.mockResolvedValue(createdFinding);
      mockEmbeddingService.isConfigured.mockReturnValue(true);
      mockEmbeddingService.generateEmbedding.mockResolvedValue([0.1, 0.2, 0.3]);
      mockPrismaService.$executeRaw.mockResolvedValue(1);

      const result = await service.create(mockWorkspaceId, createDto);

      expect(result).toEqual(createdFinding);
      expect(prisma.finding.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          workspaceId: mockWorkspaceId,
          taskId: mockTaskId,
          agentId: "research-agent",
          type: "security",
          title: "SQL injection risk",
        }),
        select: expect.any(Object),
      });
      expect(embeddingService.generateEmbedding).toHaveBeenCalledWith(createDto.summary);
      expect(prisma.$executeRaw).toHaveBeenCalled();
    });

    it("should create a finding without embedding when not configured", async () => {
      const createDto = {
        agentId: "research-agent",
        type: "security",
        title: "SQL injection risk",
        data: { severity: "high" },
        summary: "Potential SQL injection in search endpoint.",
      };

      const createdFinding = {
        id: mockFindingId,
        workspaceId: mockWorkspaceId,
        taskId: null,
        ...createDto,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrismaService.finding.create.mockResolvedValue(createdFinding);
      mockEmbeddingService.isConfigured.mockReturnValue(false);

      const result = await service.create(mockWorkspaceId, createDto);

      expect(result).toEqual(createdFinding);
      expect(embeddingService.generateEmbedding).not.toHaveBeenCalled();
      expect(prisma.$executeRaw).not.toHaveBeenCalled();
    });
  });

  describe("findAll", () => {
    it("should return paginated findings with filters", async () => {
      const findings = [
        {
          id: mockFindingId,
          workspaceId: mockWorkspaceId,
          taskId: null,
          agentId: "research-agent",
          type: "security",
          title: "SQL injection risk",
          data: { severity: "high" },
          summary: "Potential SQL injection in search endpoint.",
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      mockPrismaService.finding.findMany.mockResolvedValue(findings);
      mockPrismaService.finding.count.mockResolvedValue(1);

      const result = await service.findAll(mockWorkspaceId, {
        page: 1,
        limit: 10,
        type: "security",
        agentId: "research-agent",
      });

      expect(result).toEqual({
        data: findings,
        meta: {
          total: 1,
          page: 1,
          limit: 10,
          totalPages: 1,
        },
      });
      expect(prisma.finding.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            type: "security",
            agentId: "research-agent",
          },
        })
      );
    });
  });

  describe("findOne", () => {
    it("should return a finding", async () => {
      const finding = {
        id: mockFindingId,
        workspaceId: mockWorkspaceId,
        taskId: null,
        agentId: "research-agent",
        type: "security",
        title: "SQL injection risk",
        data: { severity: "high" },
        summary: "Potential SQL injection in search endpoint.",
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrismaService.finding.findUnique.mockResolvedValue(finding);

      const result = await service.findOne(mockFindingId, mockWorkspaceId);

      expect(result).toEqual(finding);
      expect(prisma.finding.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockFindingId,
          workspaceId: mockWorkspaceId,
        },
        select: expect.any(Object),
      });
    });

    it("should throw when finding does not exist", async () => {
      mockPrismaService.finding.findUnique.mockResolvedValue(null);

      await expect(service.findOne(mockFindingId, mockWorkspaceId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("search", () => {
    it("should throw BadRequestException when embeddings are not configured", async () => {
      mockEmbeddingService.isConfigured.mockReturnValue(false);

      await expect(
        service.search(mockWorkspaceId, {
          query: "sql injection",
        })
      ).rejects.toThrow(BadRequestException);
    });

    it("should return similarity-ranked search results", async () => {
      mockEmbeddingService.isConfigured.mockReturnValue(true);
      mockEmbeddingService.generateEmbedding.mockResolvedValue([0.1, 0.2, 0.3]);
      mockPrismaService.$queryRaw
        .mockResolvedValueOnce([
          {
            id: mockFindingId,
            workspace_id: mockWorkspaceId,
            task_id: null,
            agent_id: "research-agent",
            type: "security",
            title: "SQL injection risk",
            data: { severity: "high" },
            summary: "Potential SQL injection in search endpoint.",
            created_at: new Date(),
            updated_at: new Date(),
            score: 0.91,
          },
        ])
        .mockResolvedValueOnce([{ count: BigInt(1) }]);

      const result = await service.search(mockWorkspaceId, {
        query: "sql injection",
        page: 1,
        limit: 5,
        similarityThreshold: 0.5,
      });

      expect(result.query).toBe("sql injection");
      expect(result.data).toHaveLength(1);
      expect(result.data[0].score).toBe(0.91);
      expect(result.meta.total).toBe(1);
      expect(prisma.$queryRaw).toHaveBeenCalledTimes(2);
    });
  });

  describe("remove", () => {
    it("should delete a finding", async () => {
      mockPrismaService.finding.findUnique.mockResolvedValue({
        id: mockFindingId,
        workspaceId: mockWorkspaceId,
      });
      mockPrismaService.finding.delete.mockResolvedValue({
        id: mockFindingId,
      });

      const result = await service.remove(mockFindingId, mockWorkspaceId);

      expect(result).toEqual({ message: "Finding deleted successfully" });
      expect(prisma.finding.delete).toHaveBeenCalledWith({
        where: {
          id: mockFindingId,
          workspaceId: mockWorkspaceId,
        },
      });
    });

    it("should throw when finding does not exist", async () => {
      mockPrismaService.finding.findUnique.mockResolvedValue(null);

      await expect(service.remove(mockFindingId, mockWorkspaceId)).rejects.toThrow(
        NotFoundException
      );
    });
  });
});
