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 type { MasterpassRestOptionRenderProps } from '../types/custom-render.types';

/**
 * Regression cover for APS-4980.
 *
 * Reported request sequence, captured from a shopper who hit ORDER_NO_MISMATCH:
 *
 *   MasterpassRestBinNumberPage
 *   MasterpassRestInstallmentPage
 *   MasterpassRestOrderNoPage      <- order number the SDK transaction is bound to
 *   MasterpassRestBinNumberPage    <- backend regenerates the order number
 *   MasterpassRestInstallmentPage  <- backend regenerates it again
 *   MasterpassRestCompletePage     <- arrives with the order number from step 3
 *
 * Steps 4 and 5 are not shopper actions: they are the theme preselecting the
 * first saved card and the first installment when checkout mounts again after
 * the shopper leaves for 3D and comes back. These tests pin the request
 * sequence so that regression cannot return unnoticed.
 */

let pages: string[] = [];
let orderNoCounter = 0;
let currentOrderNo = '';

const recordPage = (page: string) => {
  pages.push(page);
};

// Mirrors the backend: every bin/installment call re-derives the order number.
const regenerateOrderNo = () => {
  orderNoCounter += 1;
  currentOrderNo = `ORDER-${orderNoCounter}`;
};

const completeCalls: Array<{ orderNoAtBackend: string }> = [];

jest.mock('../redux/api', () => ({
  useSetMasterpassRestBinNumberMutation: () => [
    () => ({
      unwrap: async () => {
        recordPage('MasterpassRestBinNumberPage');
        regenerateOrderNo();
        return {
          context_list: [
            {
              page_context: {
                installments: [{ pk: 1, installment_count: 1, label: 'Tek Çekim' }],
                card_type: { name: 'visa', slug: 'visa', logo: '' }
              }
            }
          ]
        };
      }
    }),
    { isLoading: false }
  ],
  useCheckoutMasterpassInstallmentMutation: () => [
    () => ({
      unwrap: async () => {
        recordPage('MasterpassRestInstallmentPage');
        regenerateOrderNo();
        return { context_list: [] };
      }
    }),
    { isLoading: false }
  ],
  usePrepareMasterpassOrderMutation: () => [
    () => ({
      unwrap: async () => {
        recordPage('MasterpassRestOrderNoPage');
        return {
          pre_order: { total_amount_with_interest: '100.00' },
          context_list: [{ page_context: { order_no: currentOrderNo, extras: {} } }]
        };
      }
    }),
    { isLoading: false }
  ],
  useFinalizeMasterpassOrderMutation: () => [
    () => ({
      unwrap: async () => {
        recordPage('MasterpassRestCompletePage');
        completeCalls.push({ orderNoAtBackend: currentOrderNo });
        return { errors: null };
      }
    }),
    { isLoading: false }
  ],
  useQueryMasterpassRewardsMutation: () => [
    () => ({ unwrap: async () => ({ context_list: [] }) }),
    { isLoading: false }
  ],
  useSelectMasterpassRewardsMutation: () => [
    () => ({ unwrap: async () => ({}) }),
    { isLoading: false }
  ]
}));

// The order number the SDK was handed when the transaction opened.
let orderNoHeldBySdk: string | null = null;
let sdkOutcome: any = { success: true };

jest.mock('../services/payment', () => ({
  PaymentService: class {
    async processPayment(request: any) {
      orderNoHeldBySdk = request.orderNo;
      return sdkOutcome;
    }
    async directPayment(request: any) {
      orderNoHeldBySdk = request.orderNo;
      return sdkOutcome;
    }
  }
}));

let responseHandlerResult: any = { success: true };
jest.mock('../utils/response-handler', () => ({
  handleMasterpassResponse: async () => responseHandlerResult,
  handleVerification: async () => ({ success: true, data: { result: {} } })
}));

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

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

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

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

