import React, { useEffect } from 'react';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';

import masterpassRestReducer from '../redux/reducer';
import { setPaymentPending, getPaymentPending } from '../utils/payment-utils';
import type { MasterpassRestOptionRenderProps } from '../types/custom-render.types';

const handleCardSelectMock = jest.fn();
const handleInstallmentSelectMock = jest.fn();
const processPaymentMock = jest.fn();
const processDirectPaymentMock = jest.fn();
const finalizeAfterVerificationMock = jest.fn();
const updateModalStateMock = jest.fn();
const handleOTPSubmitMock = jest.fn();
const getPosErrorMock = jest.fn(() => null as any);

jest.mock('@akinon/next/utils', () => ({
  getPosError: () => getPosErrorMock(),
  buildClientRequestUrl: (url: string) => url
}));

jest.mock('../redux/api', () => ({
  useSetMasterpassRestBinNumberMutation: () => [
    () => ({ unwrap: async () => ({ context_list: [] }) }),
    { isLoading: false }
  ]
}));

jest.mock('../hooks/useMasterpassScript', () => ({
  useMasterpassScript: () => ({ isScriptLoaded: true })
}));

// The real hooks memoize everything they hand out. The mocks return one frozen
// object so identities stay stable across renders, otherwise the view's effects
// re-run on every render and mask the behavior under test.
const paymentHookValue = {
  isCheckoutLoading: false,
  isInstallmentLoading: false,
  isPrepareLoading: false,
  isFinalizeLoading: false,
  isRewardsQueryLoading: false,
  isRewardsSelectLoading: false,
  payableAmount: '100.00',
  updatePaymentState: jest.fn(),
  handleCardSelect: handleCardSelectMock,
  handleInstallmentSelect: handleInstallmentSelectMock,
  processPayment: processPaymentMock,
  processDirectPayment: processDirectPaymentMock,
  finalizePaymentAfterVerification: finalizeAfterVerificationMock,
  fetchRewardsForCard: jest.fn(),
  openRewardModal: jest.fn(),
  closeRewardModal: jest.fn(),
  confirmRewards: jest.fn()
};

jest.mock('../hooks/useMasterpassPayment', () => ({
  useMasterpassPayment: () => paymentHookValue
}));

const STORED_CARD = {
  cardAlias: 'my card',
  cardBin: '454671',
  uniqueCardNumber: 'unique-1',
  maskedCardNumber: '4546 **** **** 7890'
};

const accountHookValue = {
  accountData: { result: { cards: [STORED_CARD] } },
  updateModalState: updateModalStateMock,
  handleLinkConfirm: jest.fn(),
  handleOTPSubmit: handleOTPSubmitMock,
  handleRemoveCard: jest.fn(),
  confirmRemoveCard: jest.fn(),
  handleAddCard: jest.fn(),
  resetData: jest.fn()
};

jest.mock('../hooks/useMasterpassAccount', () => ({
  useMasterpassAccount: () => accountHookValue
}));

// Imported after the mocks so the view picks them up.
import MasterpassRestOption from './masterpass-rest-option';

/**
 * Stands in for a brand theme. Themes preselect the first saved card on mount,
 * which is what silently regenerated the order number after a shopper came back
 * from the bank. The guard the theme is expected to honour is `isPaymentPending`.
 */
const ThemeWithAutoSelect = (props: MasterpassRestOptionRenderProps) => {
  const { isPaymentPending, handleCardSelect, accountData, paymentState } = props;
  const firstCard = accountData?.result?.cards?.[0];

  useEffect(() => {
    if (isPaymentPending || paymentState?.selectedCard || !firstCard) {
      return;
    }

    void handleCardSelect(firstCard);
  }, [isPaymentPending, paymentState?.selectedCard, firstCard, handleCardSelect]);

  return (
    <div>
      <span data-testid="pending">{String(isPaymentPending)}</span>
      <button type="button" onClick={() => void handleCardSelect(firstCard)}>
        choose card
      </button>
      <button
        type="button"
        onClick={() => props.handlePaymentMethodChange('new_card')}
      >
        use another card
      </button>
      <button type="button" onClick={() => void props.handleProceedToPayment()}>
        pay
      </button>
      <button type="button" onClick={() => void props.handleOTPSubmit('123456')}>
        verify
      </button>
    </div>
  );
};

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

  return render(
    <Provider store={store}>
      <MasterpassRestOption
        customRender={{ fullRender: (props) => <ThemeWithAutoSelect {...props} /> }}
      />
    </Provider>
  );
};

