import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { NotFoundException, BadRequestException } from "@nestjs/common";
import { RunnerJobStatus } from "@prisma/client";
import { CoordinatorIntegrationService } from "./coordinator-integration.service";
import { PrismaService } from "../prisma/prisma.service";
import { JobEventsService } from "../job-events/job-events.service";
import { HeraldService } from "../herald/herald.service";
import { BullMqService } from "../bullmq/bullmq.service";

describe("CoordinatorIntegrationService", () => {
  let service: CoordinatorIntegrationService;
  let prismaService: PrismaService;
  let jobEventsService: JobEventsService;
  let heraldService: HeraldService;
  let bullMqService: BullMqService;

  const mockWorkspace = {
    id: "workspace-123",
    name: "Test Workspace",
    slug: "test-workspace",
    settings: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockJob = {
    id: "job-123",
    workspaceId: "workspace-123",
    type: "code-task",
    status: RunnerJobStatus.PENDING,
    priority: 10,
    progressPercent: 0,
    agentTaskId: null,
    result: null,
    error: null,
    startedAt: null,
    completedAt: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockEvent = {
    id: "event-123",
    jobId: "job-123",
    stepId: null,
    type: "job.created",
    timestamp: new Date(),
    actor: "coordinator",
    payload: {},
  };

  const mockPrismaService = {
    workspace: {
      findUnique: vi.fn(),
    },
    runnerJob: {
      create: vi.fn(),
      findUnique: vi.fn(),
      update: vi.fn(),
      updateMany: vi.fn(),
    },
    $transaction: vi.fn(),
    $queryRaw: vi.fn(),
  };

  const mockJobEventsService = {
    emitEvent: vi.fn(),
    emitJobCreated: vi.fn(),
    emitJobStarted: vi.fn(),
    emitJobCompleted: vi.fn(),
    emitJobFailed: vi.fn(),
  };

  const mockHeraldService = {
    broadcastJobEvent: vi.fn(),
  };

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

  beforeEach(async () => {
    vi.clearAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CoordinatorIntegrationService,
        { provide: PrismaService, useValue: mockPrismaService },
        { provide: JobEventsService, useValue: mockJobEventsService },
        { provide: HeraldService, useValue: mockHeraldService },
        { provide: BullMqService, useValue: mockBullMqService },
      ],
    }).compile();

    service = module.get<CoordinatorIntegrationService>(CoordinatorIntegrationService);
    prismaService = module.get<PrismaService>(PrismaService);
    jobEventsService = module.get<JobEventsService>(JobEventsService);
    heraldService = module.get<HeraldService>(HeraldService);
    bullMqService = module.get<BullMqService>(BullMqService);

    // Set default mock return values
    mockPrismaService.runnerJob.updateMany.mockResolvedValue({ count: 1 });
  });

  describe("createJob", () => {
    it("should create a job and add it to the queue", async () => {
      const dto = {
        workspaceId: "workspace-123",
        type: "code-task",
        issueNumber: 42,
        repository: "mosaic/stack",
        priority: 10,
        metadata: { assignedAgent: "sonnet" },
      };

      mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace);
      mockPrismaService.runnerJob.create.mockResolvedValue(mockJob);
      mockJobEventsService.emitJobCreated.mockResolvedValue(mockEvent);
      mockBullMqService.addJob.mockResolvedValue({ id: "bullmq-job-123" });

      const result = await service.createJob(dto);

      expect(result).toHaveProperty("jobId", mockJob.id);
      expect(result).toHaveProperty("status", "PENDING");
      expect(mockPrismaService.runnerJob.create).toHaveBeenCalled();
      expect(mockJobEventsService.emitJobCreated).toHaveBeenCalledWith(
        mockJob.id,
        expect.any(Object)
      );
      expect(mockBullMqService.addJob).toHaveBeenCalled();
    });

    it("should throw NotFoundException if workspace does not exist", async () => {
      const dto = {
        workspaceId: "non-existent",
        type: "code-task",
        issueNumber: 42,
        repository: "mosaic/stack",
      };

      mockPrismaService.workspace.findUnique.mockResolvedValue(null);

      await expect(service.createJob(dto)).rejects.toThrow(NotFoundException);
    });
  });

  describe("updateJobStatus", () => {
    it("should update job status to RUNNING", async () => {
      const updatedJob = { ...mockJob, status: RunnerJobStatus.RUNNING, startedAt: new Date() };

      // Mock transaction that passes through the callback
      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const mockTx = {
          $queryRaw: vi.fn().mockResolvedValue([
            {
              id: mockJob.id,
              status: mockJob.status,
              workspace_id: mockJob.workspaceId,
              version: 1,
            },
          ]),
          runnerJob: {
            update: vi.fn().mockResolvedValue(updatedJob),
          },
        };
        return callback(mockTx);
      });

      mockJobEventsService.emitJobStarted.mockResolvedValue(mockEvent);
      mockHeraldService.broadcastJobEvent.mockResolvedValue(undefined);

      const result = await service.updateJobStatus("job-123", {
        status: "RUNNING" as const,
        agentId: "agent-42",
      });

      expect(result.status).toBe(RunnerJobStatus.RUNNING);
      expect(mockJobEventsService.emitJobStarted).toHaveBeenCalled();
    });

    it("should throw NotFoundException if job does not exist", async () => {
      // Mock transaction with empty result
      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const mockTx = {
          $queryRaw: vi.fn().mockResolvedValue([]),
          runnerJob: {
            update: vi.fn(),
          },
        };
        return callback(mockTx);
      });

      await expect(
        service.updateJobStatus("non-existent", { status: "RUNNING" as const })
      ).rejects.toThrow(NotFoundException);
    });

    it("should throw BadRequestException for invalid status transition", async () => {
      // Mock transaction with completed job
      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const mockTx = {
          $queryRaw: vi.fn().mockResolvedValue([
            {
              id: mockJob.id,
              status: RunnerJobStatus.COMPLETED,
              workspace_id: mockJob.workspaceId,
              version: 1,
            },
          ]),
          runnerJob: {
            update: vi.fn(),
          },
        };
        return callback(mockTx);
      });

      await expect(
        service.updateJobStatus("job-123", { status: "RUNNING" as const })
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe("updateJobProgress", () => {
    it("should update job progress percentage", async () => {
      const runningJob = { ...mockJob, status: RunnerJobStatus.RUNNING, version: 1 };
      const updatedJob = { ...runningJob, progressPercent: 50, version: 2 };

      mockPrismaService.runnerJob.findUnique.mockResolvedValue(runningJob);
      mockPrismaService.runnerJob.updateMany.mockResolvedValue({ count: 1 });
      mockPrismaService.runnerJob.findUnique.mockResolvedValue(updatedJob);
      mockJobEventsService.emitEvent.mockResolvedValue(mockEvent);

      const result = await service.updateJobProgress("job-123", {
        progressPercent: 50,
        currentStep: "Running tests",
      });

      expect(result.progressPercent).toBe(50);
      expect(mockJobEventsService.emitEvent).toHaveBeenCalledWith(
        "job-123",
        expect.objectContaining({ type: "job.progress" })
      );
    });

    it("should throw BadRequestException if job is not running", async () => {
      mockPrismaService.runnerJob.findUnique.mockResolvedValue(mockJob);

      await expect(service.updateJobProgress("job-123", { progressPercent: 50 })).rejects.toThrow(
        BadRequestException
      );
    });
  });

  describe("completeJob", () => {
    it("should mark job as completed and broadcast", async () => {
      const runningJob = { ...mockJob, status: RunnerJobStatus.RUNNING, startedAt: new Date() };
      const completedJob = {
        ...runningJob,
        status: RunnerJobStatus.COMPLETED,
        progressPercent: 100,
        completedAt: new Date(),
      };

      // Mock transaction with running job
      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const mockTx = {
          $queryRaw: vi.fn().mockResolvedValue([
            {
              id: mockJob.id,
              status: RunnerJobStatus.RUNNING,
              workspace_id: mockJob.workspaceId,
              version: 1,
            },
          ]),
          runnerJob: {
            update: vi.fn().mockResolvedValue(completedJob),
          },
        };
        return callback(mockTx);
      });

      mockJobEventsService.emitJobCompleted.mockResolvedValue(mockEvent);
      mockHeraldService.broadcastJobEvent.mockResolvedValue(undefined);

      const result = await service.completeJob("job-123", {
        result: { commitSha: "abc123" },
      });

      expect(result.status).toBe(RunnerJobStatus.COMPLETED);
      expect(result.progressPercent).toBe(100);
      expect(mockJobEventsService.emitJobCompleted).toHaveBeenCalled();
      expect(mockHeraldService.broadcastJobEvent).toHaveBeenCalled();
    });
  });

  describe("failJob", () => {
    it("should mark job as failed and broadcast", async () => {
      const runningJob = { ...mockJob, status: RunnerJobStatus.RUNNING };
      const failedJob = {
        ...runningJob,
        status: RunnerJobStatus.FAILED,
        error: "Test failed",
        completedAt: new Date(),
      };

      // Mock transaction with running job
      mockPrismaService.$transaction.mockImplementation(async (callback) => {
        const mockTx = {
          $queryRaw: vi.fn().mockResolvedValue([
            {
              id: mockJob.id,
              status: RunnerJobStatus.RUNNING,
              workspace_id: mockJob.workspaceId,
              version: 1,
            },
          ]),
          runnerJob: {
            update: vi.fn().mockResolvedValue(failedJob),
          },
        };
        return callback(mockTx);
      });

      mockJobEventsService.emitJobFailed.mockResolvedValue(mockEvent);
      mockHeraldService.broadcastJobEvent.mockResolvedValue(undefined);

      const result = await service.failJob("job-123", {
        error: "Test failed",
        gateResults: { lint: false, test: false },
      });

      expect(result.status).toBe(RunnerJobStatus.FAILED);
      expect(result.error).toBe("Test failed");
      expect(mockJobEventsService.emitJobFailed).toHaveBeenCalled();
      expect(mockHeraldService.broadcastJobEvent).toHaveBeenCalled();
    });
  });

  describe("getIntegrationHealth", () => {
    it("should return health status with all components", async () => {
      mockBullMqService.getHealthStatus.mockResolvedValue({
        connected: true,
        queues: { main: 5, runner: 2 },
      });

      const result = await service.getIntegrationHealth();

      expect(result).toHaveProperty("api", true);
      expect(result).toHaveProperty("bullmq");
      expect(result.bullmq.connected).toBe(true);
    });

    it("should handle BullMQ health check failure gracefully", async () => {
      mockBullMqService.getHealthStatus.mockRejectedValue(new Error("Connection failed"));

      const result = await service.getIntegrationHealth();

      expect(result.api).toBe(true);
      expect(result.bullmq.connected).toBe(false);
    });
  });

  describe("getJobDetails", () => {
    it("should return job with events and steps", async () => {
      const jobWithDetails = {
        ...mockJob,
        steps: [],
        events: [mockEvent],
      };

      mockPrismaService.runnerJob.findUnique.mockResolvedValue(jobWithDetails);

      const result = await service.getJobDetails("job-123");

      expect(result).toHaveProperty("id", "job-123");
      expect(result).toHaveProperty("events");
      expect(result).toHaveProperty("steps");
    });

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

      await expect(service.getJobDetails("non-existent")).rejects.toThrow(NotFoundException);
    });
  });
});