const accountHookValue = {
  accountData: { result: { cards: [STORED_CARD] } },
  updateModalState: jest.fn(),
  handleLinkConfirm: jest.fn(),
  handleOTPSubmit: jest.fn(async () => ({ success: true })),
  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';

/** Reproduces the preselection a brand theme performs on mount. */
const ThemeWithAutoSelect = (props: MasterpassRestOptionRenderProps) => {
  const {
    isPaymentPending,
    handleCardSelect,
    handleInstallmentSelect,
    accountData,
    paymentState
  } = props;
  const firstCard = accountData?.result?.cards?.[0];

  useEffect(() => {
    if (isPaymentPending || paymentState?.selectedCard || !firstCard) {
      return;
    }
    void handleCardSelect(firstCard);
  }, [isPaymentPending, paymentState?.selectedCard, firstCard, handleCardSelect]);

  useEffect(() => {
    if (
      isPaymentPending ||
      paymentState?.selectedInstallment ||
      !paymentState?.installments?.length
    ) {
      return;
    }
    void handleInstallmentSelect(paymentState.installments[0]);
  }, [
    isPaymentPending,
    paymentState?.installments,
    paymentState?.selectedInstallment,
    handleInstallmentSelect
  ]);

  return (
    <>
      <button type="button" onClick={() => void handleCardSelect(firstCard)}>
        choose card
      </button>
      <button type="button" onClick={() => void props.handleProceedToPayment()}>
        pay
      </button>
    </>
  );
};

/** Each render is a fresh checkout mount, exactly like a page load. */
const mountCheckout = () =>
  render(
    <Provider
      store={configureStore({
        reducer: { masterpassRest: masterpassRestReducer },
        middleware: (getDefault) => getDefault({ serializableCheck: false })
      })}
    >
      <MasterpassRestOption
        customRender={{ fullRender: (props) => <ThemeWithAutoSelect {...props} /> }}
      />
    </Provider>
  );

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

const stubNavigation = () => {
  const location = window.location;
  delete (window as any).location;
  (window as any).location = { ...location, href: '' };
  return () => {
    (window as any).location = location;
  };
};

beforeEach(() => {
  pages = [];
  orderNoCounter = 0;
  currentOrderNo = '';
  completeCalls.length = 0;
  orderNoHeldBySdk = null;
  sdkOutcome = { success: true };
  responseHandlerResult = { success: true };
});

describe('APS-4980 request sequence', () => {
  it('does not re-derive the order number when checkout remounts after a 3D redirect', async () => {
    responseHandlerResult = {
      requires3D: true,
      redirectUrl: 'https://bank.example/3d'
    };
    const restoreNavigation = stubNavigation();

    const first = mountCheckout();
    await settle();

    expect(pages).toEqual([
      'MasterpassRestBinNumberPage',
      'MasterpassRestInstallmentPage'
    ]);

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

    expect(pages).toEqual([
      'MasterpassRestBinNumberPage',
      'MasterpassRestInstallmentPage',
      'MasterpassRestOrderNoPage'
    ]);
    const orderNoBoundToTransaction = orderNoHeldBySdk;
    expect(orderNoBoundToTransaction).toBe(currentOrderNo);

    // The shopper leaves for the bank and comes back: unmount, then mount again.
    first.unmount();
    mountCheckout();
    await settle();

    // The reported bug added two more pages here, moving the backend's order
    // number away from the one the open transaction carries.
    expect(pages).toEqual([
      'MasterpassRestBinNumberPage',
      'MasterpassRestInstallmentPage',
      'MasterpassRestOrderNoPage'
    ]);
    expect(currentOrderNo).toBe(orderNoBoundToTransaction);

    restoreNavigation();
  });

  it('reproduces the mismatch when the remount is left unguarded', async () => {
    // Same flow with the guard disabled, to show the test would have caught the
    // original defect rather than passing either way.
    responseHandlerResult = {
      requires3D: true,
      redirectUrl: 'https://bank.example/3d'
    };
    const restoreNavigation = stubNavigation();

    const UnguardedTheme = (props: MasterpassRestOptionRenderProps) => {
      const { handleCardSelect, accountData, paymentState } = props;
      const firstCard = accountData?.result?.cards?.[0];

      useEffect(() => {
        if (paymentState?.selectedCard || !firstCard) return;
        void handleCardSelect(firstCard);
      }, [paymentState?.selectedCard, firstCard, handleCardSelect]);

      return (
        <button type="button" onClick={() => void props.handleProceedToPayment()}>
          pay
        </button>
      );
    };

    const mountUnguarded = () =>
      render(
        <Provider
          store={configureStore({
            reducer: { masterpassRest: masterpassRestReducer },
            middleware: (getDefault) => getDefault({ serializableCheck: false })
          })}
        >
          <MasterpassRestOption
            customRender={{ fullRender: (props) => <UnguardedTheme {...props} /> }}
          />
        </Provider>
      );

    const first = mountUnguarded();
    await settle();
    await userEvent.click(screen.getByRole('button', { name: 'pay' }));
    await settle();

    const orderNoBoundToTransaction = orderNoHeldBySdk;

    first.unmount();
    mountUnguarded();
    await settle();

    expect(pages).toEqual([
      'MasterpassRestBinNumberPage',
      'MasterpassRestInstallmentPage',
      'MasterpassRestOrderNoPage',
      'MasterpassRestBinNumberPage',
      'MasterpassRestInstallmentPage'
    ]);
    expect(currentOrderNo).not.toBe(orderNoBoundToTransaction);

    restoreNavigation();
  });

  it('completes against the same order number the transaction was opened with', async () => {
    mountCheckout();
    await settle();

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

    expect(pages).toEqual([
      'MasterpassRestBinNumberPage',
      'MasterpassRestInstallmentPage',
      'MasterpassRestOrderNoPage',
      'MasterpassRestCompletePage'
    ]);
    expect(completeCalls).toHaveLength(1);
    expect(completeCalls[0].orderNoAtBackend).toBe(orderNoHeldBySdk);
  });

  it('leaves the shopper to pick a card before a new attempt can start', async () => {
    responseHandlerResult = {
      requires3D: true,
      redirectUrl: 'https://bank.example/3d'
    };
    const restoreNavigation = stubNavigation();

    const first = mountCheckout();
    await settle();
    await userEvent.click(screen.getByRole('button', { name: 'pay' }));
    await settle();

    first.unmount();
    mountCheckout();
    await settle();

    // Nothing is preselected, so paying straight away cannot open a second
    // transaction behind the one still in flight.
    await userEvent.click(screen.getByRole('button', { name: 'pay' }));
    await settle();

    expect(pages.filter((page) => page === 'MasterpassRestOrderNoPage')).toHaveLength(1);

    restoreNavigation();
  });

  it('re-derives the order number once the shopper deliberately retries', async () => {
    responseHandlerResult = {
      requires3D: true,
      redirectUrl: 'https://bank.example/3d'
    };
    const restoreNavigation = stubNavigation();

    const first = mountCheckout();
    await settle();
    await userEvent.click(screen.getByRole('button', { name: 'pay' }));
    await settle();

    first.unmount();

    // Choosing a card again is an explicit new attempt, so a fresh order number
    // is the correct outcome here.
    responseHandlerResult = { success: true };
    mountCheckout();
    await settle();
    await userEvent.click(screen.getByRole('button', { name: 'choose card' }));
    await settle();
    await userEvent.click(screen.getByRole('button', { name: 'pay' }));
    await settle();

    expect(pages.filter((page) => page === 'MasterpassRestOrderNoPage')).toHaveLength(2);
    expect(completeCalls[0].orderNoAtBackend).toBe(orderNoHeldBySdk);

    restoreNavigation();
  });
});
