import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { RunnerJobsController } from "./runner-jobs.controller";
import { RunnerJobsService } from "./runner-jobs.service";
import { RunnerJobStatus } from "@prisma/client";
import { CreateJobDto, QueryJobsDto } from "./dto";
import type { AuthenticatedUser } from "../common/types/user.types";
import { AuthGuard } from "../auth/guards/auth.guard";
import { WorkspaceGuard } from "../common/guards/workspace.guard";
import { PermissionGuard } from "../common/guards/permission.guard";
import { ExecutionContext } from "@nestjs/common";

describe("RunnerJobsController", () => {
  let controller: RunnerJobsController;
  let service: RunnerJobsService;

  const mockRunnerJobsService = {
    create: vi.fn(),
    findAll: vi.fn(),
    findOne: vi.fn(),
    cancel: vi.fn(),
    retry: vi.fn(),
    streamEvents: vi.fn(),
  };

  const mockAuthGuard = {
    canActivate: vi.fn((context: ExecutionContext) => {
      const request = context.switchToHttp().getRequest();
      request.user = {
        id: "user-123",
        workspaceId: "workspace-123",
      };
      return true;
    }),
  };

  const mockWorkspaceGuard = {
    canActivate: vi.fn(() => true),
  };

  const mockPermissionGuard = {
    canActivate: vi.fn(() => true),
  };

  const mockUser: AuthenticatedUser = {
    id: "user-123",
    email: "test@example.com",
    name: "Test User",
    emailVerified: true,
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [RunnerJobsController],
      providers: [
        {
          provide: RunnerJobsService,
          useValue: mockRunnerJobsService,
        },
      ],
    })
      .overrideGuard(AuthGuard)
      .useValue(mockAuthGuard)
      .overrideGuard(WorkspaceGuard)
      .useValue(mockWorkspaceGuard)
      .overrideGuard(PermissionGuard)
      .useValue(mockPermissionGuard)
      .compile();

    controller = module.get<RunnerJobsController>(RunnerJobsController);
    service = module.get<RunnerJobsService>(RunnerJobsService);

    // Clear all mocks before each test
    vi.clearAllMocks();
  });

  it("should be defined", () => {
    expect(controller).toBeDefined();
  });

  describe("create", () => {
    it("should create a new runner job", async () => {
      const workspaceId = "workspace-123";
      const createDto: CreateJobDto = {
        type: "git-status",
        priority: 5,
        data: { repo: "test-repo" },
      };

      const mockJob = {
        id: "job-123",
        workspaceId,
        type: "git-status",
        status: RunnerJobStatus.PENDING,
        priority: 5,
        progressPercent: 0,
        result: { repo: "test-repo" },
        error: null,
        createdAt: new Date(),
        startedAt: null,
        completedAt: null,
        agentTaskId: null,
      };

      mockRunnerJobsService.create.mockResolvedValue(mockJob);

      const result = await controller.create(createDto, workspaceId, mockUser);

      expect(result).toEqual(mockJob);
      expect(service.create).toHaveBeenCalledWith(workspaceId, createDto);
    });
  });

  describe("findAll", () => {
    it("should return paginated jobs", async () => {
      const workspaceId = "workspace-123";
      const query: QueryJobsDto = {
        page: 1,
        limit: 10,
      };

      const mockResult = {
        data: [
          {
            id: "job-1",
            workspaceId,
            type: "git-status",
            status: RunnerJobStatus.PENDING,
            priority: 5,
            progressPercent: 0,
            createdAt: new Date(),
          },
        ],
        meta: {
          total: 1,
          page: 1,
          limit: 10,
          totalPages: 1,
        },
      };

      mockRunnerJobsService.findAll.mockResolvedValue(mockResult);

      const result = await controller.findAll(query, workspaceId);

      expect(result).toEqual(mockResult);
      expect(service.findAll).toHaveBeenCalledWith({ ...query, workspaceId });
    });
  });

  describe("findOne", () => {
    it("should return a single job", async () => {
      const jobId = "job-123";
      const workspaceId = "workspace-123";

      const mockJob = {
        id: jobId,
        workspaceId,
        type: "git-status",
        status: RunnerJobStatus.COMPLETED,
        priority: 5,
        progressPercent: 100,
        result: { status: "success" },
        error: null,
        createdAt: new Date(),
        startedAt: new Date(),
        completedAt: new Date(),
        agentTask: null,
        steps: [],
        events: [],
      };

      mockRunnerJobsService.findOne.mockResolvedValue(mockJob);

      const result = await controller.findOne(jobId, workspaceId);

      expect(result).toEqual(mockJob);
      expect(service.findOne).toHaveBeenCalledWith(jobId, workspaceId);
    });
  });

  describe("cancel", () => {
    it("should cancel a job", async () => {
      const jobId = "job-123";
      const workspaceId = "workspace-123";

      const mockCancelledJob = {
        id: jobId,
        workspaceId,
        type: "git-status",
        status: RunnerJobStatus.CANCELLED,
        priority: 5,
        progressPercent: 0,
        result: null,
        error: null,
        createdAt: new Date(),
        startedAt: null,
        completedAt: new Date(),
        agentTaskId: null,
      };

      mockRunnerJobsService.cancel.mockResolvedValue(mockCancelledJob);

      const result = await controller.cancel(jobId, workspaceId, mockUser);

      expect(result).toEqual(mockCancelledJob);
      expect(service.cancel).toHaveBeenCalledWith(jobId, workspaceId);
    });
  });

  describe("retry", () => {
    it("should retry a failed job", async () => {
      const jobId = "job-123";
      const workspaceId = "workspace-123";

      const mockNewJob = {
        id: "job-new",
        workspaceId,
        type: "git-status",
        status: RunnerJobStatus.PENDING,
        priority: 5,
        progressPercent: 0,
        result: null,
        error: null,
        createdAt: new Date(),
        startedAt: null,
        completedAt: null,
        agentTaskId: null,
      };

      mockRunnerJobsService.retry.mockResolvedValue(mockNewJob);

      const result = await controller.retry(jobId, workspaceId, mockUser);

      expect(result).toEqual(mockNewJob);
      expect(service.retry).toHaveBeenCalledWith(jobId, workspaceId);
    });
  });

  describe("streamEvents", () => {
    it("should stream events via SSE", async () => {
      const jobId = "job-123";
      const workspaceId = "workspace-123";
      const lastEventId = undefined;

      // Mock response object
      const mockRes = {
        setHeader: vi.fn(),
        write: vi.fn(),
        end: vi.fn(),
      };

      const mockEvents = [
        {
          id: "event-1",
          jobId,
          type: "step.started",
          timestamp: new Date(),
          actor: "system",
          payload: { stepId: "step-1", name: "Running tests", phase: "validation" },
        },
        {
          id: "event-2",
          jobId,
          type: "step.output",
          timestamp: new Date(),
          actor: "system",
          payload: { stepId: "step-1", chunk: "Test suite passed: 42/42" },
        },
      ];

      mockRunnerJobsService.streamEvents.mockResolvedValue(mockEvents);

      await controller.streamEvents(jobId, workspaceId, lastEventId, mockRes as never);

      // Verify headers are set
      expect(mockRes.setHeader).toHaveBeenCalledWith("Content-Type", "text/event-stream");
      expect(mockRes.setHeader).toHaveBeenCalledWith("Cache-Control", "no-cache");
      expect(mockRes.setHeader).toHaveBeenCalledWith("Connection", "keep-alive");
      expect(mockRes.setHeader).toHaveBeenCalledWith("X-Accel-Buffering", "no");

      // Verify service was called
      expect(service.streamEvents).toHaveBeenCalledWith(jobId, workspaceId, mockRes, lastEventId);
    });

    it("should handle errors during streaming", async () => {
      const jobId = "job-123";
      const workspaceId = "workspace-123";
      const lastEventId = undefined;

      const mockRes = {
        setHeader: vi.fn(),
        write: vi.fn(),
        end: vi.fn(),
      };

      const error = new Error("Job not found");
      mockRunnerJobsService.streamEvents.mockRejectedValue(error);

      await controller.streamEvents(jobId, workspaceId, lastEventId, mockRes as never);

      // Verify error is written to stream
      expect(mockRes.write).toHaveBeenCalledWith("event: error\n");
      expect(mockRes.write).toHaveBeenCalledWith(
        expect.stringContaining('"error":"Job not found"')
      );
      expect(mockRes.end).toHaveBeenCalled();
    });

    it("should support reconnection with Last-Event-ID header", async () => {
      const jobId = "job-123";
      const workspaceId = "workspace-123";
      const lastEventId = "event-5";

      const mockRes = {
        setHeader: vi.fn(),
        write: vi.fn(),
        end: vi.fn(),
      };

      mockRunnerJobsService.streamEvents.mockResolvedValue([]);

      await controller.streamEvents(jobId, workspaceId, lastEventId, mockRes as never);

      // Verify service was called with lastEventId for reconnection
      expect(service.streamEvents).toHaveBeenCalledWith(jobId, workspaceId, mockRes, lastEventId);
    });
  });
});
