import { describe, it, expect, beforeEach, vi } from "vitest";
import { ExecutionContext, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiKeyGuard } from "./api-key.guard";

describe("ApiKeyGuard", () => {
  let guard: ApiKeyGuard;
  let mockConfigService: ConfigService;

  beforeEach(() => {
    mockConfigService = {
      get: vi.fn(),
    } as unknown as ConfigService;

    guard = new ApiKeyGuard(mockConfigService);
  });

  const createMockExecutionContext = (headers: Record<string, string>): ExecutionContext => {
    return {
      switchToHttp: () => ({
        getRequest: () => ({
          headers,
        }),
      }),
    } as ExecutionContext;
  };

  describe("canActivate", () => {
    it("should return true when valid API key is provided", () => {
      const validApiKey = "test-api-key-12345";
      vi.mocked(mockConfigService.get).mockReturnValue(validApiKey);

      const context = createMockExecutionContext({
        "x-api-key": validApiKey,
      });

      const result = guard.canActivate(context);

      expect(result).toBe(true);
      expect(mockConfigService.get).toHaveBeenCalledWith("COORDINATOR_API_KEY");
    });

    it("should throw UnauthorizedException when no API key is provided", () => {
      const context = createMockExecutionContext({});

      expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
      expect(() => guard.canActivate(context)).toThrow("No API key provided");
    });

    it("should throw UnauthorizedException when API key is invalid", () => {
      const validApiKey = "correct-api-key";
      const invalidApiKey = "wrong-api-key";

      vi.mocked(mockConfigService.get).mockReturnValue(validApiKey);

      const context = createMockExecutionContext({
        "x-api-key": invalidApiKey,
      });

      expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
      expect(() => guard.canActivate(context)).toThrow("Invalid API key");
    });

    it("should throw UnauthorizedException when COORDINATOR_API_KEY is not configured", () => {
      vi.mocked(mockConfigService.get).mockReturnValue(undefined);

      const context = createMockExecutionContext({
        "x-api-key": "some-key",
      });

      expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
      expect(() => guard.canActivate(context)).toThrow("API key authentication not configured");
    });

    it("should handle uppercase header name (X-API-Key)", () => {
      const validApiKey = "test-api-key-12345";
      vi.mocked(mockConfigService.get).mockReturnValue(validApiKey);

      const context = createMockExecutionContext({
        "X-API-Key": validApiKey,
      });

      const result = guard.canActivate(context);

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

    it("should handle mixed case header name (X-Api-Key)", () => {
      const validApiKey = "test-api-key-12345";
      vi.mocked(mockConfigService.get).mockReturnValue(validApiKey);

      const context = createMockExecutionContext({
        "X-Api-Key": validApiKey,
      });

      const result = guard.canActivate(context);

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

    it("should reject empty string API key", () => {
      vi.mocked(mockConfigService.get).mockReturnValue("valid-key");

      const context = createMockExecutionContext({
        "x-api-key": "",
      });

      expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
      expect(() => guard.canActivate(context)).toThrow("No API key provided");
    });

    it("should use constant-time comparison to prevent timing attacks", () => {
      const validApiKey = "test-api-key-12345";
      vi.mocked(mockConfigService.get).mockReturnValue(validApiKey);

      // Verify that same-length keys are compared properly (exercises timingSafeEqual path)
      // and different-length keys are rejected before comparison
      const sameLength = createMockExecutionContext({
        "x-api-key": "test-api-key-12344", // Same length, one char different
      });
      const differentLength = createMockExecutionContext({
        "x-api-key": "short", // Different length
      });

      // Both should throw, proving the comparison logic handles both cases
      expect(() => guard.canActivate(sameLength)).toThrow("Invalid API key");
      expect(() => guard.canActivate(differentLength)).toThrow("Invalid API key");

      // Correct key should pass
      const correct = createMockExecutionContext({
        "x-api-key": validApiKey,
      });
      expect(guard.canActivate(correct)).toBe(true);
    });
  });
});
