import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { RejectionHandlerService } from "./rejection-handler.service";
import { PrismaService } from "../prisma/prisma.service";
import { Logger } from "@nestjs/common";
import type {
  RejectionContext,
  RejectionResult,
  EscalationConfig,
  EscalationRule,
} from "./interfaces";

describe("RejectionHandlerService", () => {
  let service: RejectionHandlerService;
  let prismaService: PrismaService;

  const mockRejectionContext: RejectionContext = {
    taskId: "task-123",
    workspaceId: "workspace-456",
    agentId: "agent-789",
    attemptCount: 3,
    failures: [
      {
        gateName: "type-check",
        failureType: "compilation-error",
        message: "Type error in module",
        attempts: 2,
      },
      {
        gateName: "test-gate",
        failureType: "test-failure",
        message: "5 tests failed",
        attempts: 1,
      },
    ],
    originalTask: "Implement user authentication",
    startedAt: new Date("2026-01-31T10:00:00Z"),
    rejectedAt: new Date("2026-01-31T12:30:00Z"),
  };

  const mockPrismaService = {
    taskRejection: {
      create: vi.fn(),
      findMany: vi.fn(),
      update: vi.fn(),
    },
  };

  const mockLogger = {
    log: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
  };

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

    service = module.get<RejectionHandlerService>(RejectionHandlerService);
    prismaService = module.get(PrismaService);

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

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

  describe("handleRejection", () => {
    it("should handle rejection and return result", async () => {
      const mockRejection = {
        id: "rejection-1",
        taskId: mockRejectionContext.taskId,
        workspaceId: mockRejectionContext.workspaceId,
        agentId: mockRejectionContext.agentId,
        attemptCount: mockRejectionContext.attemptCount,
        failures: mockRejectionContext.failures as any,
        originalTask: mockRejectionContext.originalTask,
        startedAt: mockRejectionContext.startedAt,
        rejectedAt: mockRejectionContext.rejectedAt,
        escalated: true,
        manualReview: true,
        resolvedAt: null,
        resolution: null,
      };

      mockPrismaService.taskRejection.create.mockResolvedValue(mockRejection);
      mockPrismaService.taskRejection.findMany.mockResolvedValue([mockRejection]);
      mockPrismaService.taskRejection.update.mockResolvedValue(mockRejection);

      const result = await service.handleRejection(mockRejectionContext);

      expect(result.handled).toBe(true);
      expect(result.manualReviewRequired).toBe(true);
      expect(mockPrismaService.taskRejection.create).toHaveBeenCalled();
    });

    it("should handle rejection without escalation for low attempt count", async () => {
      const lowAttemptContext: RejectionContext = {
        ...mockRejectionContext,
        attemptCount: 1,
        startedAt: new Date("2026-01-31T12:00:00Z"),
        rejectedAt: new Date("2026-01-31T12:30:00Z"), // 30 minutes - under maxWaitTime
      };

      const mockRejection = {
        id: "rejection-2",
        taskId: lowAttemptContext.taskId,
        workspaceId: lowAttemptContext.workspaceId,
        agentId: lowAttemptContext.agentId,
        attemptCount: lowAttemptContext.attemptCount,
        failures: lowAttemptContext.failures as any,
        originalTask: lowAttemptContext.originalTask,
        startedAt: lowAttemptContext.startedAt,
        rejectedAt: lowAttemptContext.rejectedAt,
        escalated: false,
        manualReview: false,
        resolvedAt: null,
        resolution: null,
      };

      mockPrismaService.taskRejection.create.mockResolvedValue(mockRejection);
      mockPrismaService.taskRejection.findMany.mockResolvedValue([mockRejection]);
      mockPrismaService.taskRejection.update.mockResolvedValue(mockRejection);

      const result = await service.handleRejection(lowAttemptContext);

      expect(result.handled).toBe(true);
      expect(result.escalated).toBe(false);
    });
  });

  describe("logRejection", () => {
    it("should log rejection to database", async () => {
      prismaService.taskRejection.create.mockResolvedValue({
        id: "rejection-3",
        taskId: mockRejectionContext.taskId,
        workspaceId: mockRejectionContext.workspaceId,
        agentId: mockRejectionContext.agentId,
        attemptCount: mockRejectionContext.attemptCount,
        failures: mockRejectionContext.failures as any,
        originalTask: mockRejectionContext.originalTask,
        startedAt: mockRejectionContext.startedAt,
        rejectedAt: mockRejectionContext.rejectedAt,
        escalated: false,
        manualReview: false,
        resolvedAt: null,
        resolution: null,
      });

      await service.logRejection(mockRejectionContext);

      expect(prismaService.taskRejection.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          taskId: mockRejectionContext.taskId,
          workspaceId: mockRejectionContext.workspaceId,
          agentId: mockRejectionContext.agentId,
          attemptCount: mockRejectionContext.attemptCount,
        }),
      });
    });
  });

  describe("determineEscalation", () => {
    it("should determine escalation rules based on max attempts", () => {
      const config: EscalationConfig = {
        rules: [
          {
            condition: "max-attempts",
            action: "notify",
            target: "admin@example.com",
            priority: "high",
          },
        ],
        notifyOnRejection: true,
        autoReassign: false,
        maxWaitTime: 60,
      };

      const rules = service.determineEscalation(mockRejectionContext, config);

      expect(rules).toHaveLength(1);
      expect(rules[0].condition).toBe("max-attempts");
      expect(rules[0].action).toBe("notify");
      expect(rules[0].priority).toBe("high");
    });

    it("should determine escalation for time exceeded", () => {
      const longRunningContext: RejectionContext = {
        ...mockRejectionContext,
        startedAt: new Date("2026-01-31T08:00:00Z"),
        rejectedAt: new Date("2026-01-31T12:00:00Z"),
      };

      const config: EscalationConfig = {
        rules: [
          {
            condition: "time-exceeded",
            action: "block",
            priority: "critical",
          },
        ],
        notifyOnRejection: true,
        autoReassign: false,
        maxWaitTime: 120, // 2 hours
      };

      const rules = service.determineEscalation(longRunningContext, config);

      expect(rules.length).toBeGreaterThan(0);
      expect(rules.some((r) => r.condition === "time-exceeded")).toBe(true);
    });

    it("should determine escalation for critical failures", () => {
      const criticalContext: RejectionContext = {
        ...mockRejectionContext,
        failures: [
          {
            gateName: "security-scan",
            failureType: "critical-vulnerability",
            message: "SQL injection detected",
            attempts: 1,
          },
        ],
      };

      const config: EscalationConfig = {
        rules: [
          {
            condition: "critical-failure",
            action: "block",
            priority: "critical",
          },
        ],
        notifyOnRejection: true,
        autoReassign: false,
        maxWaitTime: 60,
      };

      const rules = service.determineEscalation(criticalContext, config);

      expect(rules.some((r) => r.condition === "critical-failure")).toBe(true);
    });
  });

  describe("executeEscalation", () => {
    it("should execute notification escalation", async () => {
      const rules: EscalationRule[] = [
        {
          condition: "max-attempts",
          action: "notify",
          target: "admin@example.com",
          priority: "high",
        },
      ];

      // Mock sendNotification
      vi.spyOn(service, "sendNotification").mockReturnValue();

      await service.executeEscalation(mockRejectionContext, rules);

      expect(service.sendNotification).toHaveBeenCalledWith(
        mockRejectionContext,
        "admin@example.com",
        "high"
      );
    });

    it("should execute block escalation", async () => {
      const rules: EscalationRule[] = [
        {
          condition: "critical-failure",
          action: "block",
          priority: "critical",
        },
      ];

      vi.spyOn(service, "markForManualReview").mockResolvedValue();

      await service.executeEscalation(mockRejectionContext, rules);

      expect(service.markForManualReview).toHaveBeenCalledWith(
        mockRejectionContext.taskId,
        expect.any(String)
      );
    });
  });

  describe("sendNotification", () => {
    it("should send notification with context and priority", () => {
      service.sendNotification(mockRejectionContext, "admin@example.com", "high");

      // Verify logging occurred
      expect(true).toBe(true); // Placeholder - actual implementation will log
    });
  });

  describe("markForManualReview", () => {
    it("should mark task for manual review", async () => {
      const mockRejection = {
        id: "rejection-4",
        taskId: mockRejectionContext.taskId,
        workspaceId: mockRejectionContext.workspaceId,
        agentId: mockRejectionContext.agentId,
        attemptCount: mockRejectionContext.attemptCount,
        failures: mockRejectionContext.failures as any,
        originalTask: mockRejectionContext.originalTask,
        startedAt: mockRejectionContext.startedAt,
        rejectedAt: mockRejectionContext.rejectedAt,
        escalated: false,
        manualReview: false,
        resolvedAt: null,
        resolution: null,
      };

      mockPrismaService.taskRejection.findMany.mockResolvedValue([mockRejection]);
      mockPrismaService.taskRejection.update.mockResolvedValue({
        ...mockRejection,
        escalated: true,
        manualReview: true,
      });

      await service.markForManualReview(mockRejectionContext.taskId, "Max attempts exceeded");

      expect(mockPrismaService.taskRejection.findMany).toHaveBeenCalledWith({
        where: { taskId: mockRejectionContext.taskId },
        orderBy: { rejectedAt: "desc" },
        take: 1,
      });

      expect(mockPrismaService.taskRejection.update).toHaveBeenCalledWith({
        where: { id: mockRejection.id },
        data: {
          manualReview: true,
          escalated: true,
        },
      });
    });
  });

  describe("getRejectionHistory", () => {
    it("should retrieve rejection history for a task", async () => {
      const mockHistory = [
        {
          id: "rejection-5",
          taskId: mockRejectionContext.taskId,
          workspaceId: mockRejectionContext.workspaceId,
          agentId: mockRejectionContext.agentId,
          attemptCount: 1,
          failures: [],
          originalTask: mockRejectionContext.originalTask,
          startedAt: new Date("2026-01-31T09:00:00Z"),
          rejectedAt: new Date("2026-01-31T10:00:00Z"),
          escalated: false,
          manualReview: false,
          resolvedAt: null,
          resolution: null,
        },
        {
          id: "rejection-6",
          taskId: mockRejectionContext.taskId,
          workspaceId: mockRejectionContext.workspaceId,
          agentId: mockRejectionContext.agentId,
          attemptCount: 2,
          failures: [],
          originalTask: mockRejectionContext.originalTask,
          startedAt: new Date("2026-01-31T10:30:00Z"),
          rejectedAt: new Date("2026-01-31T11:30:00Z"),
          escalated: false,
          manualReview: false,
          resolvedAt: null,
          resolution: null,
        },
      ];

      prismaService.taskRejection.findMany.mockResolvedValue(mockHistory);

      const history = await service.getRejectionHistory(mockRejectionContext.taskId);

      expect(history).toHaveLength(2);
      expect(prismaService.taskRejection.findMany).toHaveBeenCalledWith({
        where: { taskId: mockRejectionContext.taskId },
        orderBy: { rejectedAt: "desc" },
      });
    });
  });

  describe("generateRejectionReport", () => {
    it("should generate a formatted rejection report", () => {
      const report = service.generateRejectionReport(mockRejectionContext);

      expect(report).toContain("Task Rejection Report");
      expect(report).toContain(mockRejectionContext.taskId);
      expect(report).toContain(mockRejectionContext.attemptCount.toString());
      expect(report).toContain("type-check");
      expect(report).toContain("test-gate");
    });

    it("should include failure details in report", () => {
      const report = service.generateRejectionReport(mockRejectionContext);

      mockRejectionContext.failures.forEach((failure) => {
        expect(report).toContain(failure.gateName);
        expect(report).toContain(failure.message);
      });
    });
  });

  describe("getDefaultEscalationConfig", () => {
    it("should return default escalation configuration", () => {
      const config = service.getDefaultEscalationConfig();

      expect(config.rules).toBeDefined();
      expect(config.rules.length).toBeGreaterThan(0);
      expect(config.notifyOnRejection).toBeDefined();
      expect(config.autoReassign).toBeDefined();
      expect(config.maxWaitTime).toBeGreaterThan(0);
    });

    it("should include all escalation conditions in default config", () => {
      const config = service.getDefaultEscalationConfig();

      const conditions = config.rules.map((r) => r.condition);
      expect(conditions).toContain("max-attempts");
      expect(conditions).toContain("critical-failure");
      expect(conditions).toContain("time-exceeded");
    });
  });
});
