import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { AdminPayoutsClient } from '../admin-payouts-client';
import type { AdminPayout } from '@alqove/api-client';

vi.mock('next/navigation', () => ({
  useRouter: vi.fn(),
  usePathname: vi.fn(() => '/admin/payouts'),
  useSearchParams: vi.fn(),
}));

const listPayoutsMock = vi.fn();
const retryPayoutMock = vi.fn();
const voidPayoutMock = vi.fn();
vi.mock('@/lib/api', () => ({
  api: {
    admin: {
      financials: {
        listPayouts: (...a: unknown[]) => listPayoutsMock(...a),
        retryPayout: (...a: unknown[]) => retryPayoutMock(...a),
        voidPayout: (...a: unknown[]) => voidPayoutMock(...a),
      },
    },
  },
}));

function wrap(node: React.ReactNode) {
  const qc = new QueryClient({
    defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
  });
  return <QueryClientProvider client={qc}>{node}</QueryClientProvider>;
}

const failedPayout: AdminPayout = {
  id: 'pay-1',
  store_id: 'store-1',
  store_name: 'Northern Atelier',
  state: 'failed',
  period_start: '2026-04-01T00:00:00Z',
  period_end: '2026-04-30T00:00:00Z',
  scheduled_for: '2026-05-02T00:00:00Z',
  gross_cents: 12500,
  debits_cents: 350,
  net_cents: 12150,
  stripe_transfer_id: null,
  transferred_at: null,
  failed_at: '2026-05-02T01:00:00Z',
  failure_reason: 'insufficient_funds',
  retries: 2,
  voided_at: null,
  voided_by: null,
  resolution_note: null,
  created_at: '2026-04-30T00:00:00Z',
  updated_at: '2026-05-02T01:00:00Z',
};

function paginatedResponse(
  rows: AdminPayout[],
  overrides: Partial<{
    current_page: number;
    last_page: number;
    total: number;
  }> = {},
) {
  return {
    data: rows,
    meta: {
      current_page: overrides.current_page ?? 1,
      last_page: overrides.last_page ?? 1,
      per_page: 20,
      total: overrides.total ?? rows.length,
      from: rows.length ? 1 : null,
      to: rows.length || null,
    },
    links: { first: null, last: null, prev: null, next: null },
  };
}

