/*
 * Copyright (c) 2018 Baidu, Inc. All Rights Reserved.
 *
 * 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 com.baidubce.formula.consul.config.spring.boot;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import com.baidubce.formula.consul.config.spring.boot.auth.BmsAuthClient;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.OperationException;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.kv.model.GetValue;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.test.context.ActiveProfiles;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;

import static com.baidubce.formula.consul.config.spring.boot.Format.FILES;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * @author luoguangming
 */
@ActiveProfiles("logging-test")
public class ConfigWatchTests {

    private ConsulConfigProperties configProperties;

    private BmsAuthClient bmsAuthClient;

    @Before
    public void setUp() throws Exception {
        this.configProperties = new ConsulConfigProperties();
        this.bmsAuthClient = new BmsAuthClient();

        Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        root.setLevel(Level.TRACE);
    }

    @Test
    public void watchPublishesEvent() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        setupWatch(eventPublisher, "/app/");

        verify(eventPublisher, times(1)).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithNullValueDoesNotPublishEvent() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        Response<List<GetValue>> response = new Response<>(null, 1l, false, 1L);

        setupWatch(eventPublisher, "/app/", response);

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchForFileFormatPublishesEvent() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        this.configProperties.setFormat(FILES);

        setupWatch(eventPublisher, "/config/app.yml");

        verify(eventPublisher, times(1)).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithNullIndexDoesNotPublishEvent() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        Response<List<GetValue>> response = new Response<>(Arrays.asList(new GetValue()), null, false, 1L);

        setupWatch(eventPublisher, "/app/", response);

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithIllegalIndexDoesNotPublishEvent() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        Response<List<GetValue>> response = new Response<>(Arrays.asList(new GetValue()), 0l, false, 1L);

        setupWatchWithIllegalIndex(eventPublisher, "/app/", response);

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithNullResponse() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        Response<List<GetValue>> response = null;

