/**
 * CSRF Guard Tests
 *
 * Tests CSRF protection using double-submit cookie pattern with session binding.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { CsrfGuard } from "./csrf.guard";
import { CsrfService } from "../services/csrf.service";

describe("CsrfGuard", () => {
  let guard: CsrfGuard;
  let reflector: Reflector;
  let csrfService: CsrfService;
  const originalEnv = process.env;

  beforeEach(() => {
    process.env = { ...originalEnv };
    process.env.CSRF_SECRET = "test-secret-0123456789abcdef0123456789abcdef";
    reflector = new Reflector();
    csrfService = new CsrfService();
    csrfService.onModuleInit();
    guard = new CsrfGuard(reflector, csrfService);
  });

  afterEach(() => {
    process.env = originalEnv;
  });

  const createContext = (
    method: string,
    cookies: Record<string, string> = {},
    headers: Record<string, string> = {},
    skipCsrf = false,
    userId?: string
  ): ExecutionContext => {
    const request = {
      method,
      cookies,
      headers,
      path: "/api/test",
      user: userId ? { id: userId, email: "test@example.com", name: "Test" } : undefined,
    };

    return {
      switchToHttp: () => ({
        getRequest: () => request,
      }),
      getHandler: () => ({}),
      getClass: () => ({}),
      getAllAndOverride: vi.fn().mockReturnValue(skipCsrf),
    } as unknown as ExecutionContext;
  };

  /**
   * Helper to generate a valid session-bound token
   */
  const generateValidToken = (userId: string): string => {
    return csrfService.generateToken(userId);
  };

  describe("Safe HTTP methods", () => {
    it("should allow GET requests without CSRF token", () => {
      const context = createContext("GET");
      expect(guard.canActivate(context)).toBe(true);
    });

    it("should allow HEAD requests without CSRF token", () => {
      const context = createContext("HEAD");
      expect(guard.canActivate(context)).toBe(true);
    });

    it("should allow OPTIONS requests without CSRF token", () => {
      const context = createContext("OPTIONS");
      expect(guard.canActivate(context)).toBe(true);
    });
  });

  describe("Endpoints marked to skip CSRF", () => {
    it("should allow POST requests when @SkipCsrf() is applied", () => {
      vi.spyOn(reflector, "getAllAndOverride").mockReturnValue(true);
      const context = createContext("POST");
      expect(guard.canActivate(context)).toBe(true);
    });
  });

  describe("State-changing methods requiring CSRF", () => {
    it("should allow POST with Bearer auth without CSRF token", () => {
      const context = createContext(
        "POST",
        {},
        { authorization: "Bearer api-token" },
        false,
        "user-123"
      );
      expect(guard.canActivate(context)).toBe(true);
    });

    it("should reject POST without CSRF token", () => {
      const context = createContext("POST", {}, {}, false, "user-123");
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token missing");
    });

    it("should reject PUT without CSRF token", () => {
      const context = createContext("PUT", {}, {}, false, "user-123");
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
    });

    it("should reject PATCH without CSRF token", () => {
      const context = createContext("PATCH", {}, {}, false, "user-123");
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
    });

    it("should reject DELETE without CSRF token", () => {
      const context = createContext("DELETE", {}, {}, false, "user-123");
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
    });

    it("should reject when only cookie token is present", () => {
      const token = generateValidToken("user-123");
      const context = createContext("POST", { "csrf-token": token }, {}, false, "user-123");
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token missing");
    });

    it("should reject when only header token is present", () => {
      const token = generateValidToken("user-123");
      const context = createContext("POST", {}, { "x-csrf-token": token }, false, "user-123");
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token missing");
    });

    it("should reject when tokens do not match", () => {
      const token1 = generateValidToken("user-123");
      const token2 = generateValidToken("user-123");
      const context = createContext(
        "POST",
        { "csrf-token": token1 },
        { "x-csrf-token": token2 },
        false,
        "user-123"
      );
      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token mismatch");
    });

    it("should allow when tokens match and session is valid", () => {
      const token = generateValidToken("user-123");
      const context = createContext(
        "POST",
        { "csrf-token": token },
        { "x-csrf-token": token },
        false,
        "user-123"
      );
      expect(guard.canActivate(context)).toBe(true);
    });

    it("should allow PATCH when tokens match and session is valid", () => {
      const token = generateValidToken("user-123");
      const context = createContext(
        "PATCH",
        { "csrf-token": token },
        { "x-csrf-token": token },
        false,
        "user-123"
      );
      expect(guard.canActivate(context)).toBe(true);
    });

    it("should allow DELETE when tokens match and session is valid", () => {
      const token = generateValidToken("user-123");
      const context = createContext(
        "DELETE",
        { "csrf-token": token },
        { "x-csrf-token": token },
        false,
        "user-123"
      );
      expect(guard.canActivate(context)).toBe(true);
    });
  });

  describe("Session binding validation", () => {
    it("should allow when user context is not yet available (global guard ordering)", () => {
      // CsrfGuard runs as APP_GUARD before per-controller AuthGuard,
      // so request.user may not be populated. Double-submit cookie match
      // is sufficient protection in this case.
      const token = generateValidToken("user-123");
      const context = createContext(
        "POST",
        { "csrf-token": token },
        { "x-csrf-token": token },
        false
        // No userId - AuthGuard hasn't run yet
      );
      expect(guard.canActivate(context)).toBe(true);
    });

    it("should reject token from different session", () => {
      // Token generated for user-A
      const tokenForUserA = generateValidToken("user-A");

      // But request is from user-B
      const context = createContext(
        "POST",
        { "csrf-token": tokenForUserA },
        { "x-csrf-token": tokenForUserA },
        false,
        "user-B" // Different user
      );

      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token not bound to session");
    });

    it("should reject token with invalid HMAC", () => {
      // Create a token with tampered HMAC
      const validToken = generateValidToken("user-123");
      const parts = validToken.split(":");
      const tamperedToken = `${parts[0]}:0000000000000000000000000000000000000000000000000000000000000000`;

      const context = createContext(
        "POST",
        { "csrf-token": tamperedToken },
        { "x-csrf-token": tamperedToken },
        false,
        "user-123"
      );

      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token not bound to session");
    });

    it("should reject token with invalid format", () => {
      const invalidToken = "not-a-valid-token";

      const context = createContext(
        "POST",
        { "csrf-token": invalidToken },
        { "x-csrf-token": invalidToken },
        false,
        "user-123"
      );

      expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
      expect(() => guard.canActivate(context)).toThrow("CSRF token not bound to session");
    });

    it("should not allow token reuse across sessions", () => {
      // Generate token for user-A
      const tokenA = generateValidToken("user-A");

      // Valid for user-A
      const contextA = createContext(
        "POST",
        { "csrf-token": tokenA },
        { "x-csrf-token": tokenA },
        false,
        "user-A"
      );
      expect(guard.canActivate(contextA)).toBe(true);

      // Invalid for user-B
      const contextB = createContext(
        "POST",
        { "csrf-token": tokenA },
        { "x-csrf-token": tokenA },
        false,
        "user-B"
      );
      expect(() => guard.canActivate(contextB)).toThrow("CSRF token not bound to session");

      // Invalid for user-C
      const contextC = createContext(
        "POST",
        { "csrf-token": tokenA },
        { "x-csrf-token": tokenA },
        false,
        "user-C"
      );
      expect(() => guard.canActivate(contextC)).toThrow("CSRF token not bound to session");
    });

    it("should allow each user to use only their own token", () => {
      const tokenA = generateValidToken("user-A");
      const tokenB = generateValidToken("user-B");

      // User A with token A - valid
      const contextAA = createContext(
        "POST",
        { "csrf-token": tokenA },
        { "x-csrf-token": tokenA },
        false,
        "user-A"
      );
      expect(guard.canActivate(contextAA)).toBe(true);

      // User B with token B - valid
      const contextBB = createContext(
        "POST",
        { "csrf-token": tokenB },
        { "x-csrf-token": tokenB },
        false,
        "user-B"
      );
      expect(guard.canActivate(contextBB)).toBe(true);

      // User A with token B - invalid (cross-session)
      const contextAB = createContext(
        "POST",
        { "csrf-token": tokenB },
        { "x-csrf-token": tokenB },
        false,
        "user-A"
      );
      expect(() => guard.canActivate(contextAB)).toThrow("CSRF token not bound to session");

      // User B with token A - invalid (cross-session)
      const contextBA = createContext(
        "POST",
        { "csrf-token": tokenA },
        { "x-csrf-token": tokenA },
        false,
        "user-B"
      );
      expect(() => guard.canActivate(contextBA)).toThrow("CSRF token not bound to session");
    });
  });
});