describe('AdminPayoutsClient', () => {
  const replace = vi.fn();
  let currentParams = new URLSearchParams('');

  beforeEach(() => {
    listPayoutsMock.mockReset();
    retryPayoutMock.mockReset();
    voidPayoutMock.mockReset();
    replace.mockReset();
    currentParams = new URLSearchParams('');
    vi.mocked(useRouter).mockReturnValue({
      replace: (url: string) => {
        replace(url);
        const qs = url.split('?')[1] ?? '';
        currentParams = new URLSearchParams(qs);
      },
      push: vi.fn(),
      back: vi.fn(),
      forward: vi.fn(),
      refresh: vi.fn(),
      prefetch: vi.fn(),
    } as unknown as ReturnType<typeof useRouter>);
    vi.mocked(useSearchParams).mockImplementation(
      () => currentParams as unknown as ReturnType<typeof useSearchParams>,
    );
    vi.mocked(usePathname).mockReturnValue('/admin/payouts');
  });

  it('defaults to the "failed" filter and passes it to the API', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    await waitFor(() => expect(listPayoutsMock).toHaveBeenCalled());
    expect(listPayoutsMock.mock.calls[0][0].state).toBe('failed');
  });

  it('renders rows with state badge, store name, and failure reason', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    await waitFor(() =>
      expect(
        screen.getByTestId('admin-payouts-row-pay-1'),
      ).toBeInTheDocument(),
    );
    expect(screen.getByText('Northern Atelier')).toBeInTheDocument();
    expect(screen.getByText('insufficient_funds')).toBeInTheDocument();
    expect(screen.getByTestId('payout-state-badge').textContent).toBe('Failed');
  });

  it('renders empty state', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([]));
    render(wrap(<AdminPayoutsClient />));
    expect(
      await screen.findByTestId('admin-payouts-empty'),
    ).toBeInTheDocument();
  });

  it('renders loading state', () => {
    listPayoutsMock.mockReturnValue(new Promise(() => {}));
    render(wrap(<AdminPayoutsClient />));
    expect(screen.getByTestId('admin-payouts-loading')).toBeInTheDocument();
  });

  it('clicking a filter chip updates the URL', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(screen.getByTestId('admin-payouts-filter-scheduled'));
    expect(replace).toHaveBeenCalledWith('/admin/payouts?state=scheduled');
  });

  it('"Failed" chip clears the state param (default)', async () => {
    currentParams = new URLSearchParams('state=scheduled');
    listPayoutsMock.mockResolvedValue(paginatedResponse([]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(screen.getByTestId('admin-payouts-filter-failed'));
    expect(replace).toHaveBeenCalledWith('/admin/payouts');
  });

  it('renders Retry and Void buttons for failed payouts', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    await waitFor(() =>
      expect(
        screen.getByTestId('admin-payouts-retry-pay-1'),
      ).toBeInTheDocument(),
    );
    expect(
      screen.getByTestId('admin-payouts-void-pay-1'),
    ).toBeInTheDocument();
  });

  it('only renders Void for scheduled payouts (no Retry)', async () => {
    listPayoutsMock.mockResolvedValue(
      paginatedResponse([{ ...failedPayout, state: 'scheduled', id: 'pay-s' }]),
    );
    render(wrap(<AdminPayoutsClient />));
    await waitFor(() =>
      expect(
        screen.getByTestId('admin-payouts-void-pay-s'),
      ).toBeInTheDocument(),
    );
    expect(
      screen.queryByTestId('admin-payouts-retry-pay-s'),
    ).not.toBeInTheDocument();
  });

  it('clicking Retry opens the retry modal with optional note', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-retry-pay-1'));
    expect(screen.getByRole('dialog', { name: /Retry payout/i })).toBeInTheDocument();
    // Submit button enabled even with no note
    expect(
      screen.getByTestId('admin-payout-action-submit'),
    ).toBeEnabled();
  });

  it('clicking Void disables submit until a 10-char note is typed', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-void-pay-1'));
    const submit = screen.getByTestId('admin-payout-action-submit');
    expect(submit).toBeDisabled();
    fireEvent.change(screen.getByTestId('admin-payout-action-note'), {
      target: { value: 'short' },
    });
    expect(submit).toBeDisabled();
    fireEvent.change(screen.getByTestId('admin-payout-action-note'), {
      target: { value: 'long enough resolution note' },
    });
    expect(submit).toBeEnabled();
  });

  it('submitting Retry calls api.admin.financials.retryPayout', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    retryPayoutMock.mockResolvedValue({ data: { ...failedPayout, state: 'scheduled' } });
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-retry-pay-1'));
    fireEvent.change(screen.getByTestId('admin-payout-action-note'), {
      target: { value: 'manual retry after Stripe fix' },
    });
    fireEvent.click(screen.getByTestId('admin-payout-action-submit'));
    await waitFor(() => expect(retryPayoutMock).toHaveBeenCalled());
    const [payoutId, body] = retryPayoutMock.mock.calls[0];
    expect(payoutId).toBe('pay-1');
    expect(body.resolution_note).toBe('manual retry after Stripe fix');
  });

  it('submitting Void calls api.admin.financials.voidPayout with note', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    voidPayoutMock.mockResolvedValue({ data: { ...failedPayout, state: 'void' } });
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-void-pay-1'));
    fireEvent.change(screen.getByTestId('admin-payout-action-note'), {
      target: { value: 'voided after seller agreement' },
    });
    fireEvent.click(screen.getByTestId('admin-payout-action-submit'));
    await waitFor(() => expect(voidPayoutMock).toHaveBeenCalled());
    const [payoutId, body] = voidPayoutMock.mock.calls[0];
    expect(payoutId).toBe('pay-1');
    expect(body.resolution_note).toBe('voided after seller agreement');
  });

  it('retry dialog shows the previous failure_reason', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-retry-pay-1'));
    const note = await screen.findByTestId(
      'admin-payout-action-failure-reason',
    );
    expect(note.textContent).toMatch(/insufficient_funds/);
  });

  it('void dialog does not show the failure_reason preamble', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-void-pay-1'));
    expect(
      screen.queryByTestId('admin-payout-action-failure-reason'),
    ).not.toBeInTheDocument();
  });

  it('Escape closes the retry dialog', async () => {
    listPayoutsMock.mockResolvedValue(paginatedResponse([failedPayout]));
    render(wrap(<AdminPayoutsClient />));
    fireEvent.click(await screen.findByTestId('admin-payouts-retry-pay-1'));
    expect(
      screen.getByRole('dialog', { name: /Retry payout/i }),
    ).toBeInTheDocument();
    fireEvent.keyDown(document, { key: 'Escape' });
    await waitFor(() =>
      expect(
        screen.queryByRole('dialog', { name: /Retry payout/i }),
      ).not.toBeInTheDocument(),
    );
  });
});
