import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { JobEventsService } from "./job-events.service";
import { PrismaService } from "../prisma/prisma.service";
import { NotFoundException } from "@nestjs/common";
import { JOB_CREATED, STEP_STARTED, AI_TOKENS_USED } from "./event-types";

describe("JobEventsService", () => {
  let service: JobEventsService;
  let prisma: PrismaService;

  const mockPrismaService = {
    runnerJob: {
      findUnique: vi.fn(),
    },
    jobStep: {
      findUnique: vi.fn(),
    },
    jobEvent: {
      create: vi.fn(),
      findMany: vi.fn(),
      count: vi.fn(),
    },
  };

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

    service = module.get<JobEventsService>(JobEventsService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe("emitEvent", () => {
    const jobId = "job-123";
    const mockEvent = {
      id: "event-123",
      jobId,
      stepId: null,
      type: JOB_CREATED,
      timestamp: new Date(),
      actor: "system",
      payload: { message: "Job created" },
    };

    it("should create a job event without stepId", async () => {
      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobEvent.create.mockResolvedValue(mockEvent);

      const result = await service.emitEvent(jobId, {
        type: JOB_CREATED,
        actor: "system",
        payload: { message: "Job created" },
      });

      expect(prisma.runnerJob.findUnique).toHaveBeenCalledWith({
        where: { id: jobId },
        select: { id: true },
      });
      expect(prisma.jobEvent.create).toHaveBeenCalledWith({
        data: {
          job: { connect: { id: jobId } },
          type: JOB_CREATED,
          timestamp: expect.any(Date),
          actor: "system",
          payload: { message: "Job created" },
        },
      });
      expect(result).toEqual(mockEvent);
    });

    it("should create a job event with stepId", async () => {
      const stepId = "step-123";
      const eventWithStep = { ...mockEvent, stepId, type: STEP_STARTED };

      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobStep.findUnique.mockResolvedValue({ id: stepId });
      mockPrismaService.jobEvent.create.mockResolvedValue(eventWithStep);

      const result = await service.emitEvent(jobId, {
        type: STEP_STARTED,
        actor: "system",
        payload: { stepName: "Setup" },
        stepId,
      });

      expect(prisma.jobStep.findUnique).toHaveBeenCalledWith({
        where: { id: stepId },
        select: { id: true },
      });
      expect(prisma.jobEvent.create).toHaveBeenCalledWith({
        data: {
          job: { connect: { id: jobId } },
          step: { connect: { id: stepId } },
          type: STEP_STARTED,
          timestamp: expect.any(Date),
          actor: "system",
          payload: { stepName: "Setup" },
        },
      });
      expect(result).toEqual(eventWithStep);
    });

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

      await expect(
        service.emitEvent(jobId, {
          type: JOB_CREATED,
          actor: "system",
          payload: {},
        })
      ).rejects.toThrow(NotFoundException);
    });

    it("should throw NotFoundException if step does not exist", async () => {
      const stepId = "step-invalid";

      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobStep.findUnique.mockResolvedValue(null);

      await expect(
        service.emitEvent(jobId, {
          type: STEP_STARTED,
          actor: "system",
          payload: {},
          stepId,
        })
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe("getEventsByJobId", () => {
    const jobId = "job-123";
    const mockEvents = [
      {
        id: "event-1",
        jobId,
        stepId: null,
        type: JOB_CREATED,
        timestamp: new Date("2026-01-01T10:00:00Z"),
        actor: "system",
        payload: {},
      },
      {
        id: "event-2",
        jobId,
        stepId: "step-1",
        type: STEP_STARTED,
        timestamp: new Date("2026-01-01T10:01:00Z"),
        actor: "system",
        payload: {},
      },
    ];

    it("should return paginated events for a job", async () => {
      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobEvent.findMany.mockResolvedValue(mockEvents);
      mockPrismaService.jobEvent.count.mockResolvedValue(2);

      const result = await service.getEventsByJobId(jobId, {});

      expect(prisma.runnerJob.findUnique).toHaveBeenCalledWith({
        where: { id: jobId },
        select: { id: true },
      });
      expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({
        where: { jobId },
        orderBy: { timestamp: "asc" },
        skip: 0,
        take: 50,
      });
      expect(prisma.jobEvent.count).toHaveBeenCalledWith({
        where: { jobId },
      });
      expect(result).toEqual({
        data: mockEvents,
        meta: {
          total: 2,
          page: 1,
          limit: 50,
          totalPages: 1,
        },
      });
    });

    it("should filter events by type", async () => {
      const filteredEvents = [mockEvents[0]];

      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobEvent.findMany.mockResolvedValue(filteredEvents);
      mockPrismaService.jobEvent.count.mockResolvedValue(1);

      const result = await service.getEventsByJobId(jobId, { type: JOB_CREATED });

      expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({
        where: { jobId, type: JOB_CREATED },
        orderBy: { timestamp: "asc" },
        skip: 0,
        take: 50,
      });
      expect(result.data).toHaveLength(1);
      expect(result.meta.total).toBe(1);
    });

    it("should filter events by stepId", async () => {
      const stepId = "step-1";
      const filteredEvents = [mockEvents[1]];

      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobEvent.findMany.mockResolvedValue(filteredEvents);
      mockPrismaService.jobEvent.count.mockResolvedValue(1);

      const result = await service.getEventsByJobId(jobId, { stepId });

      expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({
        where: { jobId, stepId },
        orderBy: { timestamp: "asc" },
        skip: 0,
        take: 50,
      });
      expect(result.data).toHaveLength(1);
    });

    it("should paginate results correctly", async () => {
      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobEvent.findMany.mockResolvedValue([mockEvents[1]]);
      mockPrismaService.jobEvent.count.mockResolvedValue(2);

      const result = await service.getEventsByJobId(jobId, { page: 2, limit: 1 });

      expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({
        where: { jobId },
        orderBy: { timestamp: "asc" },
        skip: 1,
        take: 1,
      });
      expect(result.data).toHaveLength(1);
      expect(result.meta.page).toBe(2);
      expect(result.meta.limit).toBe(1);
      expect(result.meta.totalPages).toBe(2);
    });

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

      await expect(service.getEventsByJobId(jobId, {})).rejects.toThrow(NotFoundException);
    });
  });

  describe("convenience methods", () => {
    const jobId = "job-123";

    beforeEach(() => {
      mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId });
      mockPrismaService.jobEvent.create.mockResolvedValue({
        id: "event-123",
        jobId,
        stepId: null,
        type: JOB_CREATED,
        timestamp: new Date(),
        actor: "system",
        payload: {},
      });
    });

    it("should emit job.created event", async () => {
      await service.emitJobCreated(jobId, { type: "code-task" });

      expect(prisma.jobEvent.create).toHaveBeenCalledWith({
        data: {
          job: { connect: { id: jobId } },
          type: JOB_CREATED,
          timestamp: expect.any(Date),
          actor: "system",
          payload: { type: "code-task" },
        },
      });
    });

    it("should emit job.started event", async () => {
      await service.emitJobStarted(jobId);

      expect(prisma.jobEvent.create).toHaveBeenCalledWith({
        data: {
          job: { connect: { id: jobId } },
          type: "job.started",
          timestamp: expect.any(Date),
          actor: "system",
          payload: {},
        },
      });
    });

    it("should emit step.started event", async () => {
      const stepId = "step-123";
      mockPrismaService.jobStep.findUnique.mockResolvedValue({ id: stepId });

      await service.emitStepStarted(jobId, stepId, { name: "Setup" });

      expect(prisma.jobEvent.create).toHaveBeenCalledWith({
        data: {
          job: { connect: { id: jobId } },
          step: { connect: { id: stepId } },
          type: STEP_STARTED,
          timestamp: expect.any(Date),
          actor: "system",
          payload: { name: "Setup" },
        },
      });
    });

    it("should emit ai.tokens_used event", async () => {
      await service.emitAiTokensUsed(jobId, { input: 100, output: 50 });

      expect(prisma.jobEvent.create).toHaveBeenCalledWith({
        data: {
          job: { connect: { id: jobId } },
          type: AI_TOKENS_USED,
          timestamp: expect.any(Date),
          actor: "system",
          payload: { input: 100, output: 50 },
        },
      });
    });
  });
});
