package com.mastercard.developer.interceptors;

import com.mastercard.developer.encryption.EncryptionException;
import com.mastercard.developer.encryption.FieldLevelEncryptionConfig;
import feign.Response;
import feign.Util;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;

import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;

import static com.mastercard.developer.test.TestUtils.assertPayloadEquals;
import static com.mastercard.developer.test.TestUtils.getTestFieldLevelEncryptionConfigBuilder;
import static com.mastercard.developer.utils.FeignUtils.readHeader;
import static org.hamcrest.core.Is.isA;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class OpenFeignFieldLevelEncryptionDecoderTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Test
    public void testDecode_ShouldDecryptResponsePayloadAndUpdateContentLengthHeader() throws Exception {

        // GIVEN
        String encryptedPayload = "{" +
                "    \"encryptedData\": {" +
                "        \"iv\": \"a32059c51607d0d02e823faecda5fb15\"," +
                "        \"encryptedKey\": \"a31cfe7a7981b72428c013270619554c1d645c04b9d51c7eaf996f55749ef62fd7c7f8d334f95913be41ae38c46d192670fd1acb84ebb85a00cd997f1a9a3f782229c7bf5f0fdf49fe404452d7ed4fd41fbb95b787d25893fbf3d2c75673cecc8799bbe3dd7eb4fe6d3f744b377572cdf8aba1617194e10475b6cd6a8dd4fb8264f8f51534d8f7ac7c10b4ce9c44d15066724b03a0ab0edd512f9e6521fdb5841cd6964e457d6b4a0e45ba4aac4e77d6bbe383d6147e751fa88bc26278bb9690f9ee84b17123b887be2dcef0873f4f9f2c895d90e23456fafb01b99885e31f01a3188f0ad47edf22999cc1d0ddaf49e1407375117b5d66f1f185f2b57078d255\"," +
                "        \"encryptedValue\": \"21d754bdb4567d35d58720c9f8364075\"," +
                "        \"oaepHashingAlgorithm\": \"SHA256\"" +
                "    }" +
                "}";
        FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder()
                .withDecryptionPath("$.encryptedData", "$.data")
                .build();

        Type type = mock(Type.class);
        HashMap<String, Collection<String>> headers = new HashMap<String, Collection<String>>() {
            {
                put("content-length", Collections.singleton("100"));
            }
        };
        Response response = Response.builder()
                .status(200)
                .headers(headers)
                .body(encryptedPayload, StandardCharsets.UTF_8)
                .build();
        Decoder delegate = mock(Decoder.class);

        // WHEN
        OpenFeignFieldLevelEncryptionDecoder instanceUnderTest = new OpenFeignFieldLevelEncryptionDecoder(config, delegate);
        instanceUnderTest.decode(response, type);

        // THEN
        ArgumentCaptor<Response> responseCaptor = ArgumentCaptor.forClass(Response.class);
        verify(delegate).decode(responseCaptor.capture(), any(Type.class));
        Response responseValue = responseCaptor.getValue();
        String payload = Util.toString(responseValue.body().asReader());
        assertPayloadEquals("{\"data\":\"string\"}", payload);
        assertEquals(String.valueOf(payload.length()), readHeader(responseValue, "Content-Length"));
    }

    @Test
    public void testDecode_ShouldDoNothing_WhenNoPayload() throws Exception {

        // GIVEN
        FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder().build();
        Type type = mock(Type.class);
        Response response = mock(Response.class);
        Decoder delegate = mock(Decoder.class);
        when(response.body()).thenReturn(null);

        // WHEN
        OpenFeignFieldLevelEncryptionDecoder instanceUnderTest = new OpenFeignFieldLevelEncryptionDecoder(config, delegate);
        instanceUnderTest.decode(response, type);

        // THEN
        verify(delegate).decode(any(Response.class), any(Type.class));
        verify(response).body();
        verifyNoMoreInteractions(response);
    }

    @Test
    public void testDecode_ShouldDoNothing_WhenEmptyPayload() throws Exception {

        // GIVEN
        FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder().build();
        Type type = mock(Type.class);
        Response response = mock(Response.class);
        when(response.body()).thenReturn(buildResponseBody(""));
        Decoder delegate = mock(Decoder.class);

        // WHEN
        OpenFeignFieldLevelEncryptionDecoder instanceUnderTest = new OpenFeignFieldLevelEncryptionDecoder(config, delegate);
        instanceUnderTest.decode(response, type);

        // THEN
        verify(delegate).decode(any(Response.class), any(Type.class));
        verify(response).body();
        verifyNoMoreInteractions(response);
    }

    @Test
    public void testDecode_ShouldThrowDecodeException_WhenDecryptionFails() throws Exception {

        // GIVEN
        String encryptedPayload = "{" +
                "    \"encryptedData\": {" +
                "        \"iv\": \"a2c494ca28dec4f3d6ce7d68b1044cfe\"," +
                "        \"encryptedKey\": \"NOT A VALID KEY!\"," +
                "        \"encryptedValue\": \"0672589113046bf692265b6ea6088184\"" +
                "    }" +
                "}";
        FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder()
                .withDecryptionPath("$.encryptedData", "$.data")
                .build();
        Type type = mock(Type.class);
        Response response = mock(Response.class);
        when(response.body()).thenReturn(buildResponseBody(encryptedPayload));
        Decoder delegate = mock(Decoder.class);

        // THEN
        expectedException.expect(DecodeException.class);
        expectedException.expectMessage("Failed to intercept and decrypt response!");
        expectedException.expectCause(isA(EncryptionException.class));

        // WHEN
        OpenFeignFieldLevelEncryptionDecoder instanceUnderTest = new OpenFeignFieldLevelEncryptionDecoder(config, delegate);
        instanceUnderTest.decode(response, type);
    }

    @Test
    public void testDecode_ShouldDecryptResponsePayloadAndRemoveEncryptionHttpHeaders_WhenRequestedInConfig() throws Exception {

        // GIVEN
        String encryptedPayload = "{" +
                "    \"encryptedData\": {" +
                "        \"encryptedValue\": \"21d754bdb4567d35d58720c9f8364075\"" +
                "    }" +
                "}";
        FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder()
                .withDecryptionPath("$.encryptedData", "$.data")
                .withIvHeaderName("x-iv")
                .withEncryptedKeyHeaderName("x-encrypted-key")
                .withOaepPaddingDigestAlgorithmHeaderName("x-oaep-padding-digest-algorithm")
                .withEncryptionCertificateFingerprintHeaderName("x-encryption-certificate-fingerprint")
                .withEncryptionKeyFingerprintHeaderName("x-encryption-key-fingerprint")
                .build();

        Type type = mock(Type.class);
        HashMap<String, Collection<String>> headers = new HashMap<String, Collection<String>>() {
            {
                put("content-length", Collections.singleton("100"));
                put("x-iv", Collections.singleton("a32059c51607d0d02e823faecda5fb15"));
                put("x-encrypted-key", Collections.singleton("a31cfe7a7981b72428c013270619554c1d645c04b9d51c7eaf996f55749ef62fd7c7f8d334f95913be41ae38c46d192670fd1acb84ebb85a00cd997f1a9a3f782229c7bf5f0fdf49fe404452d7ed4fd41fbb95b787d25893fbf3d2c75673cecc8799bbe3dd7eb4fe6d3f744b377572cdf8aba1617194e10475b6cd6a8dd4fb8264f8f51534d8f7ac7c10b4ce9c44d15066724b03a0ab0edd512f9e6521fdb5841cd6964e457d6b4a0e45ba4aac4e77d6bbe383d6147e751fa88bc26278bb9690f9ee84b17123b887be2dcef0873f4f9f2c895d90e23456fafb01b99885e31f01a3188f0ad47edf22999cc1d0ddaf49e1407375117b5d66f1f185f2b57078d255"));
                put("x-oaep-padding-digest-algorithm", Collections.singleton("SHA256"));
                put("x-encryption-key-fingerprint", Collections.singleton("761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79"));
                put("x-encryption-certificate-fingerprint", Collections.singleton("80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279"));
            }
        };
        Response response = Response.builder()
                .status(200)
                .headers(headers)
                .body(encryptedPayload, StandardCharsets.UTF_8)
                .build();
        Decoder delegate = mock(Decoder.class);

        // WHEN
        OpenFeignFieldLevelEncryptionDecoder instanceUnderTest = new OpenFeignFieldLevelEncryptionDecoder(config, delegate);
        instanceUnderTest.decode(response, type);

        // THEN
        ArgumentCaptor<Response> responseCaptor = ArgumentCaptor.forClass(Response.class);
        verify(delegate).decode(responseCaptor.capture(), any(Type.class));
        Response responseValue = responseCaptor.getValue();
        String payload = Util.toString(responseValue.body().asReader());
        assertPayloadEquals("{\"data\":\"string\"}", payload);
        assertEquals(String.valueOf(payload.length()), readHeader(responseValue, "Content-Length"));
        assertNull(readHeader(responseValue, "x-iv"));
        assertNull(readHeader(responseValue, "x-encrypted-key"));
        assertNull(readHeader(responseValue, "x-oaep-padding-digest-algorithm"));
        assertNull(readHeader(responseValue, "x-encryption-key-fingerprint"));
        assertNull(readHeader(responseValue, "x-encryption-certificate-fingerprint"));
    }

    private static Response.Body buildResponseBody(String payload) {
        Response response = Response.builder()
                .status(200)
                .headers(new HashMap<String, Collection<String>>())
                .body(payload, StandardCharsets.UTF_8)
                .build();
        return response.body();
    }
}