import { render, screen, waitFor, within, fireEvent, act } from '@testing-library/react';
import SubscriptionPage from '../page';

// Mock next/link
jest.mock('next/link', () => {
  return function MockLink({ children, href }: { children: React.ReactNode; href: string }) {
    return <a href={href}>{children}</a>;
  };
});

// Mock next-auth session as authenticated
jest.mock('next-auth/react', () => ({
  useSession: () => ({ data: { user: { id: 'u1' } }, status: 'authenticated' }),
}));

// Mock navigation hooks
const mockPush = jest.fn();
jest.mock('next/navigation', () => ({
  useRouter: () => ({ push: mockPush }),
  useSearchParams: () => ({ get: () => null }),
}));

// Mock plan-features helpers (avoid pulling unrelated logic into the render)
jest.mock('@/lib/plan-features', () => ({
  canSidegrade: () => false,
  isValidTier: () => false,
  BillingInterval: {},
}));

// Mock heavier child components so the page renders in isolation
jest.mock('@/components/subscription/PlanComparison', () => ({
  PlanComparison: () => <div data-testid="plan-comparison" />,
}));
jest.mock('@/components/subscription/SidegradeToggle', () => ({
  SidegradeToggle: () => <div data-testid="sidegrade-toggle" />,
}));

global.fetch = jest.fn();

const baseDealer = {
  id: 'd1',
  subscriptionTier: 'growth',
  currentBillingInterval: 'annual',
  hasCustomDomainAddon: false,
  customDomainAddonStatus: '',
  hasGrandfatheredCustomDomain: false,
};

function mockDealer(status: string) {
  (global.fetch as jest.Mock).mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ ...baseDealer, status }),
  });
}

// The dunning status alerts carry a stable test id on their container. The text
// portion is the role="alert" live region; the rescue button is a sibling outside
// the live region (interactive controls should not live inside an assertive
// live region). We scope CTA assertions to the container so we never collide with
// the always-present Billing Management feature buttons (which also open the portal).
function getDunningAlert() {
  return screen.queryByTestId('dunning-alert');
}