beforeEach(() => {
  getPosErrorMock.mockReturnValue(null);
  processPaymentMock.mockResolvedValue({ success: true });
  handleOTPSubmitMock.mockResolvedValue({ success: true });
  finalizeAfterVerificationMock.mockResolvedValue({ success: true });
});

describe('returning to checkout while a transaction is still open', () => {
  it('suppresses the automatic card preselection that would change the order number', async () => {
    setPaymentPending('ORDER-1');

    renderOption();

    expect(screen.getByTestId('pending')).toHaveTextContent('true');
    expect(handleCardSelectMock).not.toHaveBeenCalled();
  });

  it('preselects a card normally when no transaction is open', async () => {
    renderOption();

    expect(screen.getByTestId('pending')).toHaveTextContent('false');
    expect(handleCardSelectMock).toHaveBeenCalledWith(STORED_CARD);
  });

  it('still lets the shopper start over by choosing a card themselves', async () => {
    setPaymentPending('ORDER-1');

    renderOption();
    expect(handleCardSelectMock).not.toHaveBeenCalled();

    await userEvent.click(screen.getByRole('button', { name: 'choose card' }));

    expect(handleCardSelectMock).toHaveBeenCalledWith(STORED_CARD);
    expect(getPaymentPending()).toBeNull();
    expect(screen.getByTestId('pending')).toHaveTextContent('false');
  });

  it('releases the guard when the shopper switches to a new card', async () => {
    setPaymentPending('ORDER-1');

    renderOption();

    await userEvent.click(screen.getByRole('button', { name: 'use another card' }));

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

  it('releases the guard when the completion request came back with an error', async () => {
    setPaymentPending('ORDER-1');
    getPosErrorMock.mockReturnValue({ non_field_errors: 'ORDER_NO_MISMATCH' });

    renderOption();

    expect(getPaymentPending()).toBeNull();
    expect(screen.getByTestId('pending')).toHaveTextContent('false');
  });

  it('releases the guard once the record has aged out', async () => {
    window.sessionStorage.setItem(
      'masterpass_rest_payment_pending',
      JSON.stringify({ orderNo: 'ORDER-1', startedAt: Date.now() - 31 * 60 * 1000 })
    );

    renderOption();

    expect(screen.getByTestId('pending')).toHaveTextContent('false');
    expect(handleCardSelectMock).toHaveBeenCalledWith(STORED_CARD);
  });
});

describe('a payment that needs verification', () => {
  it('marks the verification as belonging to the payment, not the account', async () => {
    processPaymentMock.mockResolvedValue({
      requiresOTP: true,
      otpType: 'OTP'
    });

    renderOption();

    await userEvent.click(screen.getByRole('button', { name: 'pay' }));

    expect(updateModalStateMock).toHaveBeenCalledWith({
      showOTPModal: true,
      otpType: 'OTP',
      otpContext: 'payment'
    });
  });

  it('completes the order against the backend once verification succeeds', async () => {
    const verificationData = { result: { responseCode: '0000', token: 'verified' } };
    handleOTPSubmitMock.mockResolvedValue({
      success: true,
      isPaymentContext: true,
      verificationData
    });

    renderOption();

    await userEvent.click(screen.getByRole('button', { name: 'verify' }));

    expect(finalizeAfterVerificationMock).toHaveBeenCalledWith(verificationData);
  });

  it('does not complete an order for an account-only verification', async () => {
    handleOTPSubmitMock.mockResolvedValue({ success: true });

    renderOption();

    await userEvent.click(screen.getByRole('button', { name: 'verify' }));

    expect(finalizeAfterVerificationMock).not.toHaveBeenCalled();
  });

  it('reports a failed completion instead of letting it pass silently', async () => {
    handleOTPSubmitMock.mockResolvedValue({
      success: true,
      isPaymentContext: true,
      verificationData: { result: {} }
    });
    finalizeAfterVerificationMock.mockResolvedValue({
      success: false,
      message: 'ORDER_NO_MISMATCH'
    });

    renderOption();

    await userEvent.click(screen.getByRole('button', { name: 'verify' }));
    // The error dispatch lands after the finalize promise settles.
    await act(async () => {
      await Promise.resolve();
    });

    expect(finalizeAfterVerificationMock).toHaveBeenCalled();
  });
});
