package com.braintree.graphql.commands.impl;

import com.braintree.command.request.BrainTreeRefundTransactionRequest;
import com.braintree.command.result.BrainTreeRefundTransactionResult;
import com.braintree.commands.impl.DefaultBrainTreeLoggingHandler;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeRefundTransactionInput;
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.BrainTreeMonetaryAmount;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePartialCaptureTransactionPayload;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentMethodSnapshot;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentStatus;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentStatusEvent;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeRefund;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeRefundDetails;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeRefundTransactionPayload;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeTransactionAuthorizationProcessorResponse;
import com.braintree.payment.info.service.BraintreePaymentInfoService;
import com.braintree.transaction.service.BrainTreeTransactionService;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hybris.bootstrap.annotations.UnitTest;
import de.hybris.platform.payment.AdapterException;
import de.hybris.platform.payment.dto.TransactionStatus;
import de.hybris.platform.payment.dto.TransactionStatusDetails;
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.math.BigDecimal;
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.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 DefaultBrainTreeGraphQLRefundTransactionCommandTest {

    private static final String DEFINITION_FILE_NAME = "refundTransaction";
    private static final String MUTATION_NAME = "refundTransaction";
    private static final String DATA_MAP_KEY = "data";
    private static final String AMOUNT = "12.44";
    private static final String INPUT_PARAMETER = "input";
    private static final String TRANSACTION_ID = "transactionId";
    private static final String USD = "USD";
    private static final String RESULT_ERRORS = "errors";
    private static final String ERROR_MESSAGE = "Error message";
    private static final String LEGACY_ID = "legacyId";
    private static final String PAYPAL_ACCOUNT_INSTRUMENT_TYPE = "paypal_account";
    private static final String CAPTURE_ID = "captureId";
    private static final String PROCESSOR_RESPONSE_ERROR_MESSAGE = "processor response error message";
    private static final String LEGACY_CODE = "legacyCode";
    private static final String ERROR_MAP_KEY = "error";
    private static final String MERCHANT_TRANSACTION_CODE = "merchantTransactionCode";
    private static final String ORDER_ID = "orderId";
    private static final String REFUND_ID = "RefundID";

    @Mock
    private BrainTreeTransactionService brainTreeTransactionService;
    @Mock
    private BraintreePaymentInfoService paymentInfoService;
    @Mock
    private ObjectMapper objectMapper;
    @Mock
    private DefaultBrainTreeLoggingHandler loggingHandler;

    @Spy
    @InjectMocks
    private DefaultBrainTreeGraphQLRefundTransactionCommand command;

    @Mock
    private Logger logger;

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

    private BrainTreeRefundTransactionRequest refundTransactionRequest;
    private Map<String, Object> mapResult;
    private Map<String, Object> dataResult;
    private BigDecimal bigDecimalAmount;
    private Calendar createdAt;
    private BrainTreePaymentMethodSnapshot paymentMethodSnapshot;
    private BrainTreeRefundTransactionInput refundTransactionInput;
    private BrainTreeRefundTransactionPayload refundTransactionPayload;
    private BrainTreeRefund brainTreeRefund;

    @Before
    public void setUp() throws IOException {
        refundTransactionRequest = new BrainTreeRefundTransactionRequest(MERCHANT_TRANSACTION_CODE);
        refundTransactionRequest.setTransactionId(TRANSACTION_ID);

        refundTransactionInput = new BrainTreeRefundTransactionInput();
        bigDecimalAmount = new BigDecimal(AMOUNT);
        createdAt = new GregorianCalendar();

        brainTreeRefund = new BrainTreeRefund();
        refundTransactionPayload = new BrainTreeRefundTransactionPayload();
        refundTransactionPayload.setRefund(brainTreeRefund);

        dataResult = Map.of(MUTATION_NAME, refundTransactionPayload);
        mapResult = new HashMap<>();
        mapResult.put(DATA_MAP_KEY, dataResult);

        paymentMethodSnapshot = new BrainTreePaymentMethodSnapshot();
        paymentMethodSnapshot.setCaptureId(CAPTURE_ID);

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

    @Test
    public void shouldCreateVariablesMap() throws IOException {
        //given
        refundTransactionRequest.setOrderId(ORDER_ID);
        refundTransactionRequest.setAmount(bigDecimalAmount);
        when(brainTreeTransactionService.getGraphQLIdFromTransaction(TRANSACTION_ID)).thenReturn(TRANSACTION_ID);
        when(objectMapper.convertValue(any(), eq(BrainTreeRefundTransactionPayload.class))).thenReturn(null);

        //when
        command.perform(refundTransactionRequest);

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

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

        assertNotNull(input);
        assertEquals(TRANSACTION_ID, input.getTransactionId());
        assertNotNull(input.getRefund());
        assertEquals(bigDecimalAmount, input.getRefund().getAmount());
        assertEquals(ORDER_ID, input.getRefund().getOrderId());
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
    }

    @Test
    public void shouldTranslateResponseWhenSuccessfulTransaction() throws IOException {
        //given
        BrainTreeMonetaryAmount monetaryAmount = new BrainTreeMonetaryAmount();
        monetaryAmount.setValue(AMOUNT);
        monetaryAmount.setCurrencyCode(USD);

        BrainTreeRefundDetails brainTreeRefundDetails = new BrainTreeRefundDetails();
        brainTreeRefundDetails.setRefundId(REFUND_ID);

        brainTreeRefund.setStatus(BrainTreePaymentStatus.SETTLED);
        brainTreeRefund.setOrderId(ORDER_ID);
        brainTreeRefund.setLegacyId(LEGACY_ID);
        brainTreeRefund.setId(TRANSACTION_ID);
        brainTreeRefund.setAmount(monetaryAmount);
        brainTreeRefund.setCreatedAt(createdAt);
        brainTreeRefund.setPaymentMethodSnapshot(paymentMethodSnapshot);
        brainTreeRefund.setDetails(brainTreeRefundDetails);

        when(paymentInfoService.getPaymentInstrumentTypeBySnapshotAndOrigin(paymentMethodSnapshot))
                .thenReturn(PAYPAL_ACCOUNT_INSTRUMENT_TYPE);

        //when
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertTrue(commandResult.isSuccess());
        assertEquals(BrainTreePaymentStatus.SETTLED.name(), commandResult.getStatus());
        assertEquals(AMOUNT, commandResult.getAmount().toString());
        assertEquals(USD, commandResult.getCurrencyIsoCode());
        assertEquals(TRANSACTION_ID, commandResult.getTransactionGraphQLId());
        assertEquals(LEGACY_ID, commandResult.getTransactionId());
        assertEquals(ORDER_ID, commandResult.getOrderId());
        assertEquals(TransactionStatus.ACCEPTED, commandResult.getTransactionStatus());
        assertEquals(TransactionStatusDetails.SUCCESFULL, commandResult.getTransactionStatusDetails());
        assertNotNull(commandResult.getCreatedAt());
        assertEquals(CAPTURE_ID, commandResult.getCaptureId());
        assertEquals(PAYPAL_ACCOUNT_INSTRUMENT_TYPE, commandResult.getPaymentInstrumentType());
        assertEquals(REFUND_ID, commandResult.getRefundId());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(paymentInfoService).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
    }

    @Test
    public void shouldTranslateResponseWhenFailedTransaction() throws IOException {
        //given
        BrainTreeTransactionAuthorizationProcessorResponse processorResponse = new BrainTreeTransactionAuthorizationProcessorResponse();
        processorResponse.setMessage(PROCESSOR_RESPONSE_ERROR_MESSAGE);
        processorResponse.setLegacyCode(LEGACY_CODE);

        BrainTreePaymentStatusEvent statusEvent = new BrainTreePaymentStatusEvent();
        statusEvent.setProcessorResponse(processorResponse);

        brainTreeRefund.setStatus(BrainTreePaymentStatus.FAILED);
        brainTreeRefund.setStatusHistory(List.of(statusEvent));

        //when
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertEquals(TransactionStatus.REJECTED, commandResult.getTransactionStatus());
        assertEquals(PROCESSOR_RESPONSE_ERROR_MESSAGE, commandResult.getErrorMessage());
        assertEquals(LEGACY_CODE, commandResult.getErrorCode());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
    }

    @Test
    public void shouldTranslateResponseWhenTransactionStatusIsNull() throws IOException {
        //when
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertNull(commandResult.getStatus());
        assertNull(commandResult.getMerchantTransactionCode());
        assertNull(commandResult.getRequestId());
        assertNull(commandResult.getTransactionId());
        assertNull(commandResult.getTransactionGraphQLId());
        assertNull(commandResult.getRequestToken());
        assertNull(commandResult.getTransactionStatus());
        assertNull(commandResult.getTransactionStatusDetails());
        assertNull(commandResult.getAmount());
        assertNull(commandResult.getAmount());
        assertNull(commandResult.getCurrencyIsoCode());
        assertNull(commandResult.getCreatedAt());
        assertNull(commandResult.getCaptureId());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(objectMapper).convertValue(refundTransactionPayload, BrainTreeRefundTransactionPayload.class);
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
    }

    @Test
    public void shouldTranslateResponseWhenRefundTransactionIsNull() throws IOException {
        //given
        refundTransactionPayload.setRefund(null);

        //when
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertNull(commandResult.getStatus());
        assertNull(commandResult.getMerchantTransactionCode());
        assertNull(commandResult.getRequestId());
        assertNull(commandResult.getTransactionId());
        assertNull(commandResult.getTransactionGraphQLId());
        assertNull(commandResult.getRequestToken());
        assertNull(commandResult.getTransactionStatus());
        assertNull(commandResult.getTransactionStatusDetails());
        assertNull(commandResult.getAmount());
        assertNull(commandResult.getAmount());
        assertNull(commandResult.getCurrencyIsoCode());
        assertNull(commandResult.getCreatedAt());
        assertNull(commandResult.getCaptureId());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(objectMapper).convertValue(refundTransactionPayload, BrainTreeRefundTransactionPayload.class);
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
    }

    @Test
    public void shouldTranslateResponseWhenPartialCapturePayloadIsNull() throws IOException {
        //given
        when(objectMapper.convertValue(refundTransactionPayload, BrainTreeRefundTransactionPayload.class))
                .thenReturn(null);

        //when
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertNull(commandResult.getStatus());
        assertNull(commandResult.getMerchantTransactionCode());
        assertNull(commandResult.getRequestId());
        assertNull(commandResult.getTransactionId());
        assertNull(commandResult.getTransactionGraphQLId());
        assertNull(commandResult.getRequestToken());
        assertNull(commandResult.getTransactionStatus());
        assertNull(commandResult.getTransactionStatusDetails());
        assertNull(commandResult.getAmount());
        assertNull(commandResult.getAmount());
        assertNull(commandResult.getCurrencyIsoCode());
        assertNull(commandResult.getCreatedAt());
        assertNull(commandResult.getCaptureId());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(objectMapper).convertValue(refundTransactionPayload, BrainTreeRefundTransactionPayload.class);
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
    }

    @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
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertEquals(LEGACY_CODE, commandResult.getErrorCode());
        assertEquals(ERROR_MESSAGE, commandResult.getErrorMessage());
        assertEquals(TransactionStatus.REJECTED, commandResult.getTransactionStatus());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(objectMapper, never()).convertValue(refundTransactionPayload, BrainTreePartialCaptureTransactionPayload.class);
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
    }

    @Test
    public void shouldTranslateErrorResponseWhenErrorListEmptyError() throws IOException {
        //given
        List<Map<String, Object>> errorList = new ArrayList<>();
        mapResult.put(RESULT_ERRORS, errorList);

        //when
        BrainTreeRefundTransactionResult commandResult = command.perform(refundTransactionRequest);

        //then
        assertNotNull(commandResult);
        assertFalse(commandResult.isSuccess());
        assertNull(commandResult.getErrorCode());
        assertNull(commandResult.getErrorMessage());
        assertNotNull(commandResult.getRequestBody());
        assertNotNull(commandResult.getResponseBody());

        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(objectMapper, never()).convertValue(refundTransactionPayload, BrainTreePartialCaptureTransactionPayload.class);
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
        verify(loggingHandler, never()).getLogger();
    }

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


        //when
        command.perform(refundTransactionRequest);

        //then
        verify(command).makeGraphQlCall(eq(DEFINITION_FILE_NAME), any());
        verify(objectMapper, never()).convertValue(refundTransactionPayload, BrainTreePartialCaptureTransactionPayload.class);
        verify(paymentInfoService, never()).getPaymentInstrumentTypeBySnapshotAndOrigin(any(BrainTreePaymentMethodSnapshot.class));
        verify(loggingHandler, never()).getLogger();
    }

}
