import * as Sentry from "@sentry/nextjs";
import { render, screen, waitFor } from "@testing-library/react";
import { usePathname, useRouter } from "next/navigation";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useHasPermissions, useSession } from "@/lib/auth/auth.query";
import { usePublicConfig } from "@/lib/config/config.query";
import { WithAuthCheck } from "./with-auth-check";

// Mock Sentry
vi.mock("@sentry/nextjs", () => ({
  setUser: vi.fn(),
}));

// Mock Next.js router and navigation
vi.mock("next/navigation");

// Mock auth query
vi.mock("@/lib/auth/auth.query");

// Mock public config query (dev auto-login disabled by default in tests)
vi.mock("@/lib/config/config.query", () => ({
  usePublicConfig: vi.fn(() => ({
    data: { devAutoLoginEnabled: false },
    isLoading: false,
  })),
}));

// Provide a QueryClient stand-in so useQueryClient() doesn't require a provider
vi.mock("@tanstack/react-query", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@tanstack/react-query")>();
  return {
    ...actual,
    useQueryClient: vi.fn(() => ({ invalidateQueries: vi.fn() })),
  };
});

// Mock shared module
vi.mock("@archestra/shared", () => ({
  requiredPagePermissionsMap: {
    "/protected": { "organization:read": ["read"] },
    "/admin": { "organization:write": ["write"] },
  },
}));

const mockRouterPush = vi.fn();
const MockChild = () => (
  <div data-testid="protected-content">Protected Content</div>
);

// Helper to set window.location for tests
const setWindowLocation = (pathname: string, search = "") => {
  Object.defineProperty(window, "location", {
    value: {
      pathname,
      search,
      href: `http://localhost${pathname}${search}`,
    },
    writable: true,
  });
};

