import React from 'react';
import { renderHook, act } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';

import masterpassRestReducer from '../redux/reducer';
import { getPaymentPending, setPaymentPending } from '../utils/payment-utils';

const prepareMock = jest.fn();
const finalizeMock = jest.fn();
const binNumberMock = jest.fn();
const installmentMock = jest.fn();
const processPaymentMock = jest.fn();
const directPaymentMock = jest.fn();
const handleResponseMock = jest.fn();

const asMutation = (fn: jest.Mock) => () =>
  [(arg: unknown) => ({ unwrap: () => fn(arg) }), { isLoading: false }];

jest.mock('../redux/api', () => ({
  usePrepareMasterpassOrderMutation: asMutation(prepareMock),
  useFinalizeMasterpassOrderMutation: asMutation(finalizeMock),
  useSetMasterpassRestBinNumberMutation: asMutation(binNumberMock),
  useCheckoutMasterpassInstallmentMutation: asMutation(installmentMock),
  useQueryMasterpassRewardsMutation: asMutation(jest.fn()),
  useSelectMasterpassRewardsMutation: asMutation(jest.fn())
}));

jest.mock('../services/payment', () => ({
  PaymentService: class {
    processPayment = processPaymentMock;
    directPayment = directPaymentMock;
  }
}));

jest.mock('../utils/response-handler', () => ({
  handleMasterpassResponse: (...args: unknown[]) => handleResponseMock(...args)
}));

jest.mock('./useMasterpassToken', () => ({
  useMasterpassToken: () => ({
    refetch: jest.fn(async () => ({ data: { token: 'fresh-token' } }))
  })
}));

// Imported after the mocks so the hook picks them up.
import { useMasterpassPayment } from './useMasterpassPayment';

const PREPARED_ORDER = {
  pre_order: { total_amount_with_interest: '100.00' },
  context_list: [{ page_context: { order_no: 'ORDER-1', extras: {} } }]
};

const SELECTED_CARD = { cardAlias: 'my card', cardBin: '454671' };
const SELECTED_INSTALLMENT = { pk: 1, installment_count: 1 };

const NEW_CARD_FORM = {
  cardNumber: '4546 7112 3456 7890',
  cardholderName: 'Test User',
  expiryDate: '12/30',
  cvv: '123'
};

const renderPaymentHook = ({ withNewCardForm = false } = {}) => {
  const store = configureStore({
    reducer: { masterpassRest: masterpassRestReducer },
    middleware: (getDefault) => getDefault({ serializableCheck: false })
  });

  store.dispatch({
    type: 'masterpassRest/updatePaymentState',
    payload: {
      selectedCard: SELECTED_CARD,
      selectedInstallment: SELECTED_INSTALLMENT,
      cvc: '123',
      useThreeD: true
    }
  });

  if (withNewCardForm) {
    store.dispatch({
      type: 'masterpassRest/setNewCardFormData',
      payload: NEW_CARD_FORM
    });
  }

  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <Provider store={store}>{children}</Provider>
  );

  return { ...renderHook(() => useMasterpassPayment(), { wrapper }), store };
};

beforeEach(() => {
  prepareMock.mockResolvedValue(PREPARED_ORDER);
  finalizeMock.mockResolvedValue({ errors: null });
  processPaymentMock.mockResolvedValue({ success: true, data: { result: {} } });
  directPaymentMock.mockResolvedValue({ success: true, data: { result: {} } });
  handleResponseMock.mockResolvedValue({ success: true });
});

describe('order number lifecycle during payment', () => {
  it('marks the order number pending as soon as it is bound to a transaction', async () => {
    // The SDK call never resolves, so the assertion sees the state the user is
    // in while the bank page is open.
    processPaymentMock.mockImplementation(() => new Promise(() => {}));

    const { result } = renderPaymentHook();

    act(() => {
      void result.current.processPayment();
    });

    await act(async () => {
      await Promise.resolve();
    });

    expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
  });

  it('releases the order number once the backend has completed the order', async () => {
    const { result } = renderPaymentHook();

    await act(async () => {
      await result.current.processPayment();
    });

    expect(finalizeMock).toHaveBeenCalled();
    expect(getPaymentPending()).toBeNull();
  });

  it('keeps the order number pending while the shopper is redirected to 3D', async () => {
    handleResponseMock.mockResolvedValue({
      requires3D: true,
      redirectUrl: 'https://bank.example/3d'
    });

    // jsdom refuses real navigation; the assignment itself is not under test.
    const location = window.location;
    delete (window as any).location;
    (window as any).location = { ...location, href: '' };

    const { result } = renderPaymentHook();

    let outcome: any;
    await act(async () => {
      outcome = await result.current.processPayment();
    });

    expect(outcome.requiresRedirect).toBe(true);
    expect(getPaymentPending()?.orderNo).toBe('ORDER-1');

    (window as any).location = location;
  });

  it('keeps the order number pending while a verification step is open', async () => {
    handleResponseMock.mockResolvedValue({
      success: false,
      requiresOTP: true,
      otpType: 'OTP'
    });

    const { result } = renderPaymentHook();

    await act(async () => {
      await result.current.processPayment();
    });

    expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
  });

  it('releases the order number when the payment is rejected outright', async () => {
    handleResponseMock.mockResolvedValue({
      success: false,
      message: 'Insufficient funds'
    });

    const { result } = renderPaymentHook();

    await act(async () => {
      await result.current.processPayment();
    });

    expect(getPaymentPending()).toBeNull();
  });

  it('releases the order number when the flow throws', async () => {
    prepareMock.mockRejectedValue(new Error('network down'));
    setPaymentPending('STALE-ORDER');

    const { result } = renderPaymentHook();

    await act(async () => {
      await result.current.processPayment();
    });

    expect(getPaymentPending()).toBeNull();
  });

  it('marks the order number pending on the new-card flow too', async () => {
    directPaymentMock.mockImplementation(() => new Promise(() => {}));

    const { result } = renderPaymentHook({ withNewCardForm: true });

    act(() => {
      void result.current.processDirectPayment();
    });

    await act(async () => {
      await Promise.resolve();
    });

    expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
  });
});

describe('completing a payment after a verification step', () => {
  it('sends the verification result to the backend and releases the order number', async () => {
    setPaymentPending('ORDER-1');

    const { result } = renderPaymentHook();

    let outcome: any;
    await act(async () => {
      outcome = await result.current.finalizePaymentAfterVerification({
        result: { responseCode: '0000', token: 'verified-token' }
      });
    });

    expect(finalizeMock).toHaveBeenCalledWith({
      responseCode: '0000',
      token: 'verified-token',
      three_d_secure: true,
      transactionType: 'PURCHASE_3D'
    });
    expect(outcome.success).toBe(true);
    expect(getPaymentPending()).toBeNull();
  });

  it('surfaces backend errors instead of reporting success', async () => {
    finalizeMock.mockResolvedValue({ errors: { non_field_errors: 'ORDER_NO_MISMATCH' } });

    const { result } = renderPaymentHook();

    let outcome: any;
    await act(async () => {
      outcome = await result.current.finalizePaymentAfterVerification({
        result: { responseCode: '0000', token: 'verified-token' }
      });
    });

    expect(outcome.success).toBe(false);
    expect(outcome.message).toEqual({ non_field_errors: 'ORDER_NO_MISMATCH' });
  });
});
