import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { StitcherService } from "./stitcher.service";
import { PrismaService } from "../prisma/prisma.service";
import { BullMqService } from "../bullmq/bullmq.service";
import { QUEUE_NAMES } from "../bullmq/queues";
import type { JobDispatchContext, JobDispatchResult } from "./interfaces";

describe("StitcherService", () => {
  let service: StitcherService;
  let prismaService: PrismaService;
  let bullMqService: BullMqService;

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

  const mockBullMqService = {
    addJob: vi.fn(),
    getQueue: vi.fn(),
  };

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

    service = module.get<StitcherService>(StitcherService);
    prismaService = module.get<PrismaService>(PrismaService);
    bullMqService = module.get<BullMqService>(BullMqService);

    vi.clearAllMocks();
  });

  describe("dispatchJob", () => {
    it("should create a RunnerJob and dispatch to queue", async () => {
      const context: JobDispatchContext = {
        workspaceId: "workspace-123",
        type: "code-task",
        priority: 10,
      };

      const mockJob = {
        id: "job-123",
        workspaceId: "workspace-123",
        type: "code-task",
        status: "PENDING",
        priority: 10,
        progressPercent: 0,
        createdAt: new Date(),
      };

      mockPrismaService.runnerJob.create.mockResolvedValue(mockJob);
      mockBullMqService.addJob.mockResolvedValue({ id: "queue-job-123" });

      const result = await service.dispatchJob(context);

      expect(result).toEqual<JobDispatchResult>({
        jobId: "job-123",
        queueName: QUEUE_NAMES.MAIN,
        status: "PENDING",
      });

      expect(mockPrismaService.runnerJob.create).toHaveBeenCalledWith({
        data: {
          workspaceId: "workspace-123",
          type: "code-task",
          priority: 10,
          status: "PENDING",
          progressPercent: 0,
        },
      });

      expect(mockBullMqService.addJob).toHaveBeenCalledWith(
        QUEUE_NAMES.MAIN,
        "code-task",
        expect.objectContaining({
          jobId: "job-123",
          workspaceId: "workspace-123",
        }),
        expect.objectContaining({
          priority: 10,
        })
      );
    });

    it("should log job event after dispatch", async () => {
      const context: JobDispatchContext = {
        workspaceId: "workspace-123",
        type: "git-status",
      };

      const mockJob = {
        id: "job-456",
        workspaceId: "workspace-123",
        type: "git-status",
        status: "PENDING",
        priority: 5,
        progressPercent: 0,
        createdAt: new Date(),
      };

      mockPrismaService.runnerJob.create.mockResolvedValue(mockJob);
      mockBullMqService.addJob.mockResolvedValue({ id: "queue-job-456" });

      await service.dispatchJob(context);

      expect(mockPrismaService.jobEvent.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          jobId: "job-456",
          type: "job.queued",
          actor: "stitcher",
        }),
      });
    });

    it("should handle dispatch errors", async () => {
      const context: JobDispatchContext = {
        workspaceId: "workspace-123",
        type: "invalid-type",
      };

      mockPrismaService.runnerJob.create.mockRejectedValue(new Error("Database error"));

      await expect(service.dispatchJob(context)).rejects.toThrow("Database error");
    });
  });

  describe("applyGuardRails", () => {
    it("should return allowed for valid capabilities", () => {
      const result = service.applyGuardRails("runner", ["read"]);

      expect(result.allowed).toBe(true);
    });

    it("should return not allowed for invalid capabilities", () => {
      const result = service.applyGuardRails("runner", ["write"]);

      expect(result.allowed).toBe(false);
      expect(result.reason).toBeDefined();
    });
  });

  describe("applyQualityRails", () => {
    it("should return required gates for code tasks", () => {
      const result = service.applyQualityRails("code-task");

      expect(result.required).toBe(true);
      expect(result.gates).toContain("lint");
      expect(result.gates).toContain("typecheck");
      expect(result.gates).toContain("test");
    });

    it("should return no gates for read-only tasks", () => {
      const result = service.applyQualityRails("git-status");

      expect(result.required).toBe(false);
      expect(result.gates).toHaveLength(0);
    });
  });

  describe("trackJobEvent", () => {
    it("should create job event in database", async () => {
      const mockEvent = {
        id: "event-123",
        jobId: "job-123",
        type: "job.started",
        timestamp: new Date(),
        actor: "stitcher",
        payload: {},
      };

      mockPrismaService.jobEvent.create.mockResolvedValue(mockEvent);

      await service.trackJobEvent("job-123", "job.started", "stitcher", {});

      expect(mockPrismaService.jobEvent.create).toHaveBeenCalledWith({
        data: {
          jobId: "job-123",
          type: "job.started",
          actor: "stitcher",
          timestamp: expect.any(Date),
          payload: {},
        },
      });
    });
  });
});