        setupWatch(eventPublisher, "/app/", response);

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithOperationException() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        setupWatchThrowException(eventPublisher, "/app/");

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithExceptionAndFailFast() {
        this.configProperties.setFailFast(true);
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        setupWatch(eventPublisher, "/app/", null);

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void watchWithExceptionAndWarnLog() {
        Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        root.setLevel(Level.WARN);
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);

        setupWatch(eventPublisher, "/app/", null);

        verify(eventPublisher, never()).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    private void setupWatch(ApplicationEventPublisher eventPublisher, String context) {
        Response<List<GetValue>> response = new Response<>(Arrays.asList(new GetValue()), 1L, false, 1L);
        this.setupWatch(eventPublisher, context, response);
    }

    private void setupWatch(ApplicationEventPublisher eventPublisher, String context,
                            Response<List<GetValue>> response) {
        ConsulClient consul = mock(ConsulClient.class);

        when(consul.getKVValues(ArgumentMatchers.eq(context), nullable(String.class),
                ArgumentMatchers.any(QueryParams.class))).thenReturn(response);

        LinkedHashMap<String, Long> initialIndexes = new LinkedHashMap<>();
        initialIndexes.put(context, 0L);
        startWatch(eventPublisher, consul, initialIndexes);
    }

    private void setupWatchThrowException(ApplicationEventPublisher eventPublisher, String context) {
        ConsulClient consul = mock(ConsulClient.class);
        OperationException operationException = new OperationException(403, null, null);
        when(consul.getKVValues(ArgumentMatchers.eq(context), nullable(String.class),
                ArgumentMatchers.any(QueryParams.class))).thenThrow(operationException);

        LinkedHashMap<String, Long> initialIndexes = new LinkedHashMap<>();
        initialIndexes.put(context, 0L);
        startWatch(eventPublisher, consul, initialIndexes);
    }

    private void setupWatchWithIllegalIndex(ApplicationEventPublisher eventPublisher, String context,
                                            Response<List<GetValue>> response) {
        ConsulClient consul = mock(ConsulClient.class);

        when(consul.getKVValues(ArgumentMatchers.eq(context), nullable(String.class),
                ArgumentMatchers.any(QueryParams.class))).thenReturn(response);

        LinkedHashMap<String, Long> initialIndexes = new LinkedHashMap<>();
        initialIndexes.put(context, -1L);
        startWatch(eventPublisher, consul, initialIndexes);
    }

    private void startWatch(ApplicationEventPublisher eventPublisher,
                            ConsulClient consul, LinkedHashMap<String, Long> initialIndexes) {
        ConfigWatch watch = new ConfigWatch(this.configProperties, consul, bmsAuthClient, initialIndexes);
        watch.setApplicationEventPublisher(eventPublisher);

        watch.start();

        try {
            // wait until watch is triggered asynchronously
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testGetMethods() {
        LinkedHashMap<String, Long> initialIndexes = new LinkedHashMap<>();
        initialIndexes.put("/context", 0L);
        ConfigWatch watch = new ConfigWatch(this.configProperties, new ConsulClient(), bmsAuthClient, initialIndexes);
        Assert.assertEquals(0, watch.getPhase());
        Assert.assertEquals(true, watch.isAutoStartup());
    }

    @Test
    public void testConstructor() {
        LinkedHashMap<String, Long> initialIndexes = new LinkedHashMap<>();
        initialIndexes.put("/context/", -1L);
        List<ThreadPoolTaskScheduler> threadPoolTaskSchedulers = new ArrayList<>();
        ConfigWatch watch = new ConfigWatch(this.configProperties, null, bmsAuthClient, initialIndexes,
                threadPoolTaskSchedulers);
        Assert.assertNotNull(watch);
    }

    @Test
    public void testStop() {
        LinkedHashMap<String, Long> initialIndexes = new LinkedHashMap<>();
        initialIndexes.put("/context", 0L);
        ConfigWatch watch = new ConfigWatch(this.configProperties, new ConsulClient(), bmsAuthClient, initialIndexes);
        watch.start();
        Assert.assertTrue(watch.isRunning());
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // do nothing
            }
        };
        watch.stop(runnable);
        Assert.assertFalse(watch.isRunning());
    }

    @Test
    public void testRefreshEventData() {
        String context = "/test";
        Long preIndex = 1l;
        Long newIndex = 2l;
        ConfigWatch.RefreshEventData refreshEventData = new ConfigWatch.RefreshEventData(context, preIndex, newIndex);
        Assert.assertEquals(context, refreshEventData.getContext());
        Assert.assertEquals(preIndex, refreshEventData.getPrevIndex());
        Assert.assertEquals(newIndex, refreshEventData.getNewIndex());
        Assert.assertTrue(refreshEventData.equals(refreshEventData));
        Assert.assertFalse(refreshEventData == null);
        Assert.assertNotNull(refreshEventData.hashCode());
        ConfigWatch.RefreshEventData refreshEventData2 = new ConfigWatch.RefreshEventData(context, preIndex, newIndex);
        Assert.assertTrue(refreshEventData.equals(refreshEventData2));
    }

    @Test
    public void firstCallDoesNotPublishEvent() {
        ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
        this.configProperties.setFormat(FILES);

        GetValue getValue = new GetValue();
        String context = "/config/app.yml";
        ConsulClient consul = mock(ConsulClient.class);
        List<GetValue> getValues = Collections.singletonList(getValue);

        Response<List<GetValue>> response = new Response<>(getValues, 1L, false, 1L);
        when(consul.getKVValues(ArgumentMatchers.eq(context), ArgumentMatchers.anyString(),
                ArgumentMatchers.any(QueryParams.class)))
                .thenReturn(response);

        ConfigWatch watch = new ConfigWatch(this.configProperties, consul, bmsAuthClient,
                new LinkedHashMap<String, Long>());
        watch.setApplicationEventPublisher(eventPublisher);

        watch.watchConfigKeyValues(context);
        verify(eventPublisher, times(0)).publishEvent(ArgumentMatchers.any(RefreshEvent.class));
    }

    @Test
    public void testBmsHttpTransportConstructor() {
        BmsHttpTransport bmsHttpTransport = new BmsHttpTransport();
        Assert.assertNotNull(bmsHttpTransport);
        Assert.assertNotNull(bmsHttpTransport.getHttpClient());
    }
}