import { describe, expect, it, vi } from "vitest";
import type { BillingCheckoutStatus } from "../data/repos";
import {
  billingErrorMessage,
  isSafeBillingUrl,
  pollCheckout,
  startBillingRedirect,
} from "./billingCheckout";

function checkout(overrides: Partial<BillingCheckoutStatus> = {}): BillingCheckoutStatus {
  return {
    id: "local_1",
    plan_id: "starter",
    checkout_status: "complete",
    payment_status: "paid",
    fulfillment_status: "pending",
    ...overrides,
  };
}

describe("billing URL validation", () => {
  it("allows HTTPS and only permits localhost HTTP in development", () => {
    expect(isSafeBillingUrl("https://checkout.stripe.com/pay/cs_1", false)).toBe(true);
    expect(isSafeBillingUrl("http://localhost:4242/pay", true)).toBe(true);
    expect(isSafeBillingUrl("http://127.0.0.1:4242/pay", true)).toBe(true);
    expect(isSafeBillingUrl("http://localhost:4242/pay", false)).toBe(false);
    expect(isSafeBillingUrl("http://stripe.example/pay", true)).toBe(false);
    expect(isSafeBillingUrl("javascript:alert(1)", true)).toBe(false);
  });
});

describe("billing errors", () => {
  const t = (key: string) => key;

  it("maps transport, availability, and auth failures to localized keys", () => {
    expect(billingErrorMessage({ code: 0, httpStatus: 0 }, t, "checkout.fail")).toBe("checkout.networkError");
    expect(billingErrorMessage({ code: 503001, httpStatus: 503 }, t, "checkout.fail")).toBe("checkout.unavailable");
    expect(billingErrorMessage({ code: 401000, httpStatus: 401 }, t, "checkout.fail")).toBe("checkout.sessionExpired");
    expect(billingErrorMessage({ code: 409081, httpStatus: 409 }, t, "checkout.fail")).toBe("checkout.portalUnavailable");
    expect(billingErrorMessage(new Error("raw backend text"), t, "checkout.fail")).toBe("checkout.fail");
  });
});

describe("billing redirect", () => {
  it("reports an obvious failure when the browser stays on the page", () => {
    vi.useFakeTimers();
    const failed = vi.fn();
    startBillingRedirect(() => undefined, failed, 100);
    vi.advanceTimersByTime(100);
    expect(failed).toHaveBeenCalledOnce();
    vi.useRealTimers();
  });

  it("reports synchronous navigation failures immediately", () => {
    const failed = vi.fn();
    startBillingRedirect(() => {
      throw new Error("blocked");
    }, failed);
    expect(failed).toHaveBeenCalledOnce();
  });

  it("does not report failure after the page starts unloading", () => {
    vi.useFakeTimers();
    const failed = vi.fn();
    startBillingRedirect(() => window.dispatchEvent(new Event("pagehide")), failed, 100);
    vi.advanceTimersByTime(100);
    expect(failed).not.toHaveBeenCalled();
    vi.useRealTimers();
  });
});

describe("checkout polling", () => {
  it("waits until fulfillment, not merely successful payment", async () => {
    const getCheckout = vi
      .fn<() => Promise<BillingCheckoutStatus>>()
      .mockResolvedValueOnce(checkout())
      .mockResolvedValueOnce(checkout({ fulfillment_status: "fulfilled" }));
    const result = await pollCheckout(getCheckout, { sleep: async () => undefined });
    expect(result?.outcome).toBe("fulfilled");
    expect(getCheckout).toHaveBeenCalledTimes(2);
  });

  it("returns terminal failures immediately", async () => {
    const result = await pollCheckout(
      async () => checkout({ checkout_status: "expired", payment_status: "unpaid" }),
      { sleep: async () => undefined },
    );
    expect(result?.outcome).toBe("failed");
  });

  it("times out after the configured duration", async () => {
    const getCheckout = vi.fn(async () => checkout());
    const result = await pollCheckout(getCheckout, {
      intervalMs: 1_500,
      timeoutMs: 3_000,
      sleep: async () => undefined,
    });
    expect(result?.outcome).toBe("timeout");
    expect(getCheckout).toHaveBeenCalledTimes(3);
  });
});
