package com.braintree.graphql.commands.impl;

import com.braintree.command.request.BrainTreeCreateCreditCardPaymentMethodRequest;
import com.braintree.command.result.BrainTreeBillingAddressResult;
import com.braintree.command.result.BrainTreeCreatePaymentMethodResult;
import com.braintree.command.result.BrainTreePaymentMethodResult;
import com.braintree.commands.impl.DefaultBrainTreeLoggingHandler;
import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.customer.service.BrainTreeCustomerAccountService;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeAddressInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeVaultCreditCardPaymentMethodInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeVaultPaymentMethodInput;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeAddress;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeCustomer;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeErrorDefinition;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeErrorExtension;
import com.braintree.hybris.data.graphql.commands.response.BraintreePaymentMethodData;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentMethodDetails;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentMethodOrigin;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeVaultPaymentMethodPayload;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.payment.info.service.BraintreePaymentInfoService;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hybris.bootstrap.annotations.UnitTest;
import de.hybris.platform.payment.AdapterException;
import de.hybris.platform.payment.dto.BillingInfo;
import de.hybris.platform.servicelayer.dto.converter.Converter;
import org.apache.log4j.Logger;
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.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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

    private static final String DEFINITION_FILE_NAME = "vaultCreditCard";
    private static final String INPUT_PARAMETER = "input";
    private static final String MERCHANT_TRANSACTION_CODE = "merchantTransactionCode";
    private static final String ERROR_MESSAGE = "Error message";
    private static final String ERROR_MAP_KEY = "error";
    private static final String RESULT_ERRORS = "errors";
    private static final String DATA_MAP_KEY = "data";
    private static final String CUSTOMER_ID = "customerId";
    private static final String GRAPH_QL_ID_FOR_CUSTOMER = "graphQlIdForCustomer";
    private static final String EMAIL = "email";
    private static final String LEGACY_ID = "legacyId";
    private static final String METHOD_NONCE = "methodNonce";
    private static final String DEVICE_DATA = "deviceData";
    private static final String PAYMENT_METHOD_ID = "paymentMethodId";
    private static final String CARD_HOLDER_FULL_NAME = "John Cena";
    private static final String EXPIRATION_MONTH = "1";
    private static final String EXPIRATION_YEAR = "2030";
    private static final String BRAND_CODE = "brandCode";
    private static final String LAST_4 = "4444";
    private static final String ORIGIN_TYPE = "origin_type";
    private static final String TYPENAME = "__typename";
    private static final String PAYMENT_PROVIDER_BY_DETAILS_AND_ORIGIN = "PaymentProviderByDetailsAndOrigin";
    private static final String IMAGE_SOURCE_BY_PAYMENT_PROVIDER_AND_CARD_TYPE = "ImageSourceByPaymentProviderAndCardType";
    private static final String MERCHANT_ACCOUNT_ID = "merchantAccountId";
    private static final String BIN = "bin";
    private static final String LEGACY_CODE = "legacyCode";

    @Mock
    private BrainTreePaymentService brainTreePaymentService;
    @Mock
    private BrainTreeCustomerAccountService brainTreeCustomerAccountService;
    @Mock
    private BraintreePaymentInfoService braintreePaymentInfoService;
    @Mock
    private Converter<BrainTreeAddress, BrainTreeBillingAddressResult> graphQLAddressToAddressResult;
    @Mock
    private Converter<BillingInfo, BrainTreeAddressInput> braintreeBillingInfoToGraphQLAddressInputConverter;
    @Mock
    private ObjectMapper objectMapper;
    @Mock
    private DefaultBrainTreeLoggingHandler loggingHandler;
    @Mock
    private BrainTreeConfigService brainTreeConfigService;

    @Spy
    @InjectMocks
    private DefaultBrainTreeGraphQLCreateCreditCardPaymentMethodCommand command;

    @Captor
    private ArgumentCaptor<Map<String, Object>> variables;

    @Mock
    private Logger logger;

    private Map<String, Object> mapResult;
    private Map<String, Object> dataResult;
    private BrainTreeVaultPaymentMethodPayload vaultPaymentMethodPayload;
    private BrainTreeCreateCreditCardPaymentMethodRequest creditCardPaymentMethodRequest;
    private BraintreePaymentMethodData paymentMethod;
    private BrainTreePaymentMethodDetails paymentMethodDetails;
    private Calendar createdAt;
    private BrainTreeCustomer customer;

    @Before
    public void setUp() throws IOException {
        customer = new BrainTreeCustomer();
        customer.setLegacyId(CUSTOMER_ID);

        createdAt = new GregorianCalendar();
        paymentMethodDetails = new BrainTreePaymentMethodDetails();

        paymentMethod = new BraintreePaymentMethodData();
        paymentMethod.setDetails(paymentMethodDetails);
        paymentMethod.setCustomer(customer);

        vaultPaymentMethodPayload = new BrainTreeVaultPaymentMethodPayload();
        vaultPaymentMethodPayload.setPaymentMethod(paymentMethod);

        creditCardPaymentMethodRequest = new BrainTreeCreateCreditCardPaymentMethodRequest(MERCHANT_TRANSACTION_CODE);
        creditCardPaymentMethodRequest.setAdvancedFraudTools(false);

        dataResult = new HashMap<>();
        dataResult.put(DEFINITION_FILE_NAME, vaultPaymentMethodPayload);
        mapResult = new HashMap<>();
        mapResult.put(DATA_MAP_KEY, dataResult);

        doReturn(mapResult).when(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        when(objectMapper.convertValue(vaultPaymentMethodPayload, BrainTreeVaultPaymentMethodPayload.class))
                .thenReturn(vaultPaymentMethodPayload);
        when(loggingHandler.getLogger()).thenReturn(logger);
    }

    @Test
    public void shouldCreateVariablesMap() throws IOException {
        //given
        creditCardPaymentMethodRequest.setMethodNonce(METHOD_NONCE);
        creditCardPaymentMethodRequest.setCustomerId(CUSTOMER_ID);

        when(brainTreeCustomerAccountService.getGraphQLIdForCustomer(CUSTOMER_ID)).thenReturn(GRAPH_QL_ID_FOR_CUSTOMER);
        when(brainTreeConfigService.getMerchantAccountIdForCurrentSiteAndCurrency()).thenReturn(MERCHANT_ACCOUNT_ID);
        when(brainTreeConfigService.isVerifyCard()).thenReturn(false);

        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), variables.capture());
        assertNotNull(variables.getValue());

        BrainTreeVaultCreditCardPaymentMethodInput input =
                (BrainTreeVaultCreditCardPaymentMethodInput) variables.getValue().get(INPUT_PARAMETER);

        assertNotNull(input);
        assertEquals(METHOD_NONCE, input.getPaymentMethodId());
        assertEquals(GRAPH_QL_ID_FOR_CUSTOMER, input.getCustomerId());

        assertNotNull(input.getVerification());
        assertEquals(MERCHANT_ACCOUNT_ID, input.getVerification().getMerchantAccountId());
        assertTrue(input.getVerification().isSkip());
    }

    @Test
    public void shouldSetSkipFalseIfVerifyCardIsTrueAndThreeDSecureFlowIsFalseWhenCreateVariablesMap() throws IOException {
        //given
        when(brainTreeConfigService.isVerifyCard()).thenReturn(true);

        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), variables.capture());
        assertNotNull(variables.getValue());

        BrainTreeVaultCreditCardPaymentMethodInput input =
                (BrainTreeVaultCreditCardPaymentMethodInput) variables.getValue().get(INPUT_PARAMETER);

        assertNotNull(input);
        assertNotNull(input.getVerification());
        assertFalse(input.getVerification().isSkip());
    }

    @Test
    public void shouldSetSkipFalseIfVerifyCardIsFalseAndThreeDSecureFlowIsTrueWhenCreateVariablesMap() throws IOException {
        //given
        when(brainTreeConfigService.isVerifyCard()).thenReturn(true);

        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), variables.capture());
        assertNotNull(variables.getValue());

        BrainTreeVaultCreditCardPaymentMethodInput input =
                (BrainTreeVaultCreditCardPaymentMethodInput) variables.getValue().get(INPUT_PARAMETER);

        assertNotNull(input);
        assertNotNull(input.getVerification());
        assertFalse(input.getVerification().isSkip());
    }

    @Test
    public void shouldSetBillingAddressWhenCreateVariablesMap() throws IOException {
        //given
        BillingInfo billingAddress = new BillingInfo();
        BrainTreeAddressInput addressInput = new BrainTreeAddressInput();
        creditCardPaymentMethodRequest.setBillingInfo(billingAddress);

        when(braintreeBillingInfoToGraphQLAddressInputConverter.convert(billingAddress)).thenReturn(addressInput);

        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), variables.capture());
        assertNotNull(variables.getValue());

        BrainTreeVaultCreditCardPaymentMethodInput input =
                (BrainTreeVaultCreditCardPaymentMethodInput) variables.getValue().get(INPUT_PARAMETER);

        assertNotNull(input);
        assertNotNull(input.getBillingAddress());
        assertEquals(addressInput, input.getBillingAddress());
    }

    @Test
    public void shouldNotSetCustomerIdIfRequestCustomerIdIsNullWhenCreateVariablesMap() throws IOException {
        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), variables.capture());
        assertNotNull(variables.getValue());

        BrainTreeVaultPaymentMethodInput input = (BrainTreeVaultPaymentMethodInput) variables.getValue().get(INPUT_PARAMETER);

        assertNotNull(input);
        assertNull(input.getCustomerId());
    }

    @Test
    public void shouldSetRiskDataWhenCreateVariablesMap() throws IOException {
        //given
        creditCardPaymentMethodRequest.setAdvancedFraudTools(true);
        creditCardPaymentMethodRequest.setDeviceData(DEVICE_DATA);

        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), variables.capture());
        assertNotNull(variables.getValue());

        BrainTreeVaultPaymentMethodInput input = (BrainTreeVaultPaymentMethodInput) variables.getValue().get(INPUT_PARAMETER);

        assertNotNull(input);
        assertNotNull(input.getRiskData());
        assertEquals(DEVICE_DATA, input.getRiskData().getDeviceData());
    }

    @Test
    public void shouldTranslateResponseWhenSuccessfulTransaction() throws IOException {
        //given
        BrainTreePaymentMethodOrigin paymentMethodOrigin = new BrainTreePaymentMethodOrigin();
        paymentMethodOrigin.setType(ORIGIN_TYPE);

        paymentMethod.setCreatedAt(createdAt);
        paymentMethod.setCustomer(customer);
        paymentMethod.setLegacyId(LEGACY_ID);
        paymentMethod.setId(PAYMENT_METHOD_ID);

        paymentMethodDetails.setEmail(EMAIL);
        paymentMethodDetails.setExpirationMonth(EXPIRATION_MONTH);
        paymentMethodDetails.setExpirationYear(EXPIRATION_YEAR);
        paymentMethodDetails.setBrandCode(BRAND_CODE);
        paymentMethodDetails.setLast4(LAST_4);
        paymentMethodDetails.setCardholderName(CARD_HOLDER_FULL_NAME);
        paymentMethodDetails.setOrigin(paymentMethodOrigin);
        paymentMethodDetails.set__typename(TYPENAME);
        paymentMethodDetails.setBin(BIN);

        when(braintreePaymentInfoService.getPaymentProviderByDetailsAndOrigin(TYPENAME, ORIGIN_TYPE))
                .thenReturn(PAYMENT_PROVIDER_BY_DETAILS_AND_ORIGIN);
        when(braintreePaymentInfoService.getImageSourceByPaymentProviderAndCardType(PAYMENT_PROVIDER_BY_DETAILS_AND_ORIGIN, BRAND_CODE))
                .thenReturn(IMAGE_SOURCE_BY_PAYMENT_PROVIDER_AND_CARD_TYPE);

        //when
        BrainTreeCreatePaymentMethodResult commandResult = command.perform(creditCardPaymentMethodRequest);

        //then
        assertNotNull(commandResult);
        assertTrue(commandResult.isSuccess());
        assertEquals(LEGACY_ID, commandResult.getPaymentMethodToken());
        assertEquals(PAYMENT_METHOD_ID, commandResult.getPaymentMethodGraphQLToken());
        assertEquals(CUSTOMER_ID, commandResult.getCustomerId());
        assertEquals(createdAt, commandResult.getCreatedAt());
        assertEquals(EMAIL, commandResult.getEmail());
        assertEquals(EXPIRATION_MONTH, commandResult.getExpirationMonth());
        assertEquals(EXPIRATION_YEAR, commandResult.getExpirationYear());
        assertEquals(BRAND_CODE, commandResult.getCardType());
        assertEquals(LAST_4, commandResult.getLast4());
        assertEquals(CARD_HOLDER_FULL_NAME, commandResult.getCardholderName());
        assertNotNull(commandResult.getCardNumber());
        assertEquals(PAYMENT_PROVIDER_BY_DETAILS_AND_ORIGIN, commandResult.getPaymentProvider());
        assertEquals(IMAGE_SOURCE_BY_PAYMENT_PROVIDER_AND_CARD_TYPE, commandResult.getImageSource());

        verify(braintreePaymentInfoService).getPaymentProviderByDetailsAndOrigin(TYPENAME, ORIGIN_TYPE);
        verify(braintreePaymentInfoService)
                .getImageSourceByPaymentProviderAndCardType(PAYMENT_PROVIDER_BY_DETAILS_AND_ORIGIN, BRAND_CODE);
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
    }

    @Test
    public void shouldSetBillingAddressWhenTranslateResponse() throws IOException {
        //given
        BrainTreeAddress address = new BrainTreeAddress();
        BrainTreeBillingAddressResult addressResult = new BrainTreeBillingAddressResult();

        paymentMethodDetails.setBillingAddress(address);
        paymentMethod.setCustomer(customer);

        when(graphQLAddressToAddressResult.convert(address)).thenReturn(addressResult);

        //when
        BrainTreeCreatePaymentMethodResult commandResult = command.perform(creditCardPaymentMethodRequest);

        //then
        assertNotNull(commandResult);
        assertTrue(commandResult.isSuccess());
        assertNotNull(commandResult.getBillingAddressResult());
        assertEquals(addressResult, commandResult.getBillingAddressResult());

        verify(graphQLAddressToAddressResult).convert(address);
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
    }

    @Test
    public void shouldNotSetPaymentMethodDetailsIfNullWhenTranslateResponse() throws IOException {
        //given
        paymentMethod.setLegacyId(LEGACY_ID);
        paymentMethod.setId(PAYMENT_METHOD_ID);
        paymentMethod.setCreatedAt(createdAt);

        //when
        BrainTreeCreatePaymentMethodResult commandResult = command.perform(creditCardPaymentMethodRequest);

        //then
        assertNotNull(commandResult);
        assertEquals(LEGACY_ID, commandResult.getPaymentMethodToken());
        assertEquals(PAYMENT_METHOD_ID, commandResult.getPaymentMethodGraphQLToken());
        assertEquals(CUSTOMER_ID, commandResult.getCustomerId());
        assertEquals(createdAt, commandResult.getCreatedAt());
        assertNull(commandResult.getEmail());
        assertNull(commandResult.getExpirationMonth());
        assertNull(commandResult.getExpirationYear());
        assertNull(commandResult.getCardType());
        assertNull(commandResult.getLast4());
        assertNull(commandResult.getCardholderName());
        assertNull(commandResult.getCardNumber());
        assertNull(commandResult.getPaymentProvider());
        assertNull(commandResult.getImageSource());

        verify(braintreePaymentInfoService)
                .getImageSourceByPaymentProviderAndCardType(null, null);
        verify(graphQLAddressToAddressResult, never()).convert(any());
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
    }

    @Test
    public void shouldNotSetPaymentMethodDataIfNullWhenTranslateResponse() throws IOException {
        //given
        vaultPaymentMethodPayload.setPaymentMethod(null);

        //when
        BrainTreeCreatePaymentMethodResult commandResult = command.perform(creditCardPaymentMethodRequest);

        //then
        assertNotNull(commandResult);
        assertNull(commandResult.getPaymentMethodToken());
        assertNull(commandResult.getPaymentMethodGraphQLToken());
        assertNull(commandResult.getCustomerId());
        assertNull(commandResult.getCreatedAt());
        assertNull(commandResult.getEmail());
        assertNull(commandResult.getExpirationMonth());
        assertNull(commandResult.getExpirationYear());
        assertNull(commandResult.getCardType());
        assertNull(commandResult.getLast4());
        assertNull(commandResult.getCardholderName());
        assertNull(commandResult.getCardNumber());
        assertNull(commandResult.getPaymentProvider());
        assertNull(commandResult.getImageSource());

        verify(braintreePaymentInfoService, never()).getPaymentProviderByDetailsAndOrigin(anyString(), anyString());
        verify(braintreePaymentInfoService, never())
                .getImageSourceByPaymentProviderAndCardType(anyString(), anyString());
        verify(graphQLAddressToAddressResult, never()).convert(any());
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
    }

    @Test
    public void shouldTranslateErrorResponseWhenGraphQLResponseContainsError() throws IOException {
        //given
        BrainTreeErrorExtension errorExtension = new BrainTreeErrorExtension();
        errorExtension.setLegacyCode(LEGACY_CODE);
        BrainTreeErrorDefinition brainTreeError = new BrainTreeErrorDefinition();
        brainTreeError.setMessage(ERROR_MESSAGE);
        brainTreeError.setExtensions(errorExtension);
        Map<String, Object> errorMap = Map.of(ERROR_MAP_KEY, brainTreeError);
        List<Map<String, Object>> errorList = new ArrayList<>();
        errorList.add(errorMap);

        mapResult.put(RESULT_ERRORS, errorList);

        when(objectMapper.convertValue(errorMap, BrainTreeErrorDefinition.class)).thenReturn(brainTreeError);

        //when
        BrainTreePaymentMethodResult commandResult = command.perform(creditCardPaymentMethodRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertEquals(LEGACY_CODE, commandResult.getErrorCode());
        assertEquals(ERROR_MESSAGE, commandResult.getErrorMessage());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(loggingHandler).getLogger();
    }

    @Test(expected = AdapterException.class)
    public void shouldThrowErrorWhenGraphQLCallFails() throws IOException {
        //given
        doThrow(IOException.class).when(command).makeGraphQlCall(any(), any());

        //when
        command.perform(creditCardPaymentMethodRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
    }

}
