package com.braintree.customer.service;

import com.braintree.command.result.BrainTreeAddressResult;
import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.customer.dao.BrainTreeCustomerAccountDao;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.model.BrainTreePaymentInfoModel;
import com.braintreegateway.Address;
import de.hybris.bootstrap.annotations.UnitTest;
import de.hybris.platform.commerceservices.customer.DuplicateUidException;
import de.hybris.platform.commerceservices.customer.dao.CustomerAccountDao;
import de.hybris.platform.commerceservices.enums.CustomerType;
import de.hybris.platform.commerceservices.event.DeletedAddressEvent;
import de.hybris.platform.commerceservices.event.SavedAddressEvent;
import de.hybris.platform.core.model.c2l.CountryModel;
import de.hybris.platform.core.model.c2l.RegionModel;
import de.hybris.platform.core.model.order.OrderModel;
import de.hybris.platform.core.model.order.payment.PaymentInfoModel;
import de.hybris.platform.core.model.user.AddressModel;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.core.model.user.UserModel;
import de.hybris.platform.orderscheduling.model.CartToOrderCronJobModel;
import de.hybris.platform.servicelayer.event.EventService;
import de.hybris.platform.servicelayer.exceptions.ModelSavingException;
import de.hybris.platform.servicelayer.i18n.CommonI18NService;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.session.SessionService;
import de.hybris.platform.servicelayer.user.UserService;
import de.hybris.platform.site.BaseSiteService;
import de.hybris.platform.store.BaseStoreModel;
import de.hybris.platform.store.services.BaseStoreService;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Arrays;
import java.util.Optional;

import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;


@UnitTest
@RunWith(MockitoJUnitRunner.class)
public class BrainTreeCustomerAccountServiceTest {

    private static final String BRAINTREE_LEGACY_CUSTOMER_ID = "BraintreeCustomerId";
    private static final String BRAINTREE_GRAPHQL_CUSTOMER_ID = "BraintreeCustomerId";
    private static final String ADDRESS_ID = "AddressId";
    private static final String PWD = "pwd";
    private static final String ORDER_GUID = "orderGUID";
    private static final String CODE = "123456";
    private static final String ID = "ID";

    @Mock
    private CustomerModel customerModel;
    @Mock
    private BrainTreePaymentInfoModel brainTreePaymentInfoModel;
    @Mock
    private AddressModel addressModel;
    @Mock
    private CountryModel countryModel;
    @Mock
    private RegionModel regionModel;
    @Mock
    private OrderModel orderModel;
    @Mock
    private BaseStoreModel baseStoreModel;
    @Mock
    private BrainTreeAddressResult addressResult;
    @Mock
    private Address address;
    @Mock
    UserModel userModel;

    @Mock
    private BrainTreeConfigService brainTreeConfigService;
    @Mock
    private ModelService modelService;
    @Mock
    private BrainTreeCustomerAccountDao brainTreeCustomerAccountDao;
    @Mock
    private UserService userService;
    @Mock
    private CustomerAccountDao customerAccountDao;
    @Mock
    private BrainTreePaymentService brainTreePaymentService;
    @Mock
    private BaseStoreService baseStoreService;
    @Mock
    private CommonI18NService commonI18NService;
    @Mock
    private SessionService sessionService;
    @Mock
    private BaseSiteService baseSiteService;
    @Mock
    private EventService eventService;

    @Spy
    @InjectMocks
    private final BrainTreeCustomerAccountService unit = new BrainTreeCustomerAccountService();

    @Test
    public void shouldGetBrainTreePaymentInfoForCode() {
        when(brainTreeCustomerAccountDao.findBrainTreePaymentInfoByCustomer(customerModel, CODE))
            .thenReturn(brainTreePaymentInfoModel);

        final BrainTreePaymentInfoModel result = unit.getBrainTreePaymentInfoForCode(customerModel, CODE);

        assertEquals(brainTreePaymentInfoModel, result);
    }

    @Test
    public void shouldGetCorrectPaymentInfoForCode() {
        when(brainTreeCustomerAccountDao.findPaymentInfoByCustomer(customerModel, CODE))
            .thenReturn(brainTreePaymentInfoModel);

        final PaymentInfoModel result = unit.getPaymentInfoForCode(customerModel, CODE);

        assertEquals(brainTreePaymentInfoModel, result);
    }

