/*
 * Copyright 1999-2020 Alibaba Group Holding Ltd.
 *
 * 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.alibaba.nacos.client.naming.remote.gprc.redo;

import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.env.NacosClientProperties;
import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder;
import com.alibaba.nacos.client.naming.remote.TestConnection;
import com.alibaba.nacos.client.naming.remote.gprc.NamingGrpcClientProxy;
import com.alibaba.nacos.client.naming.remote.gprc.redo.data.BatchInstanceRedoData;
import com.alibaba.nacos.client.naming.remote.gprc.redo.data.InstanceRedoData;
import com.alibaba.nacos.client.naming.remote.gprc.redo.data.SubscriberRedoData;
import com.alibaba.nacos.common.remote.client.RpcClient;
import com.alibaba.nacos.common.utils.ReflectUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(MockitoExtension.class)
class NamingGrpcRedoServiceTest {
    
    private static final String SERVICE = "service";
    
    private static final String GROUP = "group";
    
    private static final String CLUSTER = "cluster";
    
    @Mock
    private NamingGrpcClientProxy clientProxy;
    
    @Mock
    private NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder;
    
    private NamingGrpcRedoService redoService;
    
    @BeforeEach
    void setUp() throws Exception {
        Properties prop = new Properties();
        NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(prop);
        redoService = new NamingGrpcRedoService(clientProxy, namingFuzzyWatchServiceListHolder, nacosClientProperties);
        ScheduledExecutorService redoExecutor = (ScheduledExecutorService) ReflectUtils.getFieldValue(redoService,
                "redoExecutor");
        redoExecutor.shutdownNow();
    }
    
    @AfterEach
    void tearDown() throws Exception {
        redoService.shutdown();
    }
    
    @Test
    void testDefaultProperties() throws Exception {
        Field redoThreadCountField = NamingGrpcRedoService.class.getDeclaredField("redoThreadCount");
        redoThreadCountField.setAccessible(true);
        
        Field redoDelayTimeField = NamingGrpcRedoService.class.getDeclaredField("redoDelayTime");
        redoDelayTimeField.setAccessible(true);
        
        Long redoDelayTimeValue = (Long) redoDelayTimeField.get(redoService);
        Integer redoThreadCountValue = (Integer) redoThreadCountField.get(redoService);
        
        assertEquals(Long.valueOf(3000L), redoDelayTimeValue);
        assertEquals(Integer.valueOf(1), redoThreadCountValue);
    }
    
    @Test
    void testCustomProperties() throws Exception {
        Properties prop = new Properties();
        prop.setProperty(PropertyKeyConst.REDO_DELAY_TIME, "4000");
        prop.setProperty(PropertyKeyConst.REDO_DELAY_THREAD_COUNT, "2");
        NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(prop);
        
        NamingGrpcRedoService redoService = new NamingGrpcRedoService(clientProxy, namingFuzzyWatchServiceListHolder, nacosClientProperties);
        
        Field redoThreadCountField = NamingGrpcRedoService.class.getDeclaredField("redoThreadCount");
        redoThreadCountField.setAccessible(true);
        
        Field redoDelayTimeField = NamingGrpcRedoService.class.getDeclaredField("redoDelayTime");
        redoDelayTimeField.setAccessible(true);
        
        Long redoDelayTimeValue = (Long) redoDelayTimeField.get(redoService);
        Integer redoThreadCountValue = (Integer) redoThreadCountField.get(redoService);
        assertEquals(Long.valueOf(4000L), redoDelayTimeValue);
        assertEquals(Integer.valueOf(2), redoThreadCountValue);
    }
    
    @Test
    void testOnConnected() {
        assertFalse(redoService.isConnected());
        redoService.onConnected(new TestConnection(new RpcClient.ServerInfo()));
        assertTrue(redoService.isConnected());
    }
    
    @Test
    void testOnDisConnect() {
        redoService.onConnected(new TestConnection(new RpcClient.ServerInfo()));
        redoService.cacheInstanceForRedo(SERVICE, GROUP, new Instance());
        redoService.instanceRegistered(SERVICE, GROUP);
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        redoService.subscriberRegistered(SERVICE, GROUP, CLUSTER);
        assertTrue(redoService.isConnected());
        assertTrue(redoService.findInstanceRedoData().isEmpty());
        assertTrue(redoService.findSubscriberRedoData().isEmpty());
        redoService.onDisConnect(new TestConnection(new RpcClient.ServerInfo()));
        assertFalse(redoService.isConnected());
        assertFalse(redoService.findInstanceRedoData().isEmpty());
        assertFalse(redoService.findSubscriberRedoData().isEmpty());
    }
    
    @Test
    void testCacheInstanceForRedo() {
        ConcurrentMap<String, InstanceRedoData> registeredInstances = getInstanceRedoDataMap();
        assertTrue(registeredInstances.isEmpty());
        Instance instance = new Instance();
        redoService.cacheInstanceForRedo(SERVICE, GROUP, instance);
        assertFalse(registeredInstances.isEmpty());
        InstanceRedoData actual = registeredInstances.entrySet().iterator().next().getValue();
        assertEquals(SERVICE, actual.getServiceName());
        assertEquals(GROUP, actual.getGroupName());
        assertEquals(instance, actual.get());
        assertFalse(actual.isRegistered());
        assertFalse(actual.isUnregistering());
        assertTrue(actual.isExpectedRegistered());
    }
    
    @Test
    void testCacheInstanceForRedoByBatchInstanceRedoData() {
        ConcurrentMap<String, InstanceRedoData> registeredInstances = getInstanceRedoDataMap();
        assertTrue(registeredInstances.isEmpty());
        Instance instance = new Instance();
        List<Instance> instanceList = new ArrayList<>();
        instanceList.add(instance);
        redoService.cacheInstanceForRedo(SERVICE, GROUP, instanceList);
        assertFalse(registeredInstances.isEmpty());
        BatchInstanceRedoData actual = (BatchInstanceRedoData) registeredInstances.entrySet().iterator().next()
                .getValue();
        assertEquals(SERVICE, actual.getServiceName());
        assertEquals(GROUP, actual.getGroupName());
        assertEquals(instanceList, actual.getInstances());
        assertFalse(actual.isRegistered());
        assertFalse(actual.isUnregistering());
    }
    
    @Test
    void testInstanceRegistered() {
        ConcurrentMap<String, InstanceRedoData> registeredInstances = getInstanceRedoDataMap();
        redoService.cacheInstanceForRedo(SERVICE, GROUP, new Instance());
        redoService.instanceRegistered(SERVICE, GROUP);
        InstanceRedoData actual = registeredInstances.entrySet().iterator().next().getValue();
        assertTrue(actual.isRegistered());
    }
    
    @Test
    void testInstanceDeregister() {
        ConcurrentMap<String, InstanceRedoData> registeredInstances = getInstanceRedoDataMap();
        redoService.cacheInstanceForRedo(SERVICE, GROUP, new Instance());
        redoService.instanceDeregister(SERVICE, GROUP);
        InstanceRedoData actual = registeredInstances.entrySet().iterator().next().getValue();
        assertTrue(actual.isUnregistering());
        assertFalse(actual.isExpectedRegistered());
    }
    
    @Test
    void testInstanceDeregistered() {
        ConcurrentMap<String, InstanceRedoData> registeredInstances = getInstanceRedoDataMap();
        redoService.cacheInstanceForRedo(SERVICE, GROUP, new Instance());
        redoService.instanceDeregistered(SERVICE, GROUP);
        InstanceRedoData actual = registeredInstances.entrySet().iterator().next().getValue();
        assertFalse(actual.isRegistered());
        assertTrue(actual.isUnregistering());
    }
    
    @Test
    void testRemoveInstanceForRedo() {
        ConcurrentMap<String, InstanceRedoData> registeredInstances = getInstanceRedoDataMap();
        assertTrue(registeredInstances.isEmpty());
        redoService.cacheInstanceForRedo(SERVICE, GROUP, new Instance());
        assertFalse(registeredInstances.isEmpty());
        redoService.instanceDeregister(SERVICE, GROUP);
        redoService.removeInstanceForRedo(SERVICE, GROUP);
        assertTrue(registeredInstances.isEmpty());
    }
    
    @Test
    void testFindInstanceRedoData() {
        redoService.cacheInstanceForRedo(SERVICE, GROUP, new Instance());
        assertFalse(redoService.findInstanceRedoData().isEmpty());
        redoService.instanceRegistered(SERVICE, GROUP);
        assertTrue(redoService.findInstanceRedoData().isEmpty());
        redoService.instanceDeregister(SERVICE, GROUP);
        assertFalse(redoService.findInstanceRedoData().isEmpty());
    }
    
    @SuppressWarnings("all")
    private ConcurrentMap<String, InstanceRedoData> getInstanceRedoDataMap() {
        return (ConcurrentMap<String, InstanceRedoData>) ReflectUtils.getFieldValue(redoService, "registeredInstances");
    }
    
    @Test
    void testCacheSubscriberForRedo() {
        ConcurrentMap<String, SubscriberRedoData> subscribes = getSubscriberRedoDataMap();
        assertTrue(subscribes.isEmpty());
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        assertFalse(subscribes.isEmpty());
        SubscriberRedoData actual = subscribes.entrySet().iterator().next().getValue();
        assertEquals(SERVICE, actual.getServiceName());
        assertEquals(GROUP, actual.getGroupName());
        assertEquals(CLUSTER, actual.get());
        assertFalse(actual.isRegistered());
        assertFalse(actual.isUnregistering());
    }
    
    @Test
    void testSubscriberRegistered() {
        ConcurrentMap<String, SubscriberRedoData> subscribes = getSubscriberRedoDataMap();
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        redoService.subscriberRegistered(SERVICE, GROUP, CLUSTER);
        SubscriberRedoData actual = subscribes.entrySet().iterator().next().getValue();
        assertTrue(actual.isRegistered());
    }
    
    @Test
    void testSubscriberDeregister() {
        ConcurrentMap<String, SubscriberRedoData> subscribes = getSubscriberRedoDataMap();
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        redoService.subscriberDeregister(SERVICE, GROUP, CLUSTER);
        SubscriberRedoData actual = subscribes.entrySet().iterator().next().getValue();
        assertTrue(actual.isUnregistering());
    }
    
    @Test
    void testIsSubscriberRegistered() {
        assertFalse(redoService.isSubscriberRegistered(SERVICE, GROUP, CLUSTER));
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        redoService.subscriberRegistered(SERVICE, GROUP, CLUSTER);
        assertTrue(redoService.isSubscriberRegistered(SERVICE, GROUP, CLUSTER));
    }
    
    @Test
    void testRemoveSubscriberForRedo() {
        ConcurrentMap<String, SubscriberRedoData> subscribes = getSubscriberRedoDataMap();
        assertTrue(subscribes.isEmpty());
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        assertFalse(subscribes.isEmpty());
        redoService.subscriberDeregister(SERVICE, GROUP, CLUSTER);
        redoService.removeSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        assertTrue(subscribes.isEmpty());
    }
    
    @Test
    void testFindSubscriberRedoData() {
        redoService.cacheSubscriberForRedo(SERVICE, GROUP, CLUSTER);
        assertFalse(redoService.findSubscriberRedoData().isEmpty());
        redoService.subscriberRegistered(SERVICE, GROUP, CLUSTER);
        assertTrue(redoService.findSubscriberRedoData().isEmpty());
        redoService.subscriberDeregister(SERVICE, GROUP, CLUSTER);
        assertFalse(redoService.findSubscriberRedoData().isEmpty());
    }
    
    @SuppressWarnings("all")
    private ConcurrentMap<String, SubscriberRedoData> getSubscriberRedoDataMap() {
        return (ConcurrentMap<String, SubscriberRedoData>) ReflectUtils.getFieldValue(redoService, "subscribes");
    }
}
