import { beforeAll, beforeEach, describe, expect, it, afterAll, vi } from "vitest";
import { randomUUID as uuid } from "crypto";
import { Test, TestingModule } from "@nestjs/testing";
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { PrismaClient, Prisma } from "@prisma/client";
import { FindingsService } from "./findings.service";
import { PrismaService } from "../prisma/prisma.service";
import { EmbeddingService } from "../knowledge/services/embedding.service";

const shouldRunDbIntegrationTests =
  process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL);
const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip;

const EMBEDDING_DIMENSION = 1536;

function vector(value: number): number[] {
  return Array.from({ length: EMBEDDING_DIMENSION }, () => value);
}

function toVectorLiteral(input: number[]): string {
  return `[${input.join(",")}]`;
}

describeFn("FindingsService Integration", () => {
  let moduleRef: TestingModule;
  let prisma: PrismaClient;
  let service: FindingsService;
  let workspaceId: string;
  let ownerId: string;
  let setupComplete = false;

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

  beforeAll(async () => {
    prisma = new PrismaClient();
    await prisma.$connect();

    const workspace = await prisma.workspace.create({
      data: {
        name: `Findings Integration ${Date.now()}`,
        owner: {
          create: {
            email: `findings-integration-${Date.now()}@example.com`,
            name: "Findings Integration Owner",
          },
        },
      },
    });

    workspaceId = workspace.id;
    ownerId = workspace.ownerId;

    moduleRef = await Test.createTestingModule({
      providers: [
        FindingsService,
        {
          provide: PrismaService,
          useValue: prisma,
        },
        {
          provide: EmbeddingService,
          useValue: embeddingServiceMock,
        },
      ],
    }).compile();

    service = moduleRef.get<FindingsService>(FindingsService);
    setupComplete = true;
  });

  beforeEach(() => {
    vi.clearAllMocks();
    embeddingServiceMock.isConfigured.mockReturnValue(false);
  });

  afterAll(async () => {
    if (!prisma) {
      return;
    }

    if (workspaceId) {
      await prisma.finding.deleteMany({ where: { workspaceId } });
      await prisma.workspace.deleteMany({ where: { id: workspaceId } });
    }
    if (ownerId) {
      await prisma.user.deleteMany({ where: { id: ownerId } });
    }

    if (moduleRef) {
      await moduleRef.close();
    }
    await prisma.$disconnect();
  });

  it("creates, lists, fetches, and deletes findings", async () => {
    if (!setupComplete) {
      return;
    }

    const created = await service.create(workspaceId, {
      agentId: "agent-findings-crud",
      type: "security",
      title: "Unescaped SQL fragment",
      data: { severity: "high" },
      summary: "Potential injection risk in dynamic query path.",
    });

    expect(created.id).toBeDefined();
    expect(created.workspaceId).toBe(workspaceId);
    expect(created.taskId).toBeNull();

    const listed = await service.findAll(workspaceId, {
      page: 1,
      limit: 10,
      agentId: "agent-findings-crud",
    });

    expect(listed.meta.total).toBeGreaterThanOrEqual(1);
    expect(listed.data.some((row) => row.id === created.id)).toBe(true);

    const found = await service.findOne(created.id, workspaceId);
    expect(found.id).toBe(created.id);
    expect(found.title).toBe("Unescaped SQL fragment");

    await expect(service.findOne(created.id, uuid())).rejects.toThrow(NotFoundException);

    await expect(service.remove(created.id, workspaceId)).resolves.toEqual({
      message: "Finding deleted successfully",
    });

    await expect(service.findOne(created.id, workspaceId)).rejects.toThrow(NotFoundException);
  });

  it("rejects create when taskId does not exist in workspace", async () => {
    if (!setupComplete) {
      return;
    }

    await expect(
      service.create(workspaceId, {
        taskId: uuid(),
        agentId: "agent-findings-missing-task",
        type: "bug",
        title: "Invalid task id",
        data: { source: "integration-test" },
        summary: "Should fail when task relation is not found.",
      })
    ).rejects.toThrow(NotFoundException);
  });

  it("rejects vector search when embeddings are disabled", async () => {
    if (!setupComplete) {
      return;
    }

    embeddingServiceMock.isConfigured.mockReturnValue(false);

    await expect(
      service.search(workspaceId, {
        query: "security issue",
      })
    ).rejects.toThrow(BadRequestException);
  });

  it("searches findings by vector similarity with filters", async () => {
    if (!setupComplete) {
      return;
    }

    const near = vector(0.01);
    const far = vector(0.9);

    const matchedFinding = await prisma.finding.create({
      data: {
        workspaceId,
        agentId: "agent-findings-search-a",
        type: "incident",
        title: "Authentication bypass",
        data: { score: 0.9 } as Prisma.InputJsonValue,
        summary: "Bypass risk found in login checks.",
      },
    });

    const otherFinding = await prisma.finding.create({
      data: {
        workspaceId,
        agentId: "agent-findings-search-b",
        type: "incident",
        title: "Retry timeout",
        data: { score: 0.2 } as Prisma.InputJsonValue,
        summary: "Timeout issue in downstream retries.",
      },
    });

    await prisma.$executeRaw`
      UPDATE findings
      SET embedding = ${toVectorLiteral(near)}::vector(1536)
      WHERE id = ${matchedFinding.id}::uuid
    `;
    await prisma.$executeRaw`
      UPDATE findings
      SET embedding = ${toVectorLiteral(far)}::vector(1536)
      WHERE id = ${otherFinding.id}::uuid
    `;

    embeddingServiceMock.isConfigured.mockReturnValue(true);
    embeddingServiceMock.generateEmbedding.mockResolvedValue(near);

    const result = await service.search(workspaceId, {
      query: "authentication bypass risk",
      agentId: "agent-findings-search-a",
      limit: 10,
      similarityThreshold: 0,
    });

    expect(result.query).toBe("authentication bypass risk");
    expect(result.meta.total).toBe(1);
    expect(result.data).toHaveLength(1);
    expect(result.data[0]?.id).toBe(matchedFinding.id);
    expect(result.data[0]?.agentId).toBe("agent-findings-search-a");
    expect(result.data.find((row) => row.id === otherFinding.id)).toBeUndefined();
  });
});