describe('SubscriptionPage dunning rescue CTA', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('renders the rescue CTA for payment_failed', async () => {
    mockDealer('payment_failed');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Action Required:/)).toBeInTheDocument();
    });
    const alert = getDunningAlert();
    expect(alert).toBeInTheDocument();
    expect(within(alert!).getByRole('button')).toBeInTheDocument();
  });

  it('does NOT render a self-serve billing CTA for suspended (admin-gated recovery)', async () => {
    mockDealer('suspended');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Account Suspended:/)).toBeInTheDocument();
    });
    const alert = getDunningAlert();
    expect(alert).toBeInTheDocument();
    // Suspension is an admin/violation action (and the final dunning state). Suspended dealers
    // must NOT get a one-click portal/pay CTA — recovery is gated through support only.
    expect(within(alert!).queryByRole('button')).not.toBeInTheDocument();
    expect(within(alert!).getByRole('link', { name: /contact support/i })).toBeInTheDocument();
  });

  it('does NOT render a dunning alert for active', async () => {
    mockDealer('active');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText('Current Subscription')).toBeInTheDocument();
    });
    expect(getDunningAlert()).not.toBeInTheDocument();
  });

  it('does NOT render a dunning alert for cancelled', async () => {
    mockDealer('cancelled');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Subscription Cancelled:/)).toBeInTheDocument();
    });
    expect(getDunningAlert()).not.toBeInTheDocument();
  });

  it('does NOT render a dunning alert for cancelled_pending', async () => {
    mockDealer('cancelled_pending');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Cancellation Pending:/)).toBeInTheDocument();
    });
    expect(getDunningAlert()).not.toBeInTheDocument();
  });

  it('suspended copy does not offer a self-serve pay-to-restore path', async () => {
    mockDealer('suspended');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Account Suspended:/)).toBeInTheDocument();
    });
    const alert = getDunningAlert()!;
    // Suspended dealers must not be told they can restore themselves by paying — no
    // "access is restored" promise and no pointer to the billing portal.
    expect(within(alert).queryByText(/access is restored/i)).not.toBeInTheDocument();
    expect(within(alert).queryByText(/billing portal/i)).not.toBeInTheDocument();
    expect(within(alert).queryByText(/outstanding invoice/i)).not.toBeInTheDocument();
  });

  it('suspended: support fallback links to the dashboard contact form, outside the live region', async () => {
    mockDealer('suspended');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Account Suspended:/)).toBeInTheDocument();
    });
    const container = getDunningAlert()!;
    // Hands off to the in-app support form (ticket workflow), not a raw mailto.
    const supportLink = within(container).getByRole('link', { name: /contact support/i });
    expect(supportLink).toHaveAttribute('href', '/dashboard#contact');
    // ...and it lives OUTSIDE the assertive live region, like the button.
    const liveRegion = within(container).getByRole('alert');
    expect(within(liveRegion).queryByRole('link')).not.toBeInTheDocument();
  });

  it('payment_failed copy mentions retry without claiming the card update triggers it', async () => {
    mockDealer('payment_failed');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Action Required:/)).toBeInTheDocument();
    });
    const alert = getDunningAlert()!;
    // Stripe is still on its Smart Retries schedule for past_due, so "retry" is accurate...
    expect(within(alert).getByText(/retry/i)).toBeInTheDocument();
    // ...but the copy must NOT imply the card update itself auto-collects the invoice.
    expect(within(alert).queryByText(/retry the open invoice automatically/i)).not.toBeInTheDocument();
  });

  it('exposes the loading state to assistive tech via aria-busy', async () => {
    mockDealer('payment_failed');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Action Required:/)).toBeInTheDocument();
    });
    const btn = within(getDunningAlert()!).getByRole('button');
    // Not loading yet: aria-busy must be present and false (announces in-progress later).
    expect(btn).toHaveAttribute('aria-busy', 'false');
  });

  it('keeps the rescue button OUT of the assertive live region', async () => {
    mockDealer('payment_failed');
    render(<SubscriptionPage />);
    await waitFor(() => {
      expect(screen.getByText(/Action Required:/)).toBeInTheDocument();
    });
    // The live region (role="alert") should announce text only, not wrap the button.
    const liveRegion = screen.getByRole('alert');
    expect(within(liveRegion).queryByRole('button')).not.toBeInTheDocument();
    // The button still exists, as a sibling inside the dunning container.
    expect(within(getDunningAlert()!).getByRole('button')).toBeInTheDocument();
  });

  // jsdom does not implement real navigation; assigning window.location.href emits
  // a "not implemented" console.error but does not throw. We silence that noise so
  // the meaningful (observable) assertion is the loading-state transition.
  function silenceJsdomNavigation() {
    const orig = console.error;
    const spy = jest.spyOn(console, 'error').mockImplementation((msg, ...rest) => {
      if (typeof msg === 'string' && /not implemented: navigation/i.test(msg)) return;
      // Preserve any other errors so real problems still surface.
      orig(msg, ...rest);
    });
    return () => spy.mockRestore();
  }

  it('flips to the loading state when the rescue button is clicked', async () => {
    mockDealer('payment_failed');
    const restore = silenceJsdomNavigation();

    try {
      render(<SubscriptionPage />);
      await waitFor(() => {
        expect(screen.getByText(/Action Required:/)).toBeInTheDocument();
      });
      const btn = within(getDunningAlert()!).getByRole('button');
      expect(btn).toHaveAttribute('aria-busy', 'false');

      fireEvent.click(btn);

      // The click must flip the button into its in-progress state for assistive tech.
      expect(btn).toHaveAttribute('aria-busy', 'true');
      expect(btn).toBeDisabled();
      expect(btn).toHaveTextContent(/Opening/);
    } finally {
      restore();
    }
  });

  it('resets the stuck loading state on bfcache restore (pageshow persisted)', async () => {
    mockDealer('payment_failed');
    const restore = silenceJsdomNavigation();

    try {
      render(<SubscriptionPage />);
      await waitFor(() => {
        expect(screen.getByText(/Action Required:/)).toBeInTheDocument();
      });
      const btn = within(getDunningAlert()!).getByRole('button');
      fireEvent.click(btn);
      expect(btn).toHaveAttribute('aria-busy', 'true');

      // Simulate a back-navigation restoring this page from the bfcache.
      act(() => {
        const evt = new Event('pageshow') as Event & { persisted: boolean };
        Object.defineProperty(evt, 'persisted', { value: true });
        window.dispatchEvent(evt);
      });

      expect(btn).toHaveAttribute('aria-busy', 'false');
      expect(btn).not.toBeDisabled();
      expect(btn).toHaveTextContent(/Update payment method/);
    } finally {
      restore();
    }
  });

  it('resets the stuck reactivating state on bfcache restore (pageshow persisted)', async () => {
    // handleReactivate sets reactivating=true then full-page-navigates to Stripe Checkout.
    // On a back-navigation the page restores from bfcache with reactivating still true, which
    // would leave the reactivate control stuck on "Reactivation in progress". The pageshow
    // handler must clear it. (Parallels the portal-button bfcache test above.)
    const restore = silenceJsdomNavigation();
    // The reactivate POST never resolves, so handleReactivate stays suspended at its `await`
    // with reactivating=true held (mirrors the real flow where the page navigates to Stripe
    // and is later restored from bfcache still in the in-progress state). This avoids jsdom's
    // navigation-not-implemented throw, which would otherwise reset the state via the catch.
    (global.fetch as jest.Mock).mockImplementation((url: string) => {
      if (typeof url === 'string' && url.includes('/api/subscription/reactivate')) {
        return new Promise(() => {});
      }
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ ...baseDealer, status: 'cancelled' }),
      });
    });

    try {
      render(<SubscriptionPage />);
      // baseDealer is non-starter with currentBillingInterval 'annual', so the interval
      // pre-selects and the reactivate button becomes enabled once the effect commits.
      await waitFor(() => {
        expect(
          screen.getByRole('button', { name: 'Reactivate your subscription' })
        ).toBeEnabled();
      });

      const btn = screen.getByRole('button', { name: 'Reactivate your subscription' });
      fireEvent.click(btn);

      // reactivating flips true → the button shows its in-progress state and is disabled.
      expect(btn).toHaveAttribute('aria-busy', 'true');
      expect(btn).toBeDisabled();
      expect(btn).toHaveTextContent(/Starting/);

      // Simulate a back-navigation restoring this page from the bfcache.
      act(() => {
        const evt = new Event('pageshow') as Event & { persisted: boolean };
        Object.defineProperty(evt, 'persisted', { value: true });
        window.dispatchEvent(evt);
      });

      // The stuck loading state is cleared: the button re-enables and shows its idle label.
      const after = screen.getByRole('button', { name: 'Reactivate your subscription' });
      expect(after).toHaveAttribute('aria-busy', 'false');
      expect(after).not.toBeDisabled();
      expect(after).toHaveTextContent(/Reactivate .* plan/);
    } finally {
      restore();
    }
  });
});
