import { describe, it, expect, vi, afterEach } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";

import {
  useIsAtFreeLimit,
  useSubscription,
  useTrialDaysRemaining,
  type SubscriptionState,
} from "@/lib/billing";
import { api } from "@/lib/api";

/**
 * Tests unitaires sur la logique billing côté SPA.
 *
 *  - useSubscription : query hook qui appelle /api/v1/billing/subscription
 *  - useIsAtFreeLimit : helper qui dérive un booléen pour bloquer l'UI
 *
 * On mock `api.get` pour éviter de toucher la network. Le wrapper QueryClient
 * a `retry: false` pour fail-fast en cas de mock manquant.
 */

function makeWrapper() {
  const qc = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }: { children: ReactNode }) => (
    <QueryClientProvider client={qc}>{children}</QueryClientProvider>
  );
}

function fakeState(overrides: Partial<SubscriptionState> = {}): SubscriptionState {
  return {
    plan: "free",
    caps: {
      activeInvoicesLimit: 2,
      seatsLimit: 1,
      multiUsers: false,
      replyFromUserEmail: false,
      smsEnabled: false,
    },
    activeInvoicesCount: 0,
    inGracePeriod: false,
    gracePeriodEndsAt: null,
    subscriptionStatus: null,
    inTrial: false,
    trialEndsAt: null,
    billingCycle: null,
    currentPeriodEnd: null,
    hasStripeCustomer: false,
    cancelAtPeriodEnd: false,
    ...overrides,
  };
}

afterEach(() => {
  vi.restoreAllMocks();
});

describe("useSubscription", () => {
  it("appelle l'endpoint /billing/subscription et renvoie le state", async () => {
    const state = fakeState({ plan: "pro", inGracePeriod: false });
    vi.spyOn(api, "get").mockResolvedValue(state);

    const { result } = renderHook(() => useSubscription(), {
      wrapper: makeWrapper(),
    });

    await waitFor(() => expect(result.current.data).toBeDefined());
    expect(result.current.data?.plan).toBe("pro");
    expect(api.get).toHaveBeenCalledWith("/api/v1/billing/subscription");
  });
});

describe("useIsAtFreeLimit", () => {
  it("retourne false en l'absence de données (loading)", () => {
    vi.spyOn(api, "get").mockImplementation(() => new Promise(() => {})); // jamais résolu

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    expect(result.current).toBe(false);
  });

  it("retourne false sur un plan Pro même avec beaucoup de factures", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "pro",
        caps: {
          activeInvoicesLimit: null,
          seatsLimit: 1,
          multiUsers: false,
          replyFromUserEmail: false,
          smsEnabled: false,
        },
        activeInvoicesCount: 200,
      }),
    );

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    // Petit délai pour laisser le state propagate après la mock resolve
    await waitFor(() => expect(result.current).toBe(false));
  });

  it("retourne false en grace period même si activeCount > limite", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        activeInvoicesCount: 12,
        inGracePeriod: true,
        gracePeriodEndsAt: "2099-01-01T00:00:00.000Z",
      }),
    );

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    await waitFor(() => expect(result.current).toBe(false));
  });

  it("retourne true sur Free post-grace + activeCount >= limit", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        activeInvoicesCount: 2,
        inGracePeriod: false,
      }),
    );

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(result.current).toBe(true));
  });

  it("retourne true même si activeCount > limit (over)", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        activeInvoicesCount: 8,
        inGracePeriod: false,
      }),
    );

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(result.current).toBe(true));
  });

  it("retourne false sur Free post-grace mais activeCount < limit (1 < 2)", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        activeInvoicesCount: 1,
        inGracePeriod: false,
      }),
    );

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    await waitFor(() => expect(result.current).toBe(false));
  });

  it("retourne false pendant l'essai 14 j (inTrial=true bypass le quota)", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        activeInvoicesCount: 10,
        inGracePeriod: false,
        inTrial: true,
        trialEndsAt: new Date(Date.now() + 7 * 24 * 3600_000).toISOString(),
        subscriptionStatus: "trialing",
      }),
    );

    const { result } = renderHook(() => useIsAtFreeLimit(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    await waitFor(() => expect(result.current).toBe(false));
  });
});

describe("useTrialDaysRemaining", () => {
  it("retourne null hors essai", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({ plan: "free", inTrial: false }),
    );
    const { result } = renderHook(() => useTrialDaysRemaining(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    await waitFor(() => expect(result.current).toBe(null));
  });

  it("retourne le nombre de jours arrondi sup quand essai actif", async () => {
    const sevenDays = 7 * 24 * 3600 * 1000;
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        inTrial: true,
        // 6.5 jours → arrondi sup = 7
        trialEndsAt: new Date(Date.now() + sevenDays - 12 * 3600 * 1000).toISOString(),
        subscriptionStatus: "trialing",
      }),
    );
    const { result } = renderHook(() => useTrialDaysRemaining(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    await waitFor(() => expect(result.current).toBe(7));
  });

  it("retourne 0 si trialEndsAt déjà passé (garde-fou)", async () => {
    vi.spyOn(api, "get").mockResolvedValue(
      fakeState({
        plan: "free",
        inTrial: true,
        trialEndsAt: new Date(Date.now() - 1000).toISOString(),
        subscriptionStatus: "trialing",
      }),
    );
    const { result } = renderHook(() => useTrialDaysRemaining(), {
      wrapper: makeWrapper(),
    });
    await waitFor(() => expect(api.get).toHaveBeenCalled());
    await waitFor(() => expect(result.current).toBe(0));
  });
});
