/**
 * Unit Tests for CSRF Client Utilities
 *
 * Tests for:
 * - Token retrieval from cookies
 * - Request deduplication
 * - Token refresh logic
 * - fetchWithCSRF retry behavior
 * - Type-safe fetchWithCSRFJson
 */

import {
  getCSRFToken,
  getCSRFTokenAsync,
  refreshCSRFToken,
  clearCSRFCache,
  fetchWithCSRF,
} from "@/lib/csrf-client";

// Mock fetch globally
const mockFetch = jest.fn();
global.fetch = mockFetch;

// Mock document.cookie
let mockCookie = "";
Object.defineProperty(document, "cookie", {
  get: () => mockCookie,
  set: (value: string) => {
    // Handle cookie clearing
    if (
      value.includes("max-age=0") ||
      value.includes("expires=Thu, 01 Jan 1970")
    ) {
      mockCookie = "";
    } else {
      mockCookie = value;
    }
  },
  configurable: true,
});

describe("csrf-client", () => {
  beforeEach(() => {
    // Reset mocks before each test
    mockFetch.mockReset();
    mockCookie = "";
    clearCSRFCache();
    jest.clearAllMocks();
  });

  describe("getCSRFToken", () => {
    it("should return null when no cookie exists", () => {
      mockCookie = "";
      expect(getCSRFToken()).toBeNull();
    });

    it("should extract token from cookie", () => {
      const token = "abc123def456";
      mockCookie = `csrf-token=${token}`;
      expect(getCSRFToken()).toBe(token);
    });

    it("should handle URL-encoded tokens", () => {
      const token = "token%20with%20spaces";
      mockCookie = `csrf-token=${token}`;
      expect(getCSRFToken()).toBe("token with spaces");
    });

    it("should extract token when multiple cookies exist", () => {
      const token = "mytoken123";
      mockCookie = `other=value; csrf-token=${token}; another=cookie`;
      expect(getCSRFToken()).toBe(token);
    });

    it("should return null for malformed cookie", () => {
      mockCookie = "csrf-token-invalid";
      expect(getCSRFToken()).toBeNull();
    });
  });

  describe("getCSRFTokenAsync", () => {
    it("should return cookie token if valid (64 chars)", async () => {
      const validToken = "a".repeat(64);
      mockCookie = `csrf-token=${validToken}`;

      const result = await getCSRFTokenAsync();
      expect(result).toBe(validToken);
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("should fetch token from server if no cookie", async () => {
      const serverToken = "server-token-123";
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: serverToken }),
      });

      const result = await getCSRFTokenAsync();
      expect(result).toBe(serverToken);
      expect(mockFetch).toHaveBeenCalledWith("/api/csrf", {
        credentials: "include",
        cache: "no-store",
      });
    });

    it("should cache token and return cached value on subsequent calls", async () => {
      const serverToken = "a".repeat(64);
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: serverToken }),
      });

      // First call - fetches from server
      const result1 = await getCSRFTokenAsync();
      expect(result1).toBe(serverToken);
      expect(mockFetch).toHaveBeenCalledTimes(1);

      // Simulate server setting cookie after first fetch
      mockCookie = `csrf-token=${serverToken}`;

      // Second call - returns cached value from cookie
      const result2 = await getCSRFTokenAsync();
      expect(result2).toBe(serverToken);
      expect(mockFetch).toHaveBeenCalledTimes(1); // No additional fetch
    });

    it("should deduplicate concurrent requests", async () => {
      const serverToken = "dedupe-token";
      let resolvePromise: (value: unknown) => void;
      const delayedResponse = new Promise((resolve) => {
        resolvePromise = resolve;
      });

      mockFetch.mockImplementationOnce(() => delayedResponse);

      // Start multiple concurrent requests
      const promise1 = getCSRFTokenAsync();
      const promise2 = getCSRFTokenAsync();
      const promise3 = getCSRFTokenAsync();

      // Resolve the fetch
      resolvePromise!({
        ok: true,
        json: async () => ({ csrfToken: serverToken }),
      });

      // All should resolve to the same token
      const [result1, result2, result3] = await Promise.all([
        promise1,
        promise2,
        promise3,
      ]);
      expect(result1).toBe(serverToken);
      expect(result2).toBe(serverToken);
      expect(result3).toBe(serverToken);

      // Only one fetch should have been made
      expect(mockFetch).toHaveBeenCalledTimes(1);
    });

    it("should return null on fetch error", async () => {
      mockFetch.mockRejectedValueOnce(new Error("Network error"));

      const result = await getCSRFTokenAsync();
      expect(result).toBeNull();
    });

    it("should return null on non-ok response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      });

      const result = await getCSRFTokenAsync();
      expect(result).toBeNull();
    });

    it("should allow retry after failed request", async () => {
      // First request fails
      mockFetch.mockRejectedValueOnce(new Error("Network error"));
      const result1 = await getCSRFTokenAsync();
      expect(result1).toBeNull();

      // Second request succeeds
      const serverToken = "retry-success";
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: serverToken }),
      });

      const result2 = await getCSRFTokenAsync();
      expect(result2).toBe(serverToken);
    });
  });

  describe("refreshCSRFToken", () => {
    it("should clear cache and fetch new token", async () => {
      // First, populate cache
      const oldToken = "old-token";
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: oldToken }),
      });
      await getCSRFTokenAsync();
      expect(mockFetch).toHaveBeenCalledTimes(1);

      // Now refresh
      const newToken = "new-token";
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: newToken }),
      });

      const result = await refreshCSRFToken();
      expect(result).toBe(newToken);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it("should clear cookie when refreshing", async () => {
      mockCookie = "csrf-token=old-value";

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: "new-token" }),
      });

      await refreshCSRFToken();
      // Cookie should have been cleared (our mock sets it to '')
      expect(mockCookie).toBe("");
    });

    it("should return null on refresh failure", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      });

      const result = await refreshCSRFToken();
      expect(result).toBeNull();
    });
  });

  describe("clearCSRFCache", () => {
    it("should clear cached token", async () => {
      // Populate cache
      const token = "cached";
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: token }),
      });
      await getCSRFTokenAsync();

      // Clear cache
      clearCSRFCache();

      // Next call should fetch again
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: "new-token" }),
      });
      await getCSRFTokenAsync();

      expect(mockFetch).toHaveBeenCalledTimes(2);
    });
  });

  describe("fetchWithCSRF", () => {
    const testUrl = "/api/test";

    beforeEach(() => {
      // Set up a valid CSRF token in cookie
      const validToken = "a".repeat(64);
      mockCookie = `csrf-token=${validToken}`;
    });

    it("should include CSRF token in headers", async () => {
      const validToken = "a".repeat(64);
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: async () => ({ success: true }),
      });

      await fetchWithCSRF(testUrl, { method: "POST" });

      expect(mockFetch).toHaveBeenCalledWith(
        testUrl,
        expect.objectContaining({
          headers: expect.any(Headers),
          credentials: "include",
        }),
      );

      // Check headers
      const callArgs = mockFetch.mock.calls[0];
      const headers = callArgs[1].headers as Headers;
      expect(headers.get("x-csrf-token")).toBe(validToken);
    });

    it("should set Content-Type to application/json by default", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

      await fetchWithCSRF(testUrl, { method: "POST" });

      const callArgs = mockFetch.mock.calls[0];
      const headers = callArgs[1].headers as Headers;
      expect(headers.get("Content-Type")).toBe("application/json");
    });

    it("should preserve custom headers", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

      await fetchWithCSRF(testUrl, {
        method: "POST",
        headers: { "X-Custom-Header": "custom-value" },
      });

      const callArgs = mockFetch.mock.calls[0];
      const headers = callArgs[1].headers as Headers;
      expect(headers.get("X-Custom-Header")).toBe("custom-value");
    });

    it("should retry on 403 CSRF error", async () => {
      const newToken = "b".repeat(64);

      // First request returns 403 with CSRF error
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 403,
        statusText: "Forbidden",
        headers: new Headers(),
        text: async () => "CSRF token invalid",
      });

      // Token refresh
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: newToken }),
      });

      // Retry request succeeds
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

      const response = await fetchWithCSRF(testUrl, { method: "POST" });

      expect(response.ok).toBe(true);
      expect(mockFetch).toHaveBeenCalledTimes(3); // Original + refresh + retry
    });

    it("should not retry on non-CSRF 403 error", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 403,
        statusText: "Forbidden",
        headers: new Headers(),
        text: async () => "Access denied - not authorized",
      });

      const response = await fetchWithCSRF(testUrl, { method: "POST" });

      expect(response.ok).toBe(false);
      expect(response.status).toBe(403);
      expect(mockFetch).toHaveBeenCalledTimes(1); // No retry
    });

    it("should return original response if token refresh fails", async () => {
      // First request returns 403 CSRF error
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 403,
        statusText: "Forbidden",
        headers: new Headers(),
        text: async () => "CSRF token invalid",
      });

      // Token refresh fails
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      });

      const response = await fetchWithCSRF(testUrl, { method: "POST" });

      expect(response.ok).toBe(false);
      expect(response.status).toBe(403);
      expect(mockFetch).toHaveBeenCalledTimes(2); // Original + failed refresh
    });

    it("should fetch token if not in cookie", async () => {
      mockCookie = ""; // Clear cookie
      const serverToken = "server-fetched-token";

      // Token fetch
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ csrfToken: serverToken }),
      });

      // Actual request
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      });

      await fetchWithCSRF(testUrl, { method: "POST" });

      expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + request
    });
  });

  describe("Network retry with fetchWithCSRF", () => {
    const testUrl = "/api/test";

    beforeEach(() => {
      const validToken = "a".repeat(64);
      mockCookie = `csrf-token=${validToken}`;
    });

    it("should propagate network errors", async () => {
      mockFetch.mockRejectedValueOnce(new Error("Network failure"));

      await expect(fetchWithCSRF(testUrl, { method: "POST" })).rejects.toThrow(
        "Network failure",
      );
    });
  });
});
