import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { Result } from "@/utils/result";
import { ApiError } from "@/data/services/api/api_result";

import {
  getPaymentIssueSubmitErrorLogDetails,
  SubscriptionPaymentIssueDialog,
} from "../subscription-payment-issue-dialog";
import { SubscriptionRenewalConfirmationDialog } from "../subscription-renewal-confirmation-dialog";

const mocks = vi.hoisted(() => ({
  submitFeedback: vi.fn(),
}));

vi.mock("@/lib/feedback", () => ({
  submitFeedback: mocks.submitFeedback,
}));

vi.mock("@/app/feedback/feedback-context", () => ({
  createFeedbackContext: () => ({
    appVersion: "test",
    platform: "web",
    browser: "Chrome",
    viewport: "390x844@3",
  }),
}));

describe("subscription payment dialogs", () => {
  let container: HTMLDivElement;
  let root: Root;

  beforeEach(() => {
    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
      .IS_REACT_ACT_ENVIRONMENT = true;
    mocks.submitFeedback.mockReset();
    container = document.createElement("div");
    document.body.appendChild(container);
    root = createRoot(container);
  });

  afterEach(() => {
    act(() => root.unmount());
    container.remove();
    document.body.querySelectorAll('[role="dialog"]').forEach((node) => node.remove());
  });

  it("confirms a VIP plan without submitting payment", () => {
    const onConfirm = vi.fn();
    const onCancel = vi.fn();
    act(() =>
      root.render(
        <SubscriptionRenewalConfirmationDialog
          open
          plan={{
            id: "vip_monthly",
            title: "Monthly",
            price: "19.90",
            currency: "usd",
            originalPrice: "39.90",
          }}
          onCancel={onCancel}
          onConfirm={onConfirm}
        />,
      ),
    );

    const dialog = document.body.querySelector('[role="dialog"]');
    expect(dialog?.textContent).toContain("Automatic Renewal Confirmation");
    expect(dialog?.textContent).toContain("Monthly");
    expect(dialog?.textContent).toContain("19.90 usd");
    expect(
      Array.from(dialog?.querySelectorAll("a") ?? []).map((link) =>
        link.getAttribute("href"),
      ),
    ).toEqual([
      "/legal/vip-membership-benefits.html",
      "/legal/automatic-renewal.html",
    ]);

    act(() => clickButton(dialog, "Confirm"));
    expect(onConfirm).toHaveBeenCalledOnce();
    expect(onCancel).not.toHaveBeenCalled();
  });

  it("submits Other as a Manager payment issue with one description field", async () => {
    mocks.submitFeedback.mockResolvedValue(
      Result.ok({ feedbackId: "feedback-payment-1" }),
    );
    const onClose = vi.fn();
    const onSubmitted = vi.fn();

    act(() =>
      root.render(
        <SubscriptionPaymentIssueDialog
          open
          subscriptionType="vip"
          planId="vip_monthly"
          orderId={null}
          payChannel="ezpay"
          countryCode="ID"
          characterId="maya"
          onClose={onClose}
          onSubmitted={onSubmitted}
        />,
      ),
    );

    const other = document.body.querySelector<HTMLInputElement>(
      'input[value="other"]',
    );
    expect(
      Array.from(document.body.querySelectorAll<HTMLInputElement>('input[type="radio"]')).map(
        (input) => input.parentElement?.textContent?.trim(),
      ),
    ).toEqual([
      "Concerned about card information security",
      "Insufficient card or wallet balance",
      "No supported payment method available",
      "Too expensive",
      "Other",
    ]);
    act(() => {
      other?.click();
    });
    const textarea = document.body.querySelector("textarea");
    expect(textarea?.getAttribute("placeholder")).toBe(
      "Please describe the payment problem you encountered.",
    );
    act(() => setTextareaValue(textarea, "QRIS did not open correctly."));

    await act(async () => {
      clickButton(document.body, "Submit");
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(mocks.submitFeedback).toHaveBeenCalledWith({
      category: "payment",
      content: "Payment issue: Other\nDetails: QRIS did not open correctly.",
      context: {
        appVersion: "test",
        platform: "web",
        browser: "Chrome",
        viewport: "390x844@3",
        sourcePage: "subscription",
        paymentIssueReason: "other",
        subscriptionType: "vip",
        planId: "vip_monthly",
        payChannel: "ezpay",
        countryCode: "ID",
        characterId: "maya",
      },
      images: [],
      idempotencyKey: expect.stringMatching(/^payment_feedback_[A-Za-z0-9_-]+$/),
    });
    expect(onSubmitted).toHaveBeenCalledWith(
      "Thanks. Your payment issue has been submitted.",
    );
    expect(onClose).toHaveBeenCalledOnce();
  });

  it("keeps a failed payment issue open and reuses its idempotency key for retry", async () => {
    mocks.submitFeedback
      .mockResolvedValueOnce(Result.err(new Error("backend unavailable")))
      .mockResolvedValueOnce(Result.ok({ feedbackId: "feedback-payment-2" }));
    const onClose = vi.fn();

    act(() =>
      root.render(
        <SubscriptionPaymentIssueDialog
          open
          subscriptionType="topup"
          planId="coin_1000"
          orderId="pay_pending_1"
          payChannel="stripe"
          countryCode="US"
          characterId="elio"
          onClose={onClose}
          onSubmitted={() => undefined}
        />,
      ),
    );

    const reason = document.body.querySelector<HTMLInputElement>(
      'input[value="tooExpensive"]',
    );
    act(() => reason?.click());
    await act(async () => {
      clickButton(document.body, "Submit");
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(document.body.textContent).toContain(
      "Unable to submit. Please try again.",
    );
    expect(onClose).not.toHaveBeenCalled();

    await act(async () => {
      clickButton(document.body, "Submit");
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(mocks.submitFeedback).toHaveBeenCalledTimes(2);
    expect(mocks.submitFeedback.mock.calls[0]?.[0].idempotencyKey).toBe(
      mocks.submitFeedback.mock.calls[1]?.[0].idempotencyKey,
    );
    expect(onClose).toHaveBeenCalledOnce();
  });

  it("keeps HTTP status and backend error code in payment issue failure diagnostics", () => {
    const failure = Result.err(
      new ApiError("HTTP_ERROR", "Invalid feedback context", 400, {
        detail: {
          message: "Invalid feedback context",
          errorCode: "INVALID_FEEDBACK_CONTEXT",
        },
      }),
    );

    if (failure.success) throw new Error("Expected failure result");
    expect(getPaymentIssueSubmitErrorLogDetails(failure.error)).toMatchObject({
      code: "UNKNOWN",
      message: "Invalid feedback context",
      causeName: "ApiError",
      causeMessage: "Invalid feedback context",
      httpStatus: 400,
      apiCode: "HTTP_ERROR",
      serverErrorCode: "INVALID_FEEDBACK_CONTEXT",
    });
  });
});

function clickButton(root: ParentNode | null, label: string): void {
  const button = Array.from(root?.querySelectorAll("button") ?? []).find(
    (item) => item.textContent?.trim() === label,
  );
  if (!button) throw new Error(`Expected ${label} button`);
  button.click();
}

function setTextareaValue(
  element: HTMLTextAreaElement | null,
  value: string,
): void {
  if (!element) throw new Error("Expected textarea");
  const setter = Object.getOwnPropertyDescriptor(
    HTMLTextAreaElement.prototype,
    "value",
  )?.set;
  setter?.call(element, value);
  element.dispatchEvent(new Event("input", { bubbles: true }));
}
