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

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

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

    guard = new OrchestratorApiKeyGuard(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-orchestrator-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("ORCHESTRATOR_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-orchestrator-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 ORCHESTRATOR_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-orchestrator-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-orchestrator-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 reject whitespace-only 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);

      const startTime = Date.now();
      const context1 = createMockExecutionContext({
        "x-api-key": "wrong-key-short",
      });

      try {
        guard.canActivate(context1);
      } catch {
        // Expected to fail
      }
      const shortKeyTime = Date.now() - startTime;

      const startTime2 = Date.now();
      const context2 = createMockExecutionContext({
        "x-api-key": "test-api-key-12344", // Very close to correct key
      });

      try {
        guard.canActivate(context2);
      } catch {
        // Expected to fail
      }
      const longKeyTime = Date.now() - startTime2;

      // Times should be similar (within 10ms) to prevent timing attacks
      // Note: This is a simplified test; real timing attack prevention
      // is handled by crypto.timingSafeEqual
      expect(Math.abs(shortKeyTime - longKeyTime)).toBeLessThan(10);
    });

    it("should reject keys with different lengths even if prefix matches", () => {
      const validApiKey = "orchestrator-secret-key-abc123";
      vi.mocked(mockConfigService.get).mockReturnValue(validApiKey);

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

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