/*
 * Copyright 2020 Amazon.com, Inc. or its affiliates.
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package software.amazon.lambda.powertools.parameters;

import org.assertj.core.data.MapEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
import software.amazon.awssdk.services.ssm.model.GetParameterResponse;
import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest;
import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse;
import software.amazon.awssdk.services.ssm.model.Parameter;
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;

import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;

public class SSMProviderTest {

    @Mock
    SsmClient client;

    @Mock
    TransformationManager transformationManager;

    @Captor
    ArgumentCaptor<GetParameterRequest> paramCaptor;

    @Captor
    ArgumentCaptor<GetParametersByPathRequest> paramByPathCaptor;

    CacheManager cacheManager;

    SSMProvider provider;

    @BeforeEach
    public void init() {
        openMocks(this);
        cacheManager = new CacheManager();
        provider = new SSMProvider(cacheManager, client);
    }

    @Test
    public void getValue() {
        String key = "Key1";
        String expectedValue = "Value1";
        initMock(expectedValue);

        String value = provider.getValue(key);

        assertThat(value).isEqualTo(expectedValue);
        assertThat(paramCaptor.getValue().name()).isEqualTo(key);
        assertThat(paramCaptor.getValue().withDecryption()).isFalse();
    }

    @Test
    public void getValueDecrypted() {
        String key = "Key2";
        String expectedValue = "Value2";
        initMock(expectedValue);

        String value = provider.withDecryption().getValue(key);

        assertThat(value).isEqualTo(expectedValue);
        assertThat(paramCaptor.getValue().name()).isEqualTo(key);
        assertThat(paramCaptor.getValue().withDecryption()).isTrue();
    }

    @Test
    public void getMultiple() {
        List<Parameter> parameters = new ArrayList<>();
        parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
        parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build());
        parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build());
        GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build();
        when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response);

        Map<String, String> params = provider.getMultiple("/prod/app1");
        assertThat(params).contains(
                MapEntry.entry("key1", "foo1"),
                MapEntry.entry("key2", "foo2"),
                MapEntry.entry("key3", "foo3"));
        assertThat(provider.get("/prod/app1/key1")).isEqualTo("foo1");
        assertThat(provider.get("/prod/app1/key2")).isEqualTo("foo2");
        assertThat(provider.get("/prod/app1/key3")).isEqualTo("foo3");

        assertThat(paramByPathCaptor.getValue().path()).isEqualTo("/prod/app1");
        assertThat(paramByPathCaptor.getValue().withDecryption()).isFalse();
        assertThat(paramByPathCaptor.getValue().recursive()).isFalse();
    }

    @Test
    public void getMultipleWithTrailingSlash() {
        List<Parameter> parameters = new ArrayList<>();
        parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
        parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build());
        parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build());
        GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build();
        when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response);

        Map<String, String> params = provider.getMultiple("/prod/app1/");
        assertThat(params).contains(
                MapEntry.entry("key1", "foo1"),
                MapEntry.entry("key2", "foo2"),
                MapEntry.entry("key3", "foo3"));
        assertThat(provider.get("/prod/app1/key1")).isEqualTo("foo1");
        assertThat(provider.get("/prod/app1/key2")).isEqualTo("foo2");
        assertThat(provider.get("/prod/app1/key3")).isEqualTo("foo3");

        assertThat(paramByPathCaptor.getValue().path()).isEqualTo("/prod/app1");
        assertThat(paramByPathCaptor.getValue().withDecryption()).isFalse();
        assertThat(paramByPathCaptor.getValue().recursive()).isFalse();
    }

    @Test
    public void getMultiple_cached_shouldNotCallSSM() {
        List<Parameter> parameters = new ArrayList<>();
        parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
        parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build());
        parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build());
        GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build();
        when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response);

        provider.getMultiple("/prod/app1");

        // should get the following from cache
        provider.getMultiple("/prod/app1");
        provider.get("/prod/app1/key1");
        provider.get("/prod/app1/key2");
        provider.get("/prod/app1/key3");

        verify(client, times(1)).getParametersByPath(any(GetParametersByPathRequest.class));

    }

    @Test
    public void getMultipleWithNextToken() {
        List<Parameter> parameters1 = new ArrayList<>();
        parameters1.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
        parameters1.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build());
        GetParametersByPathResponse response1 = GetParametersByPathResponse.builder().parameters(parameters1).nextToken("123abc").build();

        List<Parameter> parameters2 = new ArrayList<>();
        parameters2.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build());
        GetParametersByPathResponse response2 = GetParametersByPathResponse.builder().parameters(parameters2).build();

        when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response1, response2);

        Map<String, String> params = provider.getMultiple("/prod/app1");

        assertThat(params).contains(
                MapEntry.entry("key1", "foo1"),
                MapEntry.entry("key2", "foo2"),
                MapEntry.entry("key3", "foo3"));

        List<GetParametersByPathRequest> requestParams = paramByPathCaptor.getAllValues();
        GetParametersByPathRequest request1 = requestParams.get(0);
        GetParametersByPathRequest request2 = requestParams.get(1);

        assertThat(asList(request1, request2)).allSatisfy(req -> {
            assertThat(req.path()).isEqualTo("/prod/app1");
            assertThat(req.withDecryption()).isFalse();
            assertThat(req.recursive()).isFalse();
        });

        assertThat(request1.nextToken()).isNull();
        assertThat(request2.nextToken()).isEqualTo("123abc");
    }

    @Test
    public void testSecretsProviderBuilderMissingCacheManager_throwsException() {

        // Act & Assert
        assertThatIllegalStateException().isThrownBy(() -> SSMProvider.builder()
                        .withClient(client)
                        .withTransformationManager(transformationManager)
                        .build())
                .withMessage("No CacheManager provided, please provide one");
    }

    private void initMock(String expectedValue) {
        Parameter parameter = Parameter.builder().value(expectedValue).build();
        GetParameterResponse result = GetParameterResponse.builder().parameter(parameter).build();
        when(client.getParameter(paramCaptor.capture())).thenReturn(result);
        provider.defaultMaxAge(1, ChronoUnit.DAYS);
        provider.withMaxAge(2, ChronoUnit.DAYS);
        provider.recursive();
    }

}