import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import {
  ExecutionContext,
  ForbiddenException,
  BadRequestException,
  InternalServerErrorException,
} from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { WorkspaceGuard } from "./workspace.guard";
import { PrismaService } from "../../prisma/prisma.service";

describe("WorkspaceGuard", () => {
  let guard: WorkspaceGuard;
  let prismaService: PrismaService;

  const mockPrismaService = {
    workspaceMember: {
      findUnique: vi.fn(),
    },
    $executeRaw: vi.fn(),
  };

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

    guard = module.get<WorkspaceGuard>(WorkspaceGuard);
    prismaService = module.get<PrismaService>(PrismaService);

    // Clear all mocks
    vi.clearAllMocks();
  });

  const createMockExecutionContext = (
    user: any,
    headers: Record<string, string> = {},
    params: Record<string, string> = {},
    body: Record<string, any> = {},
    query: Record<string, string> = {}
  ): ExecutionContext => {
    const mockRequest = {
      user,
      headers,
      params,
      body,
      query,
    };

    return {
      switchToHttp: () => ({
        getRequest: () => mockRequest,
      }),
    } as ExecutionContext;
  };

  describe("canActivate", () => {
    const userId = "user-123";
    const workspaceId = "workspace-456";

    it("should allow access when user is a workspace member (via header)", async () => {
      const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId });

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId,
        userId,
        role: "MEMBER",
      });

      const result = await guard.canActivate(context);

      expect(result).toBe(true);
      expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId,
          },
        },
      });

      const request = context.switchToHttp().getRequest();
      expect(request.workspace).toEqual({ id: workspaceId });
      expect(request.user.workspaceId).toBe(workspaceId);
    });

    it("should allow access when user is a workspace member (via URL param)", async () => {
      const context = createMockExecutionContext({ id: userId }, {}, { workspaceId });

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId,
        userId,
        role: "ADMIN",
      });

      const result = await guard.canActivate(context);

      expect(result).toBe(true);
    });

    it("should allow access when user is a workspace member (via body)", async () => {
      const context = createMockExecutionContext({ id: userId }, {}, {}, { workspaceId });

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId,
        userId,
        role: "OWNER",
      });

      const result = await guard.canActivate(context);

      expect(result).toBe(true);
    });

    it("should allow access when user is a workspace member (via query string)", async () => {
      const context = createMockExecutionContext({ id: userId }, {}, {}, {}, { workspaceId });

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId,
        userId,
        role: "MEMBER",
      });

      const result = await guard.canActivate(context);

      expect(result).toBe(true);
      expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId,
            userId,
          },
        },
      });
    });

    it("should prioritize header over param, body, and query", async () => {
      const headerWorkspaceId = "workspace-header";
      const paramWorkspaceId = "workspace-param";
      const bodyWorkspaceId = "workspace-body";
      const queryWorkspaceId = "workspace-query";

      const context = createMockExecutionContext(
        { id: userId },
        { "x-workspace-id": headerWorkspaceId },
        { workspaceId: paramWorkspaceId },
        { workspaceId: bodyWorkspaceId },
        { workspaceId: queryWorkspaceId }
      );

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId: headerWorkspaceId,
        userId,
        role: "MEMBER",
      });

      await guard.canActivate(context);

      expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId: headerWorkspaceId,
            userId,
          },
        },
      });
    });

    it("should prioritize param over body and query when header missing", async () => {
      const paramWorkspaceId = "workspace-param";
      const bodyWorkspaceId = "workspace-body";
      const queryWorkspaceId = "workspace-query";

      const context = createMockExecutionContext(
        { id: userId },
        {},
        { workspaceId: paramWorkspaceId },
        { workspaceId: bodyWorkspaceId },
        { workspaceId: queryWorkspaceId }
      );

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId: paramWorkspaceId,
        userId,
        role: "MEMBER",
      });

      await guard.canActivate(context);

      expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId: paramWorkspaceId,
            userId,
          },
        },
      });
    });

    it("should prioritize body over query when header and param missing", async () => {
      const bodyWorkspaceId = "workspace-body";
      const queryWorkspaceId = "workspace-query";

      const context = createMockExecutionContext(
        { id: userId },
        {},
        {},
        { workspaceId: bodyWorkspaceId },
        { workspaceId: queryWorkspaceId }
      );

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue({
        workspaceId: bodyWorkspaceId,
        userId,
        role: "MEMBER",
      });

      await guard.canActivate(context);

      expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId: bodyWorkspaceId,
            userId,
          },
        },
      });
    });

    it("should throw ForbiddenException when user is not authenticated", async () => {
      const context = createMockExecutionContext(null, { "x-workspace-id": workspaceId });

      await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException);
      await expect(guard.canActivate(context)).rejects.toThrow("User not authenticated");
    });

    it("should throw BadRequestException when workspace ID is missing", async () => {
      const context = createMockExecutionContext({ id: userId });

      await expect(guard.canActivate(context)).rejects.toThrow(BadRequestException);
      await expect(guard.canActivate(context)).rejects.toThrow("Workspace ID is required");
    });

    it("should throw ForbiddenException when user is not a workspace member", async () => {
      const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId });

      mockPrismaService.workspaceMember.findUnique.mockResolvedValue(null);

      await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException);
      await expect(guard.canActivate(context)).rejects.toThrow(
        "You do not have access to this workspace"
      );
    });

    it("should throw InternalServerErrorException on database connection errors", async () => {
      const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId });

      mockPrismaService.workspaceMember.findUnique.mockRejectedValue(
        new Error("Database connection failed")
      );

      await expect(guard.canActivate(context)).rejects.toThrow(InternalServerErrorException);
      await expect(guard.canActivate(context)).rejects.toThrow("Failed to verify workspace access");
    });

    it("should throw InternalServerErrorException on Prisma connection timeout", async () => {
      const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId });

      const prismaError = new Prisma.PrismaClientKnownRequestError("Connection timed out", {
        code: "P1001", // Authentication failed (connection error)
        clientVersion: "5.0.0",
      });

      mockPrismaService.workspaceMember.findUnique.mockRejectedValue(prismaError);

      await expect(guard.canActivate(context)).rejects.toThrow(InternalServerErrorException);
    });

    it("should return false for Prisma not found error (P2025)", async () => {
      const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId });

      const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
        code: "P2025", // Record not found
        clientVersion: "5.0.0",
      });

      mockPrismaService.workspaceMember.findUnique.mockRejectedValue(prismaError);

      // P2025 should be treated as "not a member" -> ForbiddenException
      await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException);
      await expect(guard.canActivate(context)).rejects.toThrow(
        "You do not have access to this workspace"
      );
    });

    it("should NOT mask database pool exhaustion as access denied", async () => {
      const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId });

      const prismaError = new Prisma.PrismaClientKnownRequestError("Connection pool exhausted", {
        code: "P2024", // Connection pool timeout
        clientVersion: "5.0.0",
      });

      mockPrismaService.workspaceMember.findUnique.mockRejectedValue(prismaError);

      // Should NOT throw ForbiddenException for DB errors
      await expect(guard.canActivate(context)).rejects.toThrow(InternalServerErrorException);
      await expect(guard.canActivate(context)).rejects.not.toThrow(ForbiddenException);
    });
  });
});
