import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ConflictException } from "@nestjs/common";
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";
import { RunnerJobStatus } from "@prisma/client";
import { CoordinatorJobStatus, UpdateJobStatusDto } from "./dto";

/**
 * Concurrency tests for CoordinatorIntegrationService
 * Focus on race conditions during coordinator job status updates
 */
describe("CoordinatorIntegrationService - Concurrency", () => {
  let service: CoordinatorIntegrationService;
  let prisma: PrismaService;

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

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

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CoordinatorIntegrationService,
        {
          provide: PrismaService,
          useValue: {
            runnerJob: {
              findUnique: vi.fn(),
              update: vi.fn(),
              updateMany: vi.fn(),
            },
            $transaction: vi.fn(),
            $queryRaw: vi.fn(),
          },
        },
        {
          provide: JobEventsService,
          useValue: mockJobEventsService,
        },
        {
          provide: HeraldService,
          useValue: mockHeraldService,
        },
        {
          provide: BullMqService,
          useValue: mockBullMqService,
        },
      ],
    }).compile();

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

    vi.clearAllMocks();
  });

  describe("concurrent status updates from coordinator", () => {
    it("should use SELECT FOR UPDATE to prevent race conditions", async () => {
      const jobId = "job-123";
      const dto: UpdateJobStatusDto = {
        status: CoordinatorJobStatus.RUNNING,
        agentId: "agent-1",
        agentType: "python",
      };

      const mockJob = {
        id: jobId,
        status: RunnerJobStatus.PENDING,
        workspaceId: "workspace-123",
        version: 1,
      };

      const updatedJob = {
        ...mockJob,
        status: RunnerJobStatus.RUNNING,
        startedAt: new Date(),
        version: 2,
      };

      // Mock transaction with SELECT FOR UPDATE
      const mockTxClient = {
        $queryRaw: vi.fn().mockResolvedValue([mockJob]),
        runnerJob: {
          update: vi.fn().mockResolvedValue(updatedJob),
        },
      };

      vi.mocked(prisma.$transaction).mockImplementation(async (callback: any) => {
        return callback(mockTxClient);
      });

      const mockEvent = {
        id: "event-1",
        jobId,
        type: "job.started",
        timestamp: new Date(),
      };

      vi.mocked(mockJobEventsService.emitJobStarted).mockResolvedValue(mockEvent as any);

      const result = await service.updateJobStatus(jobId, dto);

      expect(result.status).toBe(RunnerJobStatus.RUNNING);

      // Verify SELECT FOR UPDATE was used
      expect(mockTxClient.$queryRaw).toHaveBeenCalled();
      const sqlCall = mockTxClient.$queryRaw.mock.calls[0];
      expect(sqlCall).toBeDefined();
      // Verify the SQL contains FOR UPDATE (raw SQL is passed as template parts)
      const sqlString = sqlCall?.toString() ?? "";
      expect(
        sqlString.includes("FOR UPDATE") || sqlCall?.[0]?.toString().includes("FOR UPDATE")
      ).toBe(true);
    });

    it("should handle concurrent status updates by coordinator and API", async () => {
      const jobId = "job-123";

      // Coordinator tries to mark as RUNNING
      const coordinatorDto: UpdateJobStatusDto = {
        status: CoordinatorJobStatus.RUNNING,
      };

      // Simulate transaction lock timeout (another process holds lock)
      vi.mocked(prisma.$transaction).mockRejectedValue(new Error("could not obtain lock on row"));

      await expect(service.updateJobStatus(jobId, coordinatorDto)).rejects.toThrow();
    });

    it("should serialize concurrent status transitions", async () => {
      const jobId = "job-123";

      const mockJob = {
        id: jobId,
        status: RunnerJobStatus.PENDING,
        workspaceId: "workspace-123",
        version: 1,
      };

      // Simulate transaction that waits for lock, then proceeds
      const mockTxClient = {
        $queryRaw: vi.fn().mockResolvedValue([mockJob]),
        runnerJob: {
          update: vi.fn().mockResolvedValue({
            ...mockJob,
            status: RunnerJobStatus.RUNNING,
            version: 2,
          }),
        },
      };

      vi.mocked(prisma.$transaction).mockImplementation(async (callback: any) => {
        // Simulate delay while waiting for lock
        await new Promise((resolve) => setTimeout(resolve, 100));
        return callback(mockTxClient);
      });

      const dto: UpdateJobStatusDto = {
        status: CoordinatorJobStatus.RUNNING,
      };

      vi.mocked(mockJobEventsService.emitJobStarted).mockResolvedValue({
        id: "event-1",
        jobId,
        type: "job.started",
        timestamp: new Date(),
      } as any);

      const result = await service.updateJobStatus(jobId, dto);

      expect(result.status).toBe(RunnerJobStatus.RUNNING);
      expect(prisma.$transaction).toHaveBeenCalled();
    });
  });

  describe("concurrent completion from coordinator", () => {
    it("should prevent double completion using transaction", async () => {
      const jobId = "job-123";

      const mockJob = {
        id: jobId,
        status: RunnerJobStatus.RUNNING,
        workspaceId: "workspace-123",
        startedAt: new Date(),
        version: 2,
      };

      const completedJob = {
        ...mockJob,
        status: RunnerJobStatus.COMPLETED,
        completedAt: new Date(),
        progressPercent: 100,
        result: { success: true },
        version: 3,
      };

      const mockTxClient = {
        $queryRaw: vi.fn().mockResolvedValue([mockJob]),
        runnerJob: {
          update: vi.fn().mockResolvedValue(completedJob),
        },
      };

      vi.mocked(prisma.$transaction).mockImplementation(async (callback: any) => {
        return callback(mockTxClient);
      });

      vi.mocked(mockJobEventsService.emitJobCompleted).mockResolvedValue({
        id: "event-1",
        jobId,
        type: "job.completed",
        timestamp: new Date(),
      } as any);

      const result = await service.completeJob(jobId, {
        result: { success: true },
        tokensUsed: 1000,
        durationSeconds: 120,
      });

      expect(result.status).toBe(RunnerJobStatus.COMPLETED);
      expect(mockTxClient.$queryRaw).toHaveBeenCalled();
    });

    it("should handle concurrent completion and failure attempts", async () => {
      const jobId = "job-123";

      const mockJob = {
        id: jobId,
        status: RunnerJobStatus.RUNNING,
        workspaceId: "workspace-123",
        startedAt: new Date(),
        version: 2,
      };

      // First transaction (completion) succeeds
      const completedJob = {
        ...mockJob,
        status: RunnerJobStatus.COMPLETED,
        completedAt: new Date(),
        version: 3,
      };

      // Second transaction (failure) sees completed job and should fail
      const mockTxClient1 = {
        $queryRaw: vi.fn().mockResolvedValue([mockJob]),
        runnerJob: {
          update: vi.fn().mockResolvedValue(completedJob),
        },
      };

      const mockTxClient2 = {
        $queryRaw: vi.fn().mockResolvedValue([completedJob]), // Job already completed
        runnerJob: {
          update: vi.fn(),
        },
      };

      vi.mocked(prisma.$transaction)
        .mockImplementationOnce(async (callback: any) => callback(mockTxClient1))
        .mockImplementationOnce(async (callback: any) => callback(mockTxClient2));

      vi.mocked(mockJobEventsService.emitJobCompleted).mockResolvedValue({
        id: "event-1",
        jobId,
        type: "job.completed",
        timestamp: new Date(),
      } as any);

      // First call (completion) succeeds
      const result1 = await service.completeJob(jobId, {
        result: { success: true },
      });
      expect(result1.status).toBe(RunnerJobStatus.COMPLETED);

      // Second call (failure) should be rejected due to invalid status transition
      await expect(
        service.failJob(jobId, {
          error: "Something went wrong",
        })
      ).rejects.toThrow();
    });
  });

  describe("concurrent progress updates from coordinator", () => {
    it("should handle rapid progress updates safely", async () => {
      const jobId = "job-123";

      const progressUpdates = [25, 50, 75];

      for (const progress of progressUpdates) {
        const mockJob = {
          id: jobId,
          status: RunnerJobStatus.RUNNING,
          progressPercent: progress - 25,
          version: progress / 25, // version increases with each update
        };

        const updatedJob = {
          ...mockJob,
          progressPercent: progress,
          version: mockJob.version + 1,
        };

        // First findUnique returns the current job state
        vi.mocked(prisma.runnerJob.findUnique).mockResolvedValueOnce(mockJob as any);
        // updateMany succeeds
        vi.mocked(prisma.runnerJob.updateMany).mockResolvedValue({ count: 1 });
        // Second findUnique returns the updated job state
        vi.mocked(prisma.runnerJob.findUnique).mockResolvedValueOnce(updatedJob as any);

        const result = await service.updateJobProgress(jobId, {
          progressPercent: progress,
        });

        expect(result.progressPercent).toBe(progress);
      }

      expect(mockJobEventsService.emitEvent).toHaveBeenCalledTimes(3);
    });

    it("should detect version conflicts in progress updates", async () => {
      const jobId = "job-123";

      const mockJob = {
        id: jobId,
        status: RunnerJobStatus.RUNNING,
        progressPercent: 50,
        version: 2,
      };

      vi.mocked(prisma.runnerJob.findUnique).mockResolvedValue(mockJob as any);
      // Simulate version conflict (another update happened)
      vi.mocked(prisma.runnerJob.updateMany).mockResolvedValue({ count: 0 });

      await expect(
        service.updateJobProgress(jobId, {
          progressPercent: 75,
        })
      ).rejects.toThrow(ConflictException);
    });
  });

  describe("transaction isolation", () => {
    it("should use appropriate transaction isolation level", async () => {
      const jobId = "job-123";

      const mockJob = {
        id: jobId,
        status: RunnerJobStatus.PENDING,
        version: 1,
      };

      const mockTxClient = {
        $queryRaw: vi.fn().mockResolvedValue([mockJob]),
        runnerJob: {
          update: vi.fn().mockResolvedValue({
            ...mockJob,
            status: RunnerJobStatus.RUNNING,
            version: 2,
          }),
        },
      };

      vi.mocked(prisma.$transaction).mockImplementation(async (callback: any) => {
        return callback(mockTxClient);
      });

      vi.mocked(mockJobEventsService.emitJobStarted).mockResolvedValue({
        id: "event-1",
        jobId,
        type: "job.started",
        timestamp: new Date(),
      } as any);

      await service.updateJobStatus(jobId, {
        status: CoordinatorJobStatus.RUNNING,
      });

      // Verify transaction was used (isolates the operation)
      expect(prisma.$transaction).toHaveBeenCalled();
    });
  });
});