describe("WithAuthCheck", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(useRouter).mockReturnValue({
      push: mockRouterPush,
    } as unknown as ReturnType<typeof useRouter>);
    vi.mocked(useHasPermissions).mockReturnValue({
      data: true,
      isPending: false,
    } as ReturnType<typeof useHasPermissions>);
    // Reset window.location to default state
    setWindowLocation("/", "");
    window.sessionStorage.clear();
  });

  describe("when user is not authenticated", () => {
    beforeEach(() => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
    });

    it("should redirect to sign-in with redirectTo param when accessing protected page", () => {
      vi.mocked(usePathname).mockReturnValue("/dashboard");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith(
        "/auth/sign-in?redirectTo=%2Fdashboard",
      );
    });

    it("should redirect to sign-in with encoded redirectTo param for complex paths", () => {
      vi.mocked(usePathname).mockReturnValue("/settings/teams/123");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith(
        "/auth/sign-in?redirectTo=%2Fsettings%2Fteams%2F123",
      );
    });

    it("should preserve query parameters in redirectTo param", () => {
      vi.mocked(usePathname).mockReturnValue("/search");
      setWindowLocation("/search", "?q=hello&filter=active");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith(
        "/auth/sign-in?redirectTo=%2Fsearch%3Fq%3Dhello%26filter%3Dactive",
      );
    });

    it("should not add ? when there are no query parameters", () => {
      vi.mocked(usePathname).mockReturnValue("/dashboard");
      setWindowLocation("/dashboard", "");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith(
        "/auth/sign-in?redirectTo=%2Fdashboard",
      );
    });

    it("should allow access to auth pages", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-in");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });

    it("should allow access to sign-out page without adding redirectTo", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-out");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      // Should not redirect at all - sign-out is an auth page
      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });

    it("should allow access to IdP-initiated SSO route without adding redirectTo", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sso/Okta");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });
  });

  describe("when user is authenticated", () => {
    beforeEach(() => {
      vi.mocked(useSession).mockReturnValue({
        data: {
          user: { id: "user123", email: "test@example.com" },
          session: { id: "session123" },
        },
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
    });

    it("should redirect to home when accessing auth pages without redirectTo", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-in");
      setWindowLocation("/auth/sign-in", "");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith("/");
    });

    it("should redirect to home when redirectTo is empty string", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-in");
      setWindowLocation("/auth/sign-in", "?redirectTo=");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith("/");
    });

    it("should redirect to redirectTo param when accessing auth pages after login", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-in");
      setWindowLocation("/auth/sign-in", "?redirectTo=%2Fllm%2Flogs");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith("/llm/logs");
    });

    it("should ignore malicious redirectTo param and redirect to home", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-in");
      const maliciousUrl = encodeURIComponent("https://evil.com/phishing");
      setWindowLocation("/auth/sign-in", `?redirectTo=${maliciousUrl}`);

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).toHaveBeenCalledWith("/");
    });

    it("should allow access to unprotected pages", () => {
      vi.mocked(usePathname).mockReturnValue("/dashboard");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });

    it("should allow access to sign-out page when authenticated", () => {
      vi.mocked(usePathname).mockReturnValue("/auth/sign-out");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      // Sign-out is a special auth page - should not redirect
      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });
  });

  describe("when auth check is pending", () => {
    beforeEach(() => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: true,
      } as unknown as ReturnType<typeof useSession>);
    });

    it("should render nothing while checking auth", () => {
      vi.mocked(usePathname).mockReturnValue("/dashboard");

      const { container } = render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(container.firstChild).toBeNull();
      expect(mockRouterPush).not.toHaveBeenCalled();
    });
  });

  describe("special auth pages (/auth/two-factor)", () => {
    it("should allow access to /auth/two-factor when not authenticated (2FA verification during login)", () => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
      vi.mocked(usePathname).mockReturnValue("/auth/two-factor");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });

    it("should allow access to /auth/two-factor when authenticated (2FA setup)", () => {
      vi.mocked(useSession).mockReturnValue({
        data: {
          user: { id: "user123", email: "test@example.com" },
          session: { id: "session123" },
        },
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
      vi.mocked(usePathname).mockReturnValue("/auth/two-factor");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });

    it("should allow access to /auth/recover-account when not authenticated (backup-code sign-in)", () => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
      vi.mocked(usePathname).mockReturnValue("/auth/recover-account");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });

    it("should allow access to /auth/two-factor sub-paths", () => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
      vi.mocked(usePathname).mockReturnValue("/auth/two-factor/setup");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      expect(mockRouterPush).not.toHaveBeenCalled();
      expect(screen.getByTestId("protected-content")).toBeInTheDocument();
    });
  });

  describe("Sentry user context", () => {
    it("should set Sentry user context when user is authenticated", async () => {
      const mockUser = {
        id: "user123",
        email: "test@example.com",
        name: "Test User",
      };

      vi.mocked(useSession).mockReturnValue({
        data: {
          user: mockUser,
          session: { id: "session123" },
        },
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);

      vi.mocked(usePathname).mockReturnValue("/dashboard");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      await waitFor(() => {
        expect(Sentry.setUser).toHaveBeenCalledWith({
          id: "user123",
          email: "test@example.com",
          username: "Test User",
        });
      });
    });

    it("should use email as username when name is not available", async () => {
      const mockUser = {
        id: "user456",
        email: "another@example.com",
      };

      vi.mocked(useSession).mockReturnValue({
        data: {
          user: mockUser,
          session: { id: "session456" },
        },
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);

      vi.mocked(usePathname).mockReturnValue("/dashboard");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      await waitFor(() => {
        expect(Sentry.setUser).toHaveBeenCalledWith({
          id: "user456",
          email: "another@example.com",
          username: "another@example.com",
        });
      });
    });

    it("should clear Sentry user context when user is not authenticated", async () => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);

      vi.mocked(usePathname).mockReturnValue("/auth/sign-in");

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      await waitFor(() => {
        expect(Sentry.setUser).toHaveBeenCalledWith(null);
      });
    });

    it("should handle Sentry errors silently", () => {
      vi.mocked(Sentry.setUser).mockImplementationOnce(() => {
        throw new Error("Sentry error");
      });

      vi.mocked(useSession).mockReturnValue({
        data: {
          user: { id: "user789", email: "error@example.com" },
          session: { id: "session789" },
        },
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);

      vi.mocked(usePathname).mockReturnValue("/dashboard");

      // Should not throw
      expect(() => {
        render(
          <WithAuthCheck>
            <MockChild />
          </WithAuthCheck>,
        );
      }).not.toThrow();
    });
  });

  describe("developer auto-login", () => {
    beforeEach(() => {
      vi.mocked(useSession).mockReturnValue({
        data: null,
        isPending: false,
      } as unknown as ReturnType<typeof useSession>);
    });

    afterEach(() => {
      // Restore the default (disabled) so other suites are unaffected.
      vi.mocked(usePublicConfig).mockReturnValue({
        data: { devAutoLoginEnabled: false },
        isLoading: false,
      } as unknown as ReturnType<typeof usePublicConfig>);
      vi.unstubAllGlobals();
    });

    it("calls the dev-auto-login endpoint when enabled and unauthenticated", async () => {
      vi.mocked(usePublicConfig).mockReturnValue({
        data: { devAutoLoginEnabled: true },
        isLoading: false,
      } as unknown as ReturnType<typeof usePublicConfig>);
      vi.mocked(usePathname).mockReturnValue("/dashboard");
      const fetchMock = vi.fn().mockResolvedValue({ ok: true });
      vi.stubGlobal("fetch", fetchMock);

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      await waitFor(() => {
        expect(fetchMock).toHaveBeenCalledWith("/api/auth/dev-auto-login", {
          method: "POST",
        });
      });
      // Endpoint is hit exactly once (guarded by a ref) rather than the login
      // form being shown.
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("falls back to the sign-in redirect when auto-login fails", async () => {
      vi.mocked(usePublicConfig).mockReturnValue({
        data: { devAutoLoginEnabled: true },
        isLoading: false,
      } as unknown as ReturnType<typeof usePublicConfig>);
      vi.mocked(usePathname).mockReturnValue("/dashboard");
      vi.stubGlobal(
        "fetch",
        vi.fn().mockResolvedValue({ ok: false, status: 500 }),
      );

      render(
        <WithAuthCheck>
          <MockChild />
        </WithAuthCheck>,
      );

      await waitFor(() => {
        expect(mockRouterPush).toHaveBeenCalledWith(
          "/auth/sign-in?redirectTo=%2Fdashboard",
        );
      });
    });
  });
});
