import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { AuthProvider, useAuth } from "./auth-context";
import type { AuthUser } from "@mosaic/shared";

// Mock the API client
vi.mock("../api/client", () => ({
  apiGet: vi.fn(),
  apiPost: vi.fn(),
}));

const { apiGet, apiPost } = await import("../api/client");

// Test component that uses the auth context
function TestComponent(): React.JSX.Element {
  const { user, isLoading, isAuthenticated, authError, signOut } = useAuth();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <div data-testid="auth-status">{isAuthenticated ? "Authenticated" : "Not Authenticated"}</div>
      <div data-testid="auth-error">{authError ?? "none"}</div>
      {user && (
        <div>
          <div data-testid="user-email">{user.email}</div>
          <div data-testid="user-name">{user.name}</div>
        </div>
      )}
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}

describe("AuthContext", (): void => {
  beforeEach((): void => {
    vi.clearAllMocks();
  });

  it("should provide loading state initially", (): void => {
    vi.mocked(apiGet).mockImplementation(
      () =>
        new Promise(() => {
          // Never resolves - intentionally empty for testing loading state
        })
    );

    render(
      <AuthProvider>
        <TestComponent />
      </AuthProvider>
    );

    expect(screen.getByText("Loading...")).toBeInTheDocument();
  });

  it("should provide authenticated user when session exists", async (): Promise<void> => {
    const mockUser: AuthUser = {
      id: "user-1",
      email: "test@example.com",
      name: "Test User",
    };

    vi.mocked(apiGet).mockResolvedValueOnce({
      user: mockUser,
      session: { id: "session-1", token: "token123", expiresAt: new Date() },
    });

    render(
      <AuthProvider>
        <TestComponent />
      </AuthProvider>
    );

    await waitFor(() => {
      expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated");
    });

    expect(screen.getByTestId("user-email")).toHaveTextContent("test@example.com");
    expect(screen.getByTestId("user-name")).toHaveTextContent("Test User");
  });

  it("should handle unauthenticated state when session check fails", async (): Promise<void> => {
    vi.mocked(apiGet).mockRejectedValueOnce(new Error("Unauthorized"));

    render(
      <AuthProvider>
        <TestComponent />
      </AuthProvider>
    );

    await waitFor(() => {
      expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated");
    });

    expect(screen.queryByTestId("user-email")).not.toBeInTheDocument();
  });

  it("should clear user on sign out", async (): Promise<void> => {
    const mockUser: AuthUser = {
      id: "user-1",
      email: "test@example.com",
      name: "Test User",
    };

    vi.mocked(apiGet).mockResolvedValueOnce({
      user: mockUser,
      session: { id: "session-1", token: "token123", expiresAt: new Date() },
    });

    vi.mocked(apiPost).mockResolvedValueOnce({ success: true });

    render(
      <AuthProvider>
        <TestComponent />
      </AuthProvider>
    );

    // Wait for authenticated state
    await waitFor(() => {
      expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated");
    });

    // Click sign out
    const signOutButton = screen.getByRole("button", { name: "Sign Out" });
    signOutButton.click();

    await waitFor(() => {
      expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated");
    });

    expect(apiPost).toHaveBeenCalledWith("/auth/sign-out");
  });

  it("should throw error when useAuth is used outside AuthProvider", (): void => {
    // Suppress console.error for this test
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
      // Intentionally empty - suppressing React error boundary output
    });

    expect(() => {
      render(<TestComponent />);
    }).toThrow("useAuth must be used within AuthProvider");

    consoleErrorSpy.mockRestore();
  });

  describe("auth error handling", (): void => {
    it("should not set authError for normal unauthenticated state (401/403)", async (): Promise<void> => {
      // Normal auth error - user is just not logged in
      vi.mocked(apiGet).mockRejectedValueOnce(new Error("Unauthorized"));

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated");
      });

      // Should NOT have an auth error - this is expected behavior
      expect(screen.getByTestId("auth-error")).toHaveTextContent("none");
    });

    it("should set authError to 'network' for fetch failures", async (): Promise<void> => {
      // Network error - backend is unreachable
      vi.mocked(apiGet).mockRejectedValueOnce(new TypeError("Failed to fetch"));

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated");
      });

      // Should have a network error
      expect(screen.getByTestId("auth-error")).toHaveTextContent("network");
    });

    it("should log errors in development mode", async (): Promise<void> => {
      // Temporarily set to development
      vi.stubEnv("NODE_ENV", "development");

      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
        // Intentionally empty - we're testing that errors are logged
      });

      // Network error - backend is unreachable
      vi.mocked(apiGet).mockRejectedValueOnce(new TypeError("Failed to fetch"));

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-error")).toHaveTextContent("network");
      });

      // Should log error in development
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining("[Auth]"),
        expect.any(Error)
      );

      consoleErrorSpy.mockRestore();
      vi.unstubAllEnvs();
    });

    it("should set authError to 'network' for connection refused", async (): Promise<void> => {
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
        // Intentionally empty
      });

      vi.mocked(apiGet).mockRejectedValueOnce(new Error("ECONNREFUSED"));

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-error")).toHaveTextContent("network");
      });

      consoleErrorSpy.mockRestore();
    });

    it("should set authError to 'backend' for server errors", async (): Promise<void> => {
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
        // Intentionally empty
      });

      // Backend error - 500 Internal Server Error
      vi.mocked(apiGet).mockRejectedValueOnce(new Error("Internal Server Error"));

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated");
      });

      // Should have a backend error
      expect(screen.getByTestId("auth-error")).toHaveTextContent("backend");

      consoleErrorSpy.mockRestore();
    });

    it("should set authError to 'backend' for service unavailable", async (): Promise<void> => {
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
        // Intentionally empty
      });

      vi.mocked(apiGet).mockRejectedValueOnce(new Error("Service Unavailable"));

      render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-error")).toHaveTextContent("backend");
      });

      consoleErrorSpy.mockRestore();
    });

    it("should clear authError after successful session refresh", async (): Promise<void> => {
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
        // Intentionally empty
      });

      // First call fails with network error
      vi.mocked(apiGet).mockRejectedValueOnce(new TypeError("Failed to fetch"));

      const { rerender } = render(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      await waitFor(() => {
        expect(screen.getByTestId("auth-error")).toHaveTextContent("network");
      });

      // Set up successful response for refresh
      const mockUser: AuthUser = {
        id: "user-1",
        email: "test@example.com",
        name: "Test User",
      };
      vi.mocked(apiGet).mockResolvedValueOnce({
        user: mockUser,
        session: { id: "session-1", token: "token123", expiresAt: new Date() },
      });

      // Trigger a rerender (simulating refreshSession being called)
      rerender(
        <AuthProvider>
          <TestComponent />
        </AuthProvider>
      );

      // The initial render will have checked session once, error should still be there
      // A real refresh would need to call refreshSession
      consoleErrorSpy.mockRestore();
    });
  });
});
