package com.braintree.order.adjust.authorization.impl;

import com.braintree.command.request.BrainTreeAdjustAuthorizationRequest;
import com.braintree.command.result.BrainTreeAbstractTransactionResult;
import com.braintree.command.result.BrainTreeAdjustedAuthorizationResult;
import com.braintree.constants.BraintreeConstants;
import com.braintree.enums.BrainTreeCardType;
import com.braintree.enums.BraintreePaymentTransactionStatus;
import com.braintree.exceptions.BraintreeErrorException;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.model.BrainTreePaymentInfoModel;
import com.braintree.model.PaymentTransactionLogEntryModel;
import com.braintree.transaction.service.BrainTreeTransactionService;
import de.hybris.bootstrap.annotations.UnitTest;
import de.hybris.platform.core.model.order.OrderModel;
import de.hybris.platform.payment.model.PaymentTransactionEntryModel;
import de.hybris.platform.payment.model.PaymentTransactionModel;
import de.hybris.platform.servicelayer.model.ModelService;
import org.apache.commons.lang.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import java.math.BigDecimal;
import java.util.List;
import java.util.NoSuchElementException;

import static com.braintree.constants.BraintreeConstants.BRAINTREE_SUCCESS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;


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

    private static final String ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95602 = "95602";
    private static final String ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95607 = "95607";
    private static final String TRANSACTION_ID = "transactionId";
    private static final String AMOUNT = "120.00";
    private static final String ERROR_MESSAGE = "ERROR_MESSAGE";
    private static final String ADJUST_AUTHORIZATION = "ADJUST_AUTHORIZATION";

    @Mock
    private ModelService modelService;
    @Mock
    private BrainTreePaymentService brainTreePaymentService;
    @Mock
    private BrainTreeTransactionService brainTreeTransactionService;

    @Spy
    @InjectMocks
    private DefaultBraintreeAdjustAuthorizationService unit;

    @Mock
    private PaymentTransactionEntryModel paymentTransactionEntryModel;

    @Mock
    private OrderModel orderModel;

    @Mock
    private PaymentTransactionModel transactionModel;

    @Mock
    private BrainTreePaymentInfoModel paymentInfoModel;

    @Mock
    private BrainTreeAbstractTransactionResult abstractTransactionResult;

    @Mock
    private PaymentTransactionLogEntryModel logEntryModel;

    @Mock
    private PaymentTransactionLogEntryModel logEntryModel2;

    @Captor
    private ArgumentCaptor<BrainTreeAdjustAuthorizationRequest> adjustAuthorizationRequestArgumentCaptor;

    private BrainTreeAdjustedAuthorizationResult transactionResult;
    private BigDecimal bigDecimalAmount;
    private BigDecimal newAmount;

    @Before
    public void setUp() {
        transactionResult = new BrainTreeAdjustedAuthorizationResult();
        bigDecimalAmount = new BigDecimal(AMOUNT);
        newAmount = new BigDecimal(AMOUNT);

        when(orderModel.getPaymentInfo()).thenReturn(paymentInfoModel);
        when(transactionModel.getEntries()).thenReturn(List.of(paymentTransactionEntryModel));
        when(orderModel.getPaymentTransactions()).thenReturn(List.of(transactionModel));
        when(brainTreeTransactionService.createTransactionLogEntry(transactionResult, ADJUST_AUTHORIZATION)).thenReturn(logEntryModel);
        when(paymentTransactionEntryModel.getOriginalAmount()).thenReturn(bigDecimalAmount);
    }

    @Test
    public void shouldReturnTrueWhenAdjustAuthorizationPossibleWithMastercard() {
        //given
        when(paymentInfoModel.getCardType()).thenReturn(BrainTreeCardType.MASTERCARD);
        when(transactionModel.getBraintreePaymentTransactionStatus()).thenReturn(BraintreePaymentTransactionStatus.AUTHORIZED);

        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertTrue(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldReturnTrueWhenAdjustAuthorizationPossibleWithVisa() {
        //given
        when(paymentInfoModel.getCardType()).thenReturn(BrainTreeCardType.VISA);
        when(transactionModel.getBraintreePaymentTransactionStatus()).thenReturn(BraintreePaymentTransactionStatus.AUTHORIZED);

        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertTrue(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldReturnFalseWhenCardTypeNotApplicable() {
        //given
        when(paymentInfoModel.getCardType()).thenReturn(BrainTreeCardType.DISCOVER);

        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertFalse(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldReturnFalseWhenTransactionStatusNotAuthorized() {
        //given
        when(paymentInfoModel.getCardType()).thenReturn(BrainTreeCardType.VISA);
        when(transactionModel.getBraintreePaymentTransactionStatus()).thenReturn(BraintreePaymentTransactionStatus.SUBMITTED_FOR_SETTLEMENT);

        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertFalse(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldReturnFalseWhenTransactionCardTypeIsNull() {
        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertFalse(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldReturnAdjustAuthorizationPossibleFalseWhenHardDeclineError95602Present() {
        //given
        when(paymentInfoModel.getCardType()).thenReturn(BrainTreeCardType.VISA);
        when(transactionModel.getBraintreePaymentTransactionStatus()).thenReturn(BraintreePaymentTransactionStatus.AUTHORIZED);
        when(paymentTransactionEntryModel.getErrorCode()).thenReturn(ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95602);

        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertFalse(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldReturnAdjustAuthorizationPossibleFalseWhenHardDeclineError95607Present() {
        //given
        when(paymentInfoModel.getCardType()).thenReturn(BrainTreeCardType.VISA);
        when(transactionModel.getBraintreePaymentTransactionStatus()).thenReturn(BraintreePaymentTransactionStatus.AUTHORIZED);
        when(paymentTransactionEntryModel.getErrorCode()).thenReturn(ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95607);

        //when
        boolean isAdjustAuthorizationPossible = unit.isAdjustAuthorizationPossible(orderModel, transactionModel);

        //then
        assertFalse(isAdjustAuthorizationPossible);
    }

    @Test
    public void shouldCheckAndProcessHardDeclineErrorFor95602() {
        when(abstractTransactionResult.getErrorCode()).thenReturn(ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95602);
        doReturn(ERROR_MESSAGE).when(unit).getLocalizedMessage(BraintreeConstants.PropertyConfiguration.ADJUST_TRANSACTION_AUTHORIZATION_HARD_DECLINE_ERROR);
        doNothing().when(modelService).save(paymentTransactionEntryModel);

        unit.checkAndProcessHardDeclineError(abstractTransactionResult, paymentTransactionEntryModel);

        verify(abstractTransactionResult).getErrorCode();
        verify(unit).getLocalizedMessage(BraintreeConstants.PropertyConfiguration.ADJUST_TRANSACTION_AUTHORIZATION_HARD_DECLINE_ERROR);
        verify(paymentTransactionEntryModel).setErrorCode(ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95602);
        verify(abstractTransactionResult).setErrorMessage(ERROR_MESSAGE);
        verify(modelService).save(paymentTransactionEntryModel);
    }

    @Test
    public void shouldCheckAndProcessHardDeclineErrorFor95607() {
        when(abstractTransactionResult.getErrorCode()).thenReturn(ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95607);
        doReturn(ERROR_MESSAGE).when(unit).getLocalizedMessage(BraintreeConstants.PropertyConfiguration.ADJUST_TRANSACTION_AUTHORIZATION_HARD_DECLINE_ERROR);
        doNothing().when(modelService).save(paymentTransactionEntryModel);

        unit.checkAndProcessHardDeclineError(abstractTransactionResult, paymentTransactionEntryModel);

        verify(abstractTransactionResult).getErrorCode();
        verify(unit).getLocalizedMessage(BraintreeConstants.PropertyConfiguration.ADJUST_TRANSACTION_AUTHORIZATION_HARD_DECLINE_ERROR);
        verify(paymentTransactionEntryModel).setErrorCode(ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95607);
        verify(abstractTransactionResult).setErrorMessage(ERROR_MESSAGE);
        verify(modelService).save(paymentTransactionEntryModel);
    }

    @Test
    public void shouldNotCheckAndProcessHardDeclineError() {
        when(abstractTransactionResult.getErrorCode()).thenReturn(StringUtils.EMPTY);

        unit.checkAndProcessHardDeclineError(abstractTransactionResult, paymentTransactionEntryModel);

        verify(abstractTransactionResult).getErrorCode();
        verify(modelService, never()).save(paymentTransactionEntryModel);
    }

    @Test
    public void shouldGetTransactionEntryById() {
        //given
        when(paymentTransactionEntryModel.getRequestId()).thenReturn(TRANSACTION_ID);

        //when
        PaymentTransactionEntryModel transactionEntryById = unit.getTransactionEntryById(orderModel, TRANSACTION_ID);

        //then
        assertEquals(paymentTransactionEntryModel, transactionEntryById);
    }

    @Test(expected = NoSuchElementException.class)
    public void shouldThrowErrorIfTransactionEntryByIdNotFound() {
        //given
        when(paymentTransactionEntryModel.getRequestId()).thenReturn(StringUtils.EMPTY);

        //when
        unit.getTransactionEntryById(orderModel, TRANSACTION_ID);
    }

    @Test
    public void shouldSuccessfullyAdjustAuthorization() throws BraintreeErrorException {
        //given
        doReturn(paymentTransactionEntryModel).when(unit).getTransactionEntryById(orderModel, TRANSACTION_ID);
        when(paymentTransactionEntryModel.getPaymentTransaction()).thenReturn(transactionModel);
        doReturn(true).when(unit).isAdjustAuthorizationPossible(orderModel, transactionModel);
        when(brainTreePaymentService.adjustAuthorization(any(BrainTreeAdjustAuthorizationRequest.class)))
                .thenReturn(transactionResult);

        transactionResult.setSuccess(true);
        transactionResult.setAmount(bigDecimalAmount);

        //when
        BrainTreeAdjustedAuthorizationResult adjustedAuthorizationResult =
                unit.adjustAuthorization(orderModel, TRANSACTION_ID, bigDecimalAmount);

        //then
        assertNotNull(adjustedAuthorizationResult);

        verify(brainTreePaymentService).adjustAuthorization(adjustAuthorizationRequestArgumentCaptor.capture());

        assertNotNull(adjustAuthorizationRequestArgumentCaptor.getValue());
        assertEquals(TRANSACTION_ID, adjustAuthorizationRequestArgumentCaptor.getValue().getTransactionId());
        assertEquals(bigDecimalAmount, adjustAuthorizationRequestArgumentCaptor.getValue().getAmount());
        assertTrue(adjustedAuthorizationResult.isSuccess());

        verify(transactionModel).setPlannedAmount(bigDecimalAmount);
        verify(paymentTransactionEntryModel).setAmount(bigDecimalAmount);
    }

    @Test
    public void shouldProcessErrorResult() throws BraintreeErrorException {
        //given
        doReturn(paymentTransactionEntryModel).when(unit).getTransactionEntryById(orderModel, TRANSACTION_ID);
        when(paymentTransactionEntryModel.getPaymentTransaction()).thenReturn(transactionModel);
        doReturn(true).when(unit).isAdjustAuthorizationPossible(orderModel, transactionModel);
        doNothing().when(unit).checkAndProcessHardDeclineError(transactionResult, paymentTransactionEntryModel);
        when(brainTreePaymentService.adjustAuthorization(any(BrainTreeAdjustAuthorizationRequest.class)))
                .thenReturn(transactionResult);

        transactionResult.setSuccess(false);
        transactionResult.setAmount(bigDecimalAmount);

        //when
        BrainTreeAdjustedAuthorizationResult adjustedAuthorizationResult =
                unit.adjustAuthorization(orderModel, TRANSACTION_ID, bigDecimalAmount);

        //then
        assertNotNull(adjustedAuthorizationResult);
        assertFalse(adjustedAuthorizationResult.isSuccess());

        verify(transactionModel, never()).setPlannedAmount(bigDecimalAmount);
        verify(paymentTransactionEntryModel, never()).setAmount(bigDecimalAmount);
        verify(unit).checkAndProcessHardDeclineError(transactionResult, paymentTransactionEntryModel);
    }

    @Test(expected = BraintreeErrorException.class)
    public void shouldThrowBraintreeErrorExceptionForAdjustAuthorization() throws BraintreeErrorException {
        doReturn(paymentTransactionEntryModel).when(unit).getTransactionEntryById(orderModel, TRANSACTION_ID);
        when(paymentTransactionEntryModel.getPaymentTransaction()).thenReturn(transactionModel);
        doReturn(false).when(unit).isAdjustAuthorizationPossible(orderModel, transactionModel);
        doReturn(ERROR_MESSAGE).when(unit).getLocalizedMessage(BraintreeConstants.PropertyConfiguration.TRANSACTION_AUTHORIZATION_NOT_ADJUSTABLE);

        unit.adjustAuthorization(orderModel, TRANSACTION_ID, bigDecimalAmount);

        verify(unit).getTransactionEntryById(orderModel, TRANSACTION_ID);
        verify(paymentTransactionEntryModel).getPaymentTransaction();
        verify(unit).isAdjustAuthorizationPossible(orderModel, transactionModel);
        verify(unit).getLocalizedMessage(BraintreeConstants.PropertyConfiguration.TRANSACTION_AUTHORIZATION_NOT_ADJUSTABLE);
        verify(paymentTransactionEntryModel).getRequestId();
    }

    @Test
    public void shouldCreateTransactionLogEntryWhenAnotherSuccessfulAdjustAuthorizationLogEntryExists()
            throws BraintreeErrorException {
        //given
        doReturn(paymentTransactionEntryModel).when(unit).getTransactionEntryById(orderModel, TRANSACTION_ID);
        when(paymentTransactionEntryModel.getPaymentTransaction()).thenReturn(transactionModel);
        doReturn(true).when(unit).isAdjustAuthorizationPossible(orderModel, transactionModel);
        when(brainTreePaymentService.adjustAuthorization(any(BrainTreeAdjustAuthorizationRequest.class)))
                .thenReturn(transactionResult);

        when(transactionModel.getEntriesLogs()).thenReturn(List.of(logEntryModel2));
        when(logEntryModel2.getStatus()).thenReturn(BRAINTREE_SUCCESS);
        when(logEntryModel2.getNewAmount()).thenReturn(bigDecimalAmount.doubleValue());
        when(logEntryModel2.getInteractionType()).thenReturn(ADJUST_AUTHORIZATION);

        //when
        unit.adjustAuthorization(orderModel, TRANSACTION_ID, newAmount);

        //then
        verify(logEntryModel).setNewAmount(newAmount.doubleValue());
        verify(logEntryModel).setOldAmount(bigDecimalAmount.doubleValue());
        verify(logEntryModel).setPaymentTransaction(transactionModel);
        verify(modelService).save(logEntryModel);
        verify(logEntryModel2).getNewAmount();
    }

    @Test
    public void shouldCreateTransactionLogEntryWhenAnotherUnsuccessfulAdjustAuthorizationLogEntryExists()
            throws BraintreeErrorException {
        //given
        doReturn(paymentTransactionEntryModel).when(unit).getTransactionEntryById(orderModel, TRANSACTION_ID);
        when(paymentTransactionEntryModel.getPaymentTransaction()).thenReturn(transactionModel);
        doReturn(true).when(unit).isAdjustAuthorizationPossible(orderModel, transactionModel);
        when(brainTreePaymentService.adjustAuthorization(any(BrainTreeAdjustAuthorizationRequest.class)))
                .thenReturn(transactionResult);

        when(transactionModel.getEntriesLogs()).thenReturn(List.of(logEntryModel2));
        when(logEntryModel2.getInteractionType()).thenReturn(ADJUST_AUTHORIZATION);
        when(paymentTransactionEntryModel.getOriginalAmount()).thenReturn(bigDecimalAmount);

        //when
        unit.adjustAuthorization(orderModel, TRANSACTION_ID, newAmount);

        //then
        verify(logEntryModel).setNewAmount(newAmount.doubleValue());
        verify(logEntryModel).setOldAmount(bigDecimalAmount.doubleValue());
        verify(logEntryModel).setPaymentTransaction(transactionModel);
        verify(modelService).save(logEntryModel);
        verify(paymentTransactionEntryModel).getOriginalAmount();
    }

}
