import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Test, type TestingModule } from "@nestjs/testing";
import { describe, beforeEach, expect, it, vi } from "vitest";
import { PrismaService } from "../prisma/prisma.service";
import type { GiteaPrWebhookDto } from "./dto/gitea-pr-webhook.dto";
import { GatekeeperService } from "./gatekeeper.service";

describe("GatekeeperService", () => {
  let service: GatekeeperService;

  const prisma = {
    pendingMerge: {
      upsert: vi.fn(),
      update: vi.fn(),
      findFirst: vi.fn(),
      findUnique: vi.fn(),
    },
  };

  const config = {
    get: vi.fn(),
  };

  const basePayload: GiteaPrWebhookDto = {
    type: "pull_request",
    action: "labeled",
    repository: { full_name: "mosaic/stack" },
    label: { name: "auto-merge" },
    sender: { login: "jason" },
    pull_request: {
      number: 42,
      body: "Implements Gatekeeper",
      base: { ref: "main" },
      head: { sha: "abcdef1234567890" },
      labels: [{ name: "auto-merge" }],
      url: "https://git.mosaicstack.dev/api/v1/repos/mosaic/stack/pulls/42",
      html_url: "https://git.mosaicstack.dev/mosaic/stack/pulls/42",
    },
  };

  beforeEach(async () => {
    vi.clearAllMocks();
    config.get.mockImplementation((key: string) => {
      switch (key) {
        case "GATEKEEPER_ENABLED":
          return "true";
        case "GITEA_API_TOKEN":
          return "token";
        default:
          return undefined;
      }
    });

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        GatekeeperService,
        { provide: PrismaService, useValue: prisma },
        { provide: ConfigService, useValue: config },
      ],
    }).compile();

    service = module.get(GatekeeperService);
  });

  it("moves labeled auto-merge PRs into awaiting_ci when review passes", async () => {
    prisma.pendingMerge.upsert.mockResolvedValue({ id: "merge-1" });
    prisma.pendingMerge.update.mockResolvedValue({});

    await service.handlePrEvent(basePayload);

    expect(prisma.pendingMerge.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          repo_prNumber_headSha: {
            repo: "mosaic/stack",
            prNumber: 42,
            headSha: "abcdef1234567890",
          },
        },
      })
    );
    expect(prisma.pendingMerge.update).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ data: expect.objectContaining({ state: "reviewing" }) })
    );
    expect(prisma.pendingMerge.update).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        data: expect.objectContaining({
          state: "awaiting_ci",
          reviewResult: { passed: true, issues: [] },
        }),
      })
    );
  });

  it("rejects review failures and records the reason", async () => {
    prisma.pendingMerge.upsert.mockResolvedValue({ id: "merge-2" });
    prisma.pendingMerge.findUnique.mockResolvedValue({
      id: "merge-2",
      repo: "mosaic/stack",
      prNumber: 42,
      headSha: "abcdef1234567890",
    });
    prisma.pendingMerge.update.mockResolvedValue({});
    const commentSpy = vi
      .spyOn(
        service as unknown as {
          postPullRequestComment: (repo: string, prNumber: number, body: string) => Promise<void>;
        },
        "postPullRequestComment"
      )
      .mockResolvedValue();

    await service.handlePrEvent({
      ...basePayload,
      pull_request: {
        ...basePayload.pull_request,
        body: "",
      },
    });

    expect(prisma.pendingMerge.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "merge-2" },
        data: {
          state: "rejected",
          reviewResult: {
            passed: false,
            issues: ["PR description must not be empty"],
          },
        },
      })
    );
    expect(commentSpy).toHaveBeenCalledWith(
      "mosaic/stack",
      42,
      expect.stringContaining("PR description must not be empty")
    );
  });

  it("attempts merge on green CI for awaiting_ci records", async () => {
    prisma.pendingMerge.findFirst.mockResolvedValue({
      id: "merge-3",
      repo: "mosaic/stack",
      prNumber: 42,
      headSha: "abcdef1234567890",
      state: "awaiting_ci",
      giteaMergeUrl: "https://git.mosaicstack.dev/api/v1/repos/mosaic/stack/pulls/42",
    });
    prisma.pendingMerge.update.mockResolvedValue({});
    const mergeSpy = vi.spyOn(service, "attemptMerge").mockResolvedValue();

    await service.handleCiEvent("mosaic/stack", 42, "abcdef1234567890", "success");

    expect(prisma.pendingMerge.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "merge-3" },
        data: expect.objectContaining({ ciStatus: "success" }),
      })
    );
    expect(mergeSpy).toHaveBeenCalledWith("merge-3");
  });

  it("rejects failed CI results", async () => {
    prisma.pendingMerge.findFirst.mockResolvedValue({
      id: "merge-4",
      repo: "mosaic/stack",
      prNumber: 42,
      headSha: "abcdef1234567890",
      state: "awaiting_ci",
    });
    prisma.pendingMerge.update.mockResolvedValue({});
    const rejectSpy = vi.spyOn(service, "rejectMerge").mockResolvedValue();

    await service.handleCiEvent("mosaic/stack", 42, "abcdef1234567890", "failure");

    expect(rejectSpy).toHaveBeenCalledWith("merge-4", "CI reported failure");
  });

  it("skips all work when Gatekeeper is disabled", async () => {
    const warnSpy = vi.spyOn(Logger.prototype, "warn").mockImplementation(() => undefined);
    config.get.mockImplementation((key: string) => {
      if (key === "GATEKEEPER_ENABLED") {
        return "false";
      }

      return undefined;
    });

    await service.handlePrEvent(basePayload);
    await service.handleCiEvent("mosaic/stack", 42, "abcdef1234567890", "success");

    expect(prisma.pendingMerge.upsert).not.toHaveBeenCalled();
    expect(prisma.pendingMerge.findFirst).not.toHaveBeenCalled();
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Gatekeeper is disabled"));
  });
});
