import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ExecutionContext, UnauthorizedException } from "@nestjs/common";
import { AuthGuard } from "./auth.guard";
import { AuthService } from "../auth.service";

describe("AuthGuard", () => {
  let guard: AuthGuard;
  let authService: AuthService;

  const mockAuthService = {
    verifySession: vi.fn(),
  };

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

    guard = module.get<AuthGuard>(AuthGuard);
    authService = module.get<AuthService>(AuthService);

    vi.clearAllMocks();
  });

  const createMockExecutionContext = (
    headers: Record<string, string> = {},
    cookies: Record<string, string> = {}
  ): ExecutionContext => {
    const mockRequest = {
      headers,
      cookies,
    };

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

  describe("canActivate", () => {
    const mockSessionData = {
      user: {
        id: "user-123",
        email: "test@example.com",
        name: "Test User",
      },
      session: {
        id: "session-123",
        token: "session-token",
        expiresAt: new Date(Date.now() + 86400000),
      },
    };

    describe("Bearer token authentication", () => {
      it("should return true for valid Bearer token", async () => {
        mockAuthService.verifySession.mockResolvedValue(mockSessionData);

        const context = createMockExecutionContext({
          authorization: "Bearer valid-token",
        });

        const result = await guard.canActivate(context);

        expect(result).toBe(true);
        expect(mockAuthService.verifySession).toHaveBeenCalledWith("valid-token");
      });

      it("should throw UnauthorizedException for invalid Bearer token", async () => {
        mockAuthService.verifySession.mockResolvedValue(null);

        const context = createMockExecutionContext({
          authorization: "Bearer invalid-token",
        });

        await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
        await expect(guard.canActivate(context)).rejects.toThrow("Invalid or expired session");
      });
    });

    describe("Cookie-based authentication", () => {
      it("should return true for valid session cookie", async () => {
        mockAuthService.verifySession.mockResolvedValue(mockSessionData);

        const context = createMockExecutionContext(
          {},
          {
            "better-auth.session_token": "cookie-token",
          }
        );

        const result = await guard.canActivate(context);

        expect(result).toBe(true);
        expect(mockAuthService.verifySession).toHaveBeenCalledWith("cookie-token");
      });

      it("should prefer cookie over Bearer token when both present", async () => {
        mockAuthService.verifySession.mockResolvedValue(mockSessionData);

        const context = createMockExecutionContext(
          {
            authorization: "Bearer bearer-token",
          },
          {
            "better-auth.session_token": "cookie-token",
          }
        );

        const result = await guard.canActivate(context);

        expect(result).toBe(true);
        expect(mockAuthService.verifySession).toHaveBeenCalledWith("cookie-token");
      });

      it("should fallback to Bearer token if no cookie", async () => {
        mockAuthService.verifySession.mockResolvedValue(mockSessionData);

        const context = createMockExecutionContext(
          {
            authorization: "Bearer bearer-token",
          },
          {}
        );

        const result = await guard.canActivate(context);

        expect(result).toBe(true);
        expect(mockAuthService.verifySession).toHaveBeenCalledWith("bearer-token");
      });
    });

    describe("Error handling", () => {
      it("should throw UnauthorizedException if no token provided", async () => {
        const context = createMockExecutionContext({}, {});

        await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
        await expect(guard.canActivate(context)).rejects.toThrow(
          "No authentication token provided"
        );
      });

      it("should throw UnauthorizedException if session verification fails", async () => {
        mockAuthService.verifySession.mockRejectedValue(new Error("Verification failed"));

        const context = createMockExecutionContext({
          authorization: "Bearer error-token",
        });

        await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
        await expect(guard.canActivate(context)).rejects.toThrow("Authentication failed");
      });

      it("should attach user and session to request on success", async () => {
        mockAuthService.verifySession.mockResolvedValue(mockSessionData);

        const mockRequest = {
          headers: {
            authorization: "Bearer valid-token",
          },
          cookies: {},
        };

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

        await guard.canActivate(context);

        expect(mockRequest).toHaveProperty("user", mockSessionData.user);
        expect(mockRequest).toHaveProperty("session", mockSessionData.session);
      });
    });
  });
});
