import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Session, AuthError } from "@supabase/supabase-js";
import { supabase } from "@app/auth/supabase";
import { expectConsole } from "@app/tests/failOnConsole";

// Mock supabase
vi.mock("@app/auth/supabase", () => ({
  supabase: {
    auth: {
      getSession: vi.fn(),
      refreshSession: vi.fn(),
    },
  },
}));

// Stub apiClient's heavy UI/util deps so importing it stays cheap. None are
// exercised here (toast, plan settings and handleHttpError paths aren't hit),
// and re-importing the real toast graph per test made these tests time out.
vi.mock("@app/components/toast", () => ({ alert: vi.fn() }));
vi.mock("@app/utils/appSettings", () => ({ openPlanSettings: vi.fn() }));
vi.mock("@app/services/httpErrorHandler", () => ({
  handleHttpError: vi.fn().mockResolvedValue(undefined),
}));

describe("apiClient", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Reset modules to get fresh instance of apiClient
    vi.resetModules();
  });

  it("should add JWT token to request headers when session exists", async () => {
    const mockToken = "test-jwt-token-12345";
    const mockSession = {
      access_token: mockToken,
      refresh_token: "refresh-token",
      expires_in: 3600,
      token_type: "bearer",
      user: { id: "user-123" },
    } as unknown as Session;

    // Mock getSession to return a session with token
    vi.mocked(supabase.auth.getSession).mockResolvedValue({
      data: { session: mockSession },
      error: null,
    });

    // Import apiClient after mocking
    const { default: apiClient } = await import("@app/services/apiClient");

    // Create a mock adapter to intercept the request
    const mockAdapter = vi.fn((config) => {
      // Verify the Authorization header is set correctly
      expect(config.headers.Authorization).toBe(`Bearer ${mockToken}`);
      return Promise.resolve({
        data: { success: true },
        status: 200,
        statusText: "OK",
        headers: {},
        config,
      });
    });

    // Replace the adapter
    apiClient.defaults.adapter = mockAdapter;

    // Make a test request
    await apiClient.get("/api/v1/test");

    // Verify the request was made with the token
    expect(mockAdapter).toHaveBeenCalled();
    expect(supabase.auth.getSession).toHaveBeenCalled();
  });

  it("should handle requests when no session exists", async () => {
    // Mock getSession to return no session
    vi.mocked(supabase.auth.getSession).mockResolvedValue({
      data: { session: null },
      error: null,
    });

    // Import apiClient after mocking
    const { default: apiClient } = await import("@app/services/apiClient");

    // Create a mock adapter to intercept the request
    const mockAdapter = vi.fn((config) => {
      // Verify no Authorization header is set
      expect(config.headers.Authorization).toBeUndefined();
      return Promise.resolve({
        data: { success: true },
        status: 200,
        statusText: "OK",
        headers: {},
        config,
      });
    });

    // Replace the adapter
    apiClient.defaults.adapter = mockAdapter;

    // Make a test request
    await apiClient.get("/api/v1/test");

    // Verify the request was made without a token
    expect(mockAdapter).toHaveBeenCalled();
    expect(supabase.auth.getSession).toHaveBeenCalled();
  });

  it("should refresh token on 401 response", async () => {
    const oldToken = "old-token";
    const newToken = "new-refreshed-token";

    const oldSession = {
      access_token: oldToken,
      refresh_token: "refresh-token",
      expires_in: 3600,
      token_type: "bearer",
      user: { id: "user-123" },
    } as unknown as Session;

    const newSession = {
      access_token: newToken,
      refresh_token: "new-refresh-token",
      expires_in: 3600,
      token_type: "bearer",
      user: { id: "user-123" },
    } as unknown as Session;

    // Mock initial session for first request
    let getSessionCallCount = 0;
    vi.mocked(supabase.auth.getSession).mockImplementation(async () => {
      getSessionCallCount++;
      // First call returns old session, subsequent calls return new session
      if (getSessionCallCount === 1) {
        return { data: { session: oldSession }, error: null };
      }
      return { data: { session: newSession }, error: null };
    });

    // Mock refresh to return new session
    vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
      data: { user: null, session: newSession },
      error: null,
    });

    // Import apiClient after mocking
    const { default: apiClient } = await import("@app/services/apiClient");

    let requestCount = 0;
    const mockAdapter = vi.fn((config) => {
      requestCount++;

      // First request returns 401
      if (requestCount === 1) {
        // Verify first request has old token
        expect(config.headers.Authorization).toBe(`Bearer ${oldToken}`);
        const error = Object.assign(new Error("Unauthorized"), {
          response: { status: 401, data: { error: "Unauthorized" } },
          config,
        });
        return Promise.reject(error);
      }

      // Second request (after refresh) should have new token
      // The interceptor will call getSession again, which now returns the new session
      expect(config.headers.Authorization).toBe(`Bearer ${newToken}`);
      return Promise.resolve({
        data: { success: true },
        status: 200,
        statusText: "OK",
        headers: {},
        config,
      });
    });

    // Replace the adapter
    apiClient.defaults.adapter = mockAdapter;

    // Make a test request that will trigger 401 and retry
    const response = await apiClient.get("/api/v1/test");

    // Verify the token was refreshed and request retried
    expect(response.data).toEqual({ success: true });
    expect(supabase.auth.refreshSession).toHaveBeenCalled();
    expect(mockAdapter).toHaveBeenCalledTimes(2);
    expect(getSessionCallCount).toBe(3); // Called for initial request, for checking if refresh is possible, and for retry
  });

  it("should handle refresh token failure", async () => {
    expectConsole.error(/\[API Client\] Token refresh failed/);
    const oldToken = "old-token";

    const oldSession = {
      access_token: oldToken,
      user: { id: "user-123" },
    };

    // Mock initial session
    vi.mocked(supabase.auth.getSession).mockResolvedValue({
      data: { session: oldSession as unknown as Session },
      error: null,
    });
    vi.mocked(supabase.auth.getSession).mockResolvedValue({
      data: { session: oldSession as unknown as Session },
      error: null,
    });

    // Mock refresh to fail
    vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
      data: { user: null, session: null },
      error: {
        name: "AuthError",
        message: "Refresh failed",
        status: 400,
        code: "auth_error",
      } as unknown as AuthError,
    });

    // Import apiClient after mocking
    const { default: apiClient } = await import("@app/services/apiClient");

    // Mock window.location for redirect test
    Object.defineProperty(window, "location", {
      writable: true,
      value: { href: "" },
    });

    const mockAdapter = vi.fn((config) => {
      // Always return 401 to trigger refresh
      const error = Object.assign(new Error("Unauthorized"), {
        response: { status: 401, data: { error: "Unauthorized" } },
        config,
      });
      return Promise.reject(error);
    });

    // Replace the adapter
    apiClient.defaults.adapter = mockAdapter;

    // Make a test request that will trigger 401
    try {
      await apiClient.get("/api/v1/test");
      // Should not reach here
      expect(true).toBe(false);
    } catch (_) {
      // Verify refresh was attempted
      expect(supabase.auth.refreshSession).toHaveBeenCalled();
      // Verify redirect to login
      expect(window.location.href).toBe("/login");
    }
  });
});