    @Test
    public void shouldGetBrainTreePaymentInfos() {
        final List<BrainTreePaymentInfoModel> brainTreePaymentInfoModels = new ArrayList<BrainTreePaymentInfoModel>();

        when(brainTreeCustomerAccountDao.findBrainTreePaymentInfosByCustomer(customerModel, true))
            .thenReturn(brainTreePaymentInfoModels);

        final List<BrainTreePaymentInfoModel> result = unit.getBrainTreePaymentInfos(customerModel, true);

        assertEquals(brainTreePaymentInfoModels, result);
    }

    @Test
    public void shouldUnlinkCCPaymentInfo() {
        final Collection<PaymentInfoModel> paymentInfoList = Arrays.asList(brainTreePaymentInfoModel);
        final Collection<PaymentInfoModel> paymentInfoListWithoutBTPaymentModel = new ArrayList<>(paymentInfoList);
        paymentInfoListWithoutBTPaymentModel.remove(brainTreePaymentInfoModel);

        when(customerModel.getPaymentInfos()).thenReturn(paymentInfoList);

        unit.unlinkCCPaymentInfo(customerModel, brainTreePaymentInfoModel);

        final InOrder inOrder = inOrder(customerModel, modelService);
        inOrder.verify(customerModel).setPaymentInfos(paymentInfoListWithoutBTPaymentModel);
        inOrder.verify(modelService).save(customerModel);
        inOrder.verify(modelService).refresh(customerModel);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionWhenNoPaymentInfo() {
        final Collection<PaymentInfoModel> paymentInfoList = new ArrayList<>();

        when(customerModel.getPaymentInfos()).thenReturn(paymentInfoList);

        unit.unlinkCCPaymentInfo(customerModel, brainTreePaymentInfoModel);
    }

    @Test
    public void shouldDeleteAddressEntryShouldCallRemoveMethodAndPublishEvent() {
        final List<AddressModel> addressModels = Arrays.asList(addressModel);

        when(customerModel.getAddresses()).thenReturn(addressModels);
        when(addressModel.getBrainTreeAddressId()).thenReturn(BRAINTREE_LEGACY_CUSTOMER_ID);
        when(addressModel.getCountry()).thenReturn(countryModel);
        when(addressModel.getRegion()).thenReturn(regionModel);

        unit.deleteAddressEntry(customerModel, addressModel);
        verify(brainTreePaymentService).removeAddress(any());
        verify(eventService).publishEvent(any(DeletedAddressEvent.class));
    }

    @Test
    public void shouldNotDeleteAddressEntryOnBraintreeSide() {
        final List<AddressModel> addressModels = Arrays.asList(addressModel);

        when(customerModel.getAddresses()).thenReturn(addressModels);
        when(addressModel.getBrainTreeAddressId()).thenReturn("");

        unit.deleteAddressEntry(customerModel, addressModel);

        verify(brainTreePaymentService, never()).removeAddress(any());
        verify(eventService).publishEvent(any(DeletedAddressEvent.class));
    }

    @Test
    public void saveAddressEntryShouldOnlyPublishEvent() {
        unit.saveAddressEntry(customerModel, addressModel);
        verify(eventService).publishEvent(any(SavedAddressEvent.class));
        verify(brainTreePaymentService, never()).createAddress(any(), any());
    }

    @Test
    public void shouldOnlyPublishEvent() {
        when(addressModel.getShippingAddress()).thenReturn(true);
        when(brainTreeConfigService.isStoreInVaultForCurrentUser()).thenReturn(false);

        unit.saveAddressEntry(customerModel, addressModel);

        verify(addressModel).getShippingAddress();
        verify(brainTreeConfigService).isStoreInVaultForCurrentUser();
        verify(eventService).publishEvent(any(SavedAddressEvent.class));
        verify(brainTreePaymentService, never()).createAddress(any(), any());
    }

    @Test
    public void saveAddressEntryShouldOnlyPublishEventAndSaveOnBrainTreeSide() {
        when(addressModel.getShippingAddress()).thenReturn(true);
        when(brainTreeConfigService.isStoreInVaultForCurrentUser()).thenReturn(Boolean.TRUE);
        when(address.getId()).thenReturn(ADDRESS_ID);
        when(addressResult.getAddress()).thenReturn(address);
        when(brainTreePaymentService.createAddress(any(), any())).thenReturn(addressResult);

        unit.saveAddressEntry(customerModel, addressModel);

        verify(addressModel).setBrainTreeAddressId(ADDRESS_ID);
        verify(eventService).publishEvent(any(SavedAddressEvent.class));
        verify(brainTreePaymentService).createAddress(any(), any());
    }

    @Test(expected = IllegalArgumentException.class)
    public void deleteBrainTreePaymentInfoShouldThrowExceptionIfPaymentInfoDoNotBelongCustomer() {
        unit.deleteBrainTreePaymentInfo(customerModel, brainTreePaymentInfoModel);
    }

    @Test
    public void deleteBrainTreePaymentInfo() {
        final List<PaymentInfoModel> infoModels = Arrays.asList(brainTreePaymentInfoModel);

        when(customerModel.getPaymentInfos()).thenReturn(infoModels);

        unit.deleteBrainTreePaymentInfo(customerModel, brainTreePaymentInfoModel);

        verify(brainTreePaymentInfoModel).setSaved(false);
        verify(modelService).save(brainTreePaymentInfoModel);
    }

    @Test
    public void shouldCancelAssociatedReplenishmentsWhenDeleteBrainTreePaymentInfo() {
        final CartToOrderCronJobModel cartToOrderCronJobModel = mock(CartToOrderCronJobModel.class);
        final List<PaymentInfoModel> infoModels = Arrays.asList(brainTreePaymentInfoModel);

        when(customerModel.getPaymentInfos()).thenReturn(infoModels);
        when(brainTreePaymentInfoModel.getCartToOrderCronJob()).thenReturn(List.of(cartToOrderCronJobModel));

        unit.deleteBrainTreePaymentInfo(customerModel, brainTreePaymentInfoModel);

        verify(cartToOrderCronJobModel).setActive(false);
    }

    @Test
    public void convertGuestToCustomer() throws DuplicateUidException {
        when(baseStoreService.getCurrentBaseStore()).thenReturn(baseStoreModel);
        when(customerModel.getBraintreeCustomerId()).thenReturn(BRAINTREE_LEGACY_CUSTOMER_ID);
        when(customerModel.getType()).thenReturn(CustomerType.GUEST);
        when(userService.getCurrentUser()).thenReturn(customerModel);

        when(orderModel.getUser()).thenReturn(customerModel);
        when(orderModel.getDeliveryAddress()).thenReturn(addressModel);
        when(customerAccountDao.findOrderByGUIDAndStore(ORDER_GUID, baseStoreModel, null)).thenReturn(orderModel);

        when(brainTreeConfigService.isStoreInVault()).thenReturn(Boolean.TRUE);
        when(address.getId()).thenReturn(ADDRESS_ID);
        when(addressResult.getAddress()).thenReturn(address);

        when(brainTreePaymentService.createAddress(any(), any())).thenReturn(addressResult);

        unit.convertGuestToCustomer(PWD, ORDER_GUID);

        verify(addressModel).setBrainTreeAddressId(ADDRESS_ID);
        verify(brainTreePaymentService).createAddress(any(), any());
    }

    @Test(expected = DuplicateUidException.class)
    public void shouldCatchDuplicateUidException() throws DuplicateUidException {
        when(baseStoreService.getCurrentBaseStore()).thenReturn(baseStoreModel);
        when(customerAccountDao.findOrderByGUIDAndStore(ORDER_GUID, baseStoreModel, null)).thenThrow(new ModelSavingException(""));

        unit.convertGuestToCustomer(PWD, ORDER_GUID);
    }

    @Test
    public void shouldNotSaveAddressOnBraintreeSideBecauseStoreInVaultIsFalse() throws DuplicateUidException {
        when(baseStoreService.getCurrentBaseStore()).thenReturn(baseStoreModel);
        doReturn(orderModel).when(unit).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        when(orderModel.getUser()).thenReturn(customerModel);
        when(customerModel.getType()).thenReturn(CustomerType.GUEST);

        when(orderModel.getDeliveryAddress()).thenReturn(addressModel);
        when(userService.getCurrentUser()).thenReturn(customerModel);
        when(brainTreeConfigService.isStoreInVault()).thenReturn(false);

        unit.convertGuestToCustomer(PWD, ORDER_GUID);

        verify(orderModel).getUser();
        verify(customerModel).getType();
        verify(baseStoreService, times(3)).getCurrentBaseStore();
        verify(unit, times(2)).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        verify(orderModel).getDeliveryAddress();
        verify(userService, times(2)).getCurrentUser();
        verify(brainTreeConfigService).isStoreInVault();
    }

    @Test
    public void shouldNotSaveAddressOnBraintreeSideBecauseCustomerIdIsEmpty() throws DuplicateUidException {
        when(baseStoreService.getCurrentBaseStore()).thenReturn(baseStoreModel);
        doReturn(orderModel).when(unit).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        when(orderModel.getUser()).thenReturn(customerModel);
        when(customerModel.getType()).thenReturn(CustomerType.GUEST);

        when(orderModel.getDeliveryAddress()).thenReturn(addressModel);
        when(userService.getCurrentUser()).thenReturn(customerModel);
        when(brainTreeConfigService.isStoreInVault()).thenReturn(true);
        when(customerModel.getBraintreeCustomerId()).thenReturn("");

        unit.convertGuestToCustomer(PWD, ORDER_GUID);

        verify(orderModel).getUser();
        verify(customerModel).getType();
        verify(baseStoreService, times(3)).getCurrentBaseStore();
        verify(unit, times(2)).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        verify(orderModel).getDeliveryAddress();
        verify(userService, times(2)).getCurrentUser();
        verify(brainTreeConfigService).isStoreInVault();
        verify(customerModel).getBraintreeCustomerId();
    }

    @Test
    public void shouldNotSaveAddressOnBraintreeSideBecauseCustomerTypeIsIncorrect() throws DuplicateUidException {
        when(baseStoreService.getCurrentBaseStore()).thenReturn(baseStoreModel);
        doReturn(orderModel).when(unit).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        when(orderModel.getUser()).thenReturn(customerModel);
        when(customerModel.getType()).thenReturn(CustomerType.GUEST);

        when(orderModel.getDeliveryAddress()).thenReturn(addressModel);
        when(userService.getCurrentUser()).thenReturn(userModel);

        unit.convertGuestToCustomer(PWD, ORDER_GUID);

        verify(orderModel).getUser();
        verify(customerModel).getType();
        verify(baseStoreService, times(3)).getCurrentBaseStore();
        verify(unit, times(2)).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        verify(orderModel).getDeliveryAddress();
        verify(userService).getCurrentUser();
    }

    @Test
    public void shouldNotSaveAddressOnBraintreeSideBecauseDeliveryAddressIsNull() throws DuplicateUidException {
        when(baseStoreService.getCurrentBaseStore()).thenReturn(baseStoreModel);
        doReturn(orderModel).when(unit).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        when(orderModel.getUser()).thenReturn(customerModel);
        when(customerModel.getType()).thenReturn(CustomerType.GUEST);

        when(orderModel.getDeliveryAddress()).thenReturn(null);
        when(userService.getCurrentUser()).thenReturn(customerModel);

        unit.convertGuestToCustomer(PWD, ORDER_GUID);

        verify(orderModel).getUser();
        verify(customerModel).getType();
        verify(baseStoreService, times(3)).getCurrentBaseStore();
        verify(unit, times(2)).getOrderDetailsForGUID(ORDER_GUID, baseStoreModel);
        verify(orderModel).getDeliveryAddress();
        verify(userService).getCurrentUser();
    }

    @Test
    public void getGraphQLIdForCustomerShouldExchangeAndNotSaveIfCustomerWithThisIdNotExits() {
        unit.getGraphQLIdForCustomer(BRAINTREE_LEGACY_CUSTOMER_ID);

        verify(brainTreePaymentService).exchangeLegacyIdToGraphQLId(any());
        verify(modelService, never()).save(customerModel);
    }

    @Test
    public void getGraphQLIdForCustomer() {
        when(brainTreeCustomerAccountDao.findCustomerByBrainTreeCustomerId(BRAINTREE_LEGACY_CUSTOMER_ID))
            .thenReturn(customerModel);
        when(brainTreePaymentService.exchangeLegacyIdToGraphQLId(any())).thenReturn(BRAINTREE_GRAPHQL_CUSTOMER_ID);

        unit.getGraphQLIdForCustomer(BRAINTREE_LEGACY_CUSTOMER_ID);

        verify(brainTreePaymentService).exchangeLegacyIdToGraphQLId(any());
        verify(customerModel).setBraintreeCustomerGraphQLId(BRAINTREE_GRAPHQL_CUSTOMER_ID);
        verify(modelService).save(customerModel);
    }

    @Test
    public void shouldReturnBraintreeCustomerGraphQLId() {
        when(brainTreeCustomerAccountDao.findCustomerByBrainTreeCustomerId(BRAINTREE_LEGACY_CUSTOMER_ID)).thenReturn(customerModel);
        when(customerModel.getBraintreeCustomerGraphQLId()).thenReturn(BRAINTREE_GRAPHQL_CUSTOMER_ID);

        final String result = unit.getGraphQLIdForCustomer(BRAINTREE_LEGACY_CUSTOMER_ID);

        assertEquals(BRAINTREE_GRAPHQL_CUSTOMER_ID, result);
        verify(brainTreeCustomerAccountDao).findCustomerByBrainTreeCustomerId(BRAINTREE_LEGACY_CUSTOMER_ID);
        verify(customerModel).getBraintreeCustomerGraphQLId();
    }

    @Test
    public void shouldGetCustomerModel() {
        final Optional<CustomerModel> optionalCustomerModel = Optional.of(mock(CustomerModel.class));

        when(brainTreeCustomerAccountDao.findCustomerByPayPalPayerId(BRAINTREE_LEGACY_CUSTOMER_ID)).thenReturn(optionalCustomerModel);

        final Optional<CustomerModel> result = unit.getCustomerByPayerId(BRAINTREE_LEGACY_CUSTOMER_ID);

        assertEquals(optionalCustomerModel, result);
        verify(brainTreeCustomerAccountDao).findCustomerByPayPalPayerId(BRAINTREE_LEGACY_CUSTOMER_ID);
    }

    @Test
    public void shouldSetAccessTokenForCustomer() {
        final Optional<CustomerModel> optionalCustomerModel = Optional.of(customerModel);

        when(brainTreeCustomerAccountDao.findCustomerByPayPalPayerId(CODE)).thenReturn(optionalCustomerModel);
        doNothing().when(modelService).save(customerModel);

        unit.setAccessTokenForCustomer(ID, CODE);

        verify(brainTreeCustomerAccountDao).findCustomerByPayPalPayerId(CODE);
        verify(customerModel).setPayPalAccessToken(ID);
        verify(modelService).save(customerModel);
    }

    @Test
    public void shouldNotSetAccessTokenForCustomer() {
        final Optional<CustomerModel> optionalCustomerModel = Optional.empty();

        when(brainTreeCustomerAccountDao.findCustomerByPayPalPayerId(CODE)).thenReturn(optionalCustomerModel);

        unit.setAccessTokenForCustomer(ID, CODE);

        verify(brainTreeCustomerAccountDao).findCustomerByPayPalPayerId(CODE);
        verify(modelService, never()).save(customerModel);
    }

    @Test
    public void shouldSetDefaultAccessToken() {
        final Optional<CustomerModel> optionalCustomerModel = Optional.of(customerModel);

        when(brainTreeCustomerAccountDao.findCustomerByPayPalPayerId(CODE)).thenReturn(optionalCustomerModel);
        doNothing().when(modelService).save(customerModel);

        unit.setDefaultAccessToken(CODE);

        verify(brainTreeCustomerAccountDao).findCustomerByPayPalPayerId(CODE);
        verify(customerModel).setPayPalAccessToken(StringUtils.EMPTY);
        verify(modelService).save(customerModel);
    }

    @Test
    public void shouldNotSetDefaultAccessToken() {
        final Optional<CustomerModel> optionalCustomerModel = Optional.empty();

        when(brainTreeCustomerAccountDao.findCustomerByPayPalPayerId(CODE)).thenReturn(optionalCustomerModel);

        unit.setDefaultAccessToken(CODE);

        verify(brainTreeCustomerAccountDao).findCustomerByPayPalPayerId(CODE);
        verify(modelService, never()).save(customerModel);
    }

}
