/*
 * Copyright 1999-2018 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.config.server.service;

import com.alibaba.nacos.common.utils.MD5Utils;
import com.alibaba.nacos.config.server.model.CacheItem;
import com.alibaba.nacos.config.server.model.ConfigCacheGray;
import com.alibaba.nacos.config.server.model.gray.GrayRuleManager;
import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskService;
import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory;
import com.alibaba.nacos.config.server.utils.GroupKey2;
import com.alibaba.nacos.config.server.utils.PropertyUtil;
import com.alibaba.nacos.config.server.utils.SimpleReadWriteLock;
import com.alibaba.nacos.sys.env.EnvUtil;
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.stubbing.OngoingStubbing;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;

@ExtendWith(SpringExtension.class)
class ConfigCacheServiceTest {
    
    MockedStatic<PropertyUtil> propertyUtilMockedStatic;
    
    MockedStatic<ConfigDiskServiceFactory> configDiskServiceFactoryMockedStatic;
    
    @Mock
    ConfigDiskService configDiskService;
    
    MockedStatic<EnvUtil> envUtilMockedStatic;
    
    @BeforeEach
    void before() {
        envUtilMockedStatic = Mockito.mockStatic(EnvUtil.class);
        configDiskServiceFactoryMockedStatic = Mockito.mockStatic(ConfigDiskServiceFactory.class);
        configDiskServiceFactoryMockedStatic.when(() -> ConfigDiskServiceFactory.getInstance())
                .thenReturn(configDiskService);
        propertyUtilMockedStatic = Mockito.mockStatic(PropertyUtil.class);
    }
    
    @AfterEach
    void after() {
        envUtilMockedStatic.close();
        propertyUtilMockedStatic.close();
        configDiskServiceFactoryMockedStatic.close();
    }
    
    @Test
    void testDumpFormal() throws Exception {
        String dataId = "dataIdtestDumpMd5NewTsNewMd5123";
        String group = "group11";
        String tenant = "tenant112";
        String content = "mockContnet11";
        String md5 = "mockmd511";
        String groupKey = GroupKey2.getKey(dataId, group, tenant);
        //make sure not exist prev cache.
        CacheItem contentCache = ConfigCacheService.getContentCache(groupKey);
        assertTrue(contentCache == null);
        long ts = System.currentTimeMillis();
        String type = "json";
        String encryptedDataKey = "key12345";
        boolean result = ConfigCacheService.dumpWithMd5(dataId, group, tenant, content, md5, ts, type,
                encryptedDataKey);
        assertTrue(result);
        //verify cache.
        CacheItem contentCache1 = ConfigCacheService.getContentCache(groupKey);
        assertEquals(ts, contentCache1.getConfigCache().getLastModifiedTs());
        assertEquals(md5, contentCache1.getConfigCache().getMd5());
        assertEquals(type, contentCache1.getType());
        assertEquals(encryptedDataKey, contentCache1.getConfigCache().getEncryptedDataKey());
        Mockito.verify(configDiskService, times(1)).saveToDisk(eq(dataId), eq(group), eq(tenant), eq(content));
        
        //modified ts and content and md5
        String contentNew = content + "11";
        long newTs = System.currentTimeMillis() + 12L;
        ConfigCacheService.dump(dataId, group, tenant, contentNew, newTs, type, encryptedDataKey);
        //expect save to disk invoked.
        Mockito.verify(configDiskService, times(1)).saveToDisk(eq(dataId), eq(group), eq(tenant), eq(contentNew));
        assertEquals(newTs, contentCache1.getConfigCache().getLastModifiedTs());
        String newMd5 = MD5Utils.md5Hex(contentNew, "UTF-8");
        assertEquals(newMd5, contentCache1.getConfigCache().getMd5());
        
        //modified ts old
        long oldTs2 = newTs - 123L;
        String contentWithOldTs = contentNew + "123456";
        ConfigCacheService.dump(dataId, group, tenant, contentWithOldTs, oldTs2, type, encryptedDataKey);
        //expect save to disk invoked.
        Mockito.verify(configDiskService, times(0)).saveToDisk(eq(dataId), eq(group), eq(tenant), eq(contentWithOldTs));
        //not change ts and md5
        assertEquals(newTs, contentCache1.getConfigCache().getLastModifiedTs());
        assertEquals(newMd5, contentCache1.getConfigCache().getMd5());
        
        //modified ts new only
        long newTs2 = newTs + 123L;
        ConfigCacheService.dump(dataId, group, tenant, contentNew, newTs2, type, encryptedDataKey);
        assertEquals(newTs2, contentCache1.getConfigCache().getLastModifiedTs());
        
        //save to disk error
        doThrow(new IOException("No space left on device")).when(configDiskService)
                .saveToDisk(anyString(), anyString(), anyString(), anyString());
        try {
            long newTs3 = newTs2 + 123L;
            boolean dumpErrorResult = ConfigCacheService.dump(dataId, group, tenant, contentNew + "234567", newTs3,
                    type, encryptedDataKey);
            envUtilMockedStatic.verify(() -> EnvUtil.systemExit(), times(1));
            assertFalse(dumpErrorResult);
        } catch (Throwable throwable) {
            assertFalse(true);
        }
        
        //test remove
        boolean remove = ConfigCacheService.remove(dataId, group, tenant);
        assertTrue(remove);
        Mockito.verify(configDiskService, times(1)).removeConfigInfo(dataId, group, tenant);
        CacheItem contentCacheAfterRemove = ConfigCacheService.getContentCache(groupKey);
        assertNull(contentCacheAfterRemove);
        
    }
    
    @Test
    public void testDumpGray() throws Exception {
        String dataId = "dataIdtestDumpBetaNewCache123";
        String group = "group11";
        String tenant = "tenant112";
        String grayName = "grayName";
        String grayRule = "{\"type\":\"tag\",\"version\":\"1.0.0\",\"expr\":\"dgray123\",\"priority\":1}";
        String content = "mockContent11";
        
        String md5 = MD5Utils.md5Hex(content, "UTF-8");
        String groupKey = GroupKey2.getKey(dataId, group, tenant);
        String encryptedDataKey = "key12345";
        long ts = System.currentTimeMillis();
        //init gray cache
        boolean result = ConfigCacheService.dumpGray(dataId, group, tenant, grayName, grayRule, content, ts,
                encryptedDataKey);
        assertTrue(result);
        CacheItem contentCache = ConfigCacheService.getContentCache(groupKey);
        assertEquals(md5, contentCache.getConfigCacheGray().get(grayName).getMd5());
        assertEquals(ts, contentCache.getConfigCacheGray().get(grayName).getLastModifiedTs());
        assertEquals(encryptedDataKey, contentCache.getConfigCacheGray().get(grayName).getEncryptedDataKey());
        Mockito.verify(configDiskService, times(1))
                .saveGrayToDisk(eq(dataId), eq(group), eq(tenant), eq(grayName), eq(content));
        
        //ts newer ,md5 update
        long tsNew = System.currentTimeMillis();
        String contentNew = content + tsNew;
        String md5New = MD5Utils.md5Hex(contentNew, "UTF-8");
        boolean resultNew = ConfigCacheService.dumpGray(dataId, group, tenant, grayName, grayRule, contentNew, tsNew,
                encryptedDataKey);
        assertTrue(resultNew);
        assertEquals(md5New, contentCache.getConfigCacheGray().get(grayName).getMd5());
        assertEquals(tsNew, contentCache.getConfigCacheGray().get(grayName).getLastModifiedTs());
        assertEquals(encryptedDataKey, contentCache.getConfigCacheGray().get(grayName).getEncryptedDataKey());
        Mockito.verify(configDiskService, times(1))
                .saveGrayToDisk(eq(dataId), eq(group), eq(tenant), eq(grayName), eq(contentNew));
        
        //ts old ,md5 update
        long tsOld = tsNew - 1;
        String contentWithOldTs = "contentWithOldTs" + tsOld;
        boolean resultOld = ConfigCacheService.dumpGray(dataId, group, tenant, grayName, grayRule, contentWithOldTs,
                tsOld, encryptedDataKey);
        assertTrue(resultOld);
        assertEquals(md5New, contentCache.getConfigCacheGray().get(grayName).getMd5());
        assertEquals(tsNew, contentCache.getConfigCacheGray().get(grayName).getLastModifiedTs());
        assertEquals(encryptedDataKey, contentCache.getConfigCacheGray().get(grayName).getEncryptedDataKey());
        Mockito.verify(configDiskService, times(0))
                .saveGrayToDisk(eq(dataId), eq(group), eq(tenant), eq(grayName), eq(contentWithOldTs));
        
        //ts new ,md5 not update,grayRule changes
        long tsNew2 = tsNew + 1;
        String grayRuleNew = "{\"type\":\"tag\",\"version\":\"1.0.0\",\"expr\":\"gray1234\",\"priority\":1}";
        
        String contentWithPrev = contentNew;
        boolean resultNew2 = ConfigCacheService.dumpGray(dataId, group, tenant, grayName, grayRuleNew, contentWithPrev,
                tsNew2, encryptedDataKey);
        assertTrue(resultNew2);
        assertEquals(md5New, contentCache.getConfigCacheGray().get(grayName).getMd5());
        assertEquals(tsNew2, contentCache.getConfigCacheGray().get(grayName).getLastModifiedTs());
        assertEquals(encryptedDataKey, contentCache.getConfigCacheGray().get(grayName).getEncryptedDataKey());
        assertEquals(GrayRuleManager.constructGrayRule(GrayRuleManager.deserializeConfigGrayPersistInfo(grayRuleNew)),
                contentCache.getConfigCacheGray().get(grayName).getGrayRule());
        
        //ts new only,md5 not update,beta ips not change
        long tsNew3 = tsNew2 + 1;
        String contentWithPrev2 = contentNew;
        String grayRulePrev = grayRuleNew;
        boolean resultNew3 = ConfigCacheService.dumpGray(dataId, group, tenant, grayName, grayRulePrev,
                contentWithPrev2, tsNew3, encryptedDataKey);
        assertTrue(resultNew3);
        assertEquals(md5New, contentCache.getConfigCacheGray().get(grayName).getMd5());
        assertEquals(tsNew3, contentCache.getConfigCacheGray().get(grayName).getLastModifiedTs());
        assertEquals(encryptedDataKey, contentCache.getConfigCacheGray().get(grayName).getEncryptedDataKey());
        assertEquals(GrayRuleManager.constructGrayRule(GrayRuleManager.deserializeConfigGrayPersistInfo(grayRuleNew)),
                contentCache.getConfigCacheGray().get(grayName).getGrayRule());
        
        //ts not update,md5 not update,beta ips not change
        long tsNew4 = tsNew3;
        String contentWithPrev4 = contentNew;
        boolean resultNew4 = ConfigCacheService.dumpGray(dataId, group, tenant, grayName, grayRulePrev,
                contentWithPrev4, tsNew4, encryptedDataKey);
        assertTrue(resultNew4);
        assertEquals(md5New, contentCache.getConfigCacheGray().get(grayName).getMd5());
        assertEquals(tsNew3, contentCache.getConfigCacheGray().get(grayName).getLastModifiedTs());
        assertEquals(encryptedDataKey, contentCache.getConfigCacheGray().get(grayName).getEncryptedDataKey());
        assertEquals(GrayRuleManager.constructGrayRule(GrayRuleManager.deserializeConfigGrayPersistInfo(grayRuleNew)),
                contentCache.getConfigCacheGray().get(grayName).getGrayRule());
        
        //test remove
        boolean removeBeta = ConfigCacheService.removeGray(dataId, group, tenant, grayName);
        assertTrue(removeBeta);
        Mockito.verify(configDiskService, times(1)).removeConfigInfo4Gray(dataId, group, tenant, grayName);
        Map<String, ConfigCacheGray> grayCacheAfterRemove = ConfigCacheService.getContentCache(groupKey)
                .getConfigCacheGray();
        assertNull(grayCacheAfterRemove);
    }
    
    @Test
    void testTryConfigReadLock() throws Exception {
        String dataId = "123testTryConfigReadLock";
        String group = "1234";
        String tenant = "1234";
        CacheItem cacheItem = Mockito.mock(CacheItem.class);
        SimpleReadWriteLock lock = Mockito.mock(SimpleReadWriteLock.class);
        Mockito.when(cacheItem.getRwLock()).thenReturn(lock);
        String groupKey = GroupKey2.getKey(dataId, group, tenant);
        Field cache1 = ConfigCacheService.class.getDeclaredField("CACHE");
        cache1.setAccessible(true);
        ConcurrentHashMap<String, CacheItem> cache = (ConcurrentHashMap<String, CacheItem>) cache1.get(null);
        cache.put(groupKey, cacheItem);
        
        // lock ==0,not exist
        int readLock = ConfigCacheService.tryConfigReadLock(groupKey + "3245");
        assertEquals(0, readLock);
        
        //lock == 1 , success get lock
        Mockito.when(lock.tryReadLock()).thenReturn(true);
        int readLockSuccess = ConfigCacheService.tryConfigReadLock(groupKey);
        assertEquals(1, readLockSuccess);
        
        //lock ==-1 fail after spin all times;
        OngoingStubbing<Boolean> when = Mockito.when(lock.tryReadLock());
        for (int i = 0; i < 10; i++) {
            when = when.thenReturn(false);
        }
        int readLockFail = ConfigCacheService.tryConfigReadLock(groupKey);
        assertEquals(-1, readLockFail);
        
        //lock ==1 success after serval spin  times;
        OngoingStubbing<Boolean> when2 = Mockito.when(lock.tryReadLock());
        for (int i = 0; i < 5; i++) {
            when2 = when2.thenReturn(false);
        }
        when2.thenReturn(true);
        int readLockSuccessAfterRetry = ConfigCacheService.tryConfigReadLock(groupKey);
        assertEquals(1, readLockSuccessAfterRetry);
    }
}
