/*
 * 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.test.config;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.AbstractListener;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.config.http.HttpAgent;
import com.alibaba.nacos.client.config.http.MetricsHttpAgent;
import com.alibaba.nacos.client.config.http.ServerHttpAgent;
import com.alibaba.nacos.common.http.HttpRestResult;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.common.utils.ThreadUtils;
import com.alibaba.nacos.plugin.encryption.EncryptionPluginManager;
import com.alibaba.nacos.plugin.encryption.spi.EncryptionPluginService;
import org.apache.commons.codec.binary.Base64;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.web.server.LocalServerPort;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
abstract class AbstractConfigAPIConfigITCase {
    
    public static final long TIME_OUT = 5000;
    
    private static final String CONFIG_CONTROLLER_PATH = "/v1/cs/configs";
    
    private static final String SPECIAL_CHARACTERS = "!@#$%^&*()_+-=_|/'?.";
    
    private static ConfigService iconfig = null;
    
    private static HttpAgent agent = null;
    
    private static String dataId = "yanlin";
    
    private static String group = "yanlin";
    
    @Value("${server.servlet.context-path}")
    private String contextPath;
    
    @LocalServerPort
    private int port;
    
    private EncryptionPluginService mockEncryptionPluginService;
    
    @BeforeEach
    public void initEncryptionPluginService() {
        mockEncryptionPluginService = new EncryptionPluginService() {
            
            private static final String ALGORITHM = "AES";
            
            private static final String AES_PKCS5P = "AES/ECB/PKCS5Padding";
            
            // 随机生成密钥-用来加密数据内容
            private final String contentKey = generateKey();
            
            // 随机生成密钥-用来加密密钥
            private final String theKeyOfContentKey = generateKey();
            
            private String generateKey() {
                SecureRandom secureRandom = new SecureRandom();
                KeyGenerator keyGenerator;
                try {
                    keyGenerator = KeyGenerator.getInstance(ALGORITHM);
                } catch (NoSuchAlgorithmException e) {
                    throw new RuntimeException(e);
                }
                keyGenerator.init(128, secureRandom);
                SecretKey secretKey = keyGenerator.generateKey();
                byte[] keyBytes = secretKey.getEncoded();
                return Base64.encodeBase64String(keyBytes);
            }
            
            @Override
            public String encrypt(String secretKey, String content) {
                return Base64.encodeBase64String(aes(Cipher.ENCRYPT_MODE, content, secretKey));
            }
            
            @Override
            public String decrypt(String secretKey, String content) {
                if (StringUtils.isBlank(secretKey)) {
                    return null;
                }
                return aesDecrypt(content, secretKey);
            }
            
            @Override
            public String generateSecretKey() {
                return contentKey;
            }
            
            @Override
            public String algorithmName() {
                return ALGORITHM.toLowerCase();
            }
            
            @Override
            public String encryptSecretKey(String secretKey) {
                return Base64.encodeBase64String(aes(Cipher.ENCRYPT_MODE, generateSecretKey(), theKeyOfContentKey));
            }
            
            @Override
            public String decryptSecretKey(String secretKey) {
                if (StringUtils.isBlank(secretKey)) {
                    return null;
                }
                return aesDecrypt(secretKey, theKeyOfContentKey);
            }
            
            private byte[] aes(int mode, String content, String key) {
                try {
                    return aesBytes(mode, content.getBytes(StandardCharsets.UTF_8), key);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            
            private byte[] aesBytes(int mode, byte[] content, String key) {
                SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
                Cipher cipher = null;
                try {
                    cipher = Cipher.getInstance(AES_PKCS5P);
                    cipher.init(mode, keySpec);
                    return cipher.doFinal(content);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            
            private String aesDecrypt(String content, String key) {
                byte[] bytes = aesBytes(Cipher.DECRYPT_MODE, Base64.decodeBase64(content), key);
                return new String(bytes, StandardCharsets.UTF_8);
            }
        };
        EncryptionPluginManager.join(mockEncryptionPluginService);
    }
    
    @BeforeEach
    public void setUp() throws Exception {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1" + ":" + port);
        properties.put(PropertyKeyConst.CONTEXT_PATH, contextPath);
        if (null == iconfig) {
            iconfig = NacosFactory.createConfigService(properties);
        }
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
    }
    
    @AfterEach
    public void cleanup() throws Exception {
        HttpRestResult<String> result = null;
        try {
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("beta", "true");
            result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(), TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertTrue(JacksonUtils.toObj(result.getData()).get("data").booleanValue());
            assertTrue(JacksonUtils.toObj(result.getData()).get("data").booleanValue());
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }
    
    /**
     * Retrieve data successfully.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 3 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void getConfig() throws Exception {
        final String content = "test";
        boolean result = iconfig.publishConfig(dataId, group, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        String value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertEquals(content, value);
        result = iconfig.removeConfig(dataId, group);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        value = iconfig.getConfig(dataId, group, TIME_OUT);
        System.out.println(value);
        assertNull(value);
    }
    
    /**
     * Publish and retrieve encrypted data successfully.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 3 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void publishAndGetConfig() throws Exception {
        String dataId = "cipher-aes-dataId";
        final String content = "test";
        boolean result = iconfig.publishConfig(dataId, group, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        String value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertEquals(content, value);
        result = iconfig.removeConfig(dataId, group);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertNull(value);
    }
    
    /**
     * Test retrieving configuration when server has no config.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testGetConfigWhenServerHasNoConfig() throws Exception {
        String content = iconfig.getConfig(dataId, "nacos", TIME_OUT);
        assertNull(content);
    }
    
    /**
     * Test fetching config when dataId is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testGetConfigWithNullDataId() throws Exception {
        try {
            String content = iconfig.getConfig(null, group, TIME_OUT);
        } catch (Exception e) {
            assertTrue(true);
            return;
        }
        fail();
    }
    
    /**
     * Test fetching config when group is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testGetConfigWithNullGroup() throws Exception {
        final String dataId = "nacos_getconfig_4";
        final String content = "test";
        boolean result = iconfig.publishConfig(dataId, null, content);
        assertTrue(result);
        Thread.sleep(TIME_OUT);
        
        String value = iconfig.getConfig(dataId, null, TIME_OUT);
        assertEquals(content, value);
        
        result = iconfig.removeConfig(dataId, null);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
    }
    
    /**
     * Test publishing config to Nacos when the server does not have the config.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testPublishConfigWhenServerDoesNotExist() throws Exception {
        final String content = "publishConfigTest";
        boolean result = iconfig.publishConfig(dataId, group, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        result = iconfig.removeConfig(dataId, group);
        assertTrue(result);
    }
    
    /**
     * Test updating config in Nacos when the server already has the config.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testUpdateConfigWhenServerHasExistingConfig() throws Exception {
        final String content = "publishConfigTest";
        boolean result = iconfig.publishConfig(dataId, group, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        
        final String content1 = "test.abc";
        result = iconfig.publishConfig(dataId, group, content1);
        Thread.sleep(TIME_OUT);
        String value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertEquals(content1, value);
    }
    
    /**
     * Test publishing config to Nacos with special characters.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testPublishConfigWithSpecialCharacters() throws Exception {
        String content = "test" + SPECIAL_CHARACTERS;
        boolean result = iconfig.publishConfig(dataId, group, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        
        String value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertEquals(content, value);
    }
    
    /**
     * Test publishing config to Nacos when dataId is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testPublishConfigWithNullDataId() throws Exception {
        try {
            String content = "test";
            boolean result = iconfig.publishConfig(null, group, content);
            Thread.sleep(TIME_OUT);
            assertTrue(result);
        } catch (Exception e) {
            assertTrue(true);
            return;
        }
        fail();
    }
    
    /**
     * Test publishing config to Nacos when group is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testPublishConfigWithNullGroup() throws Exception {
        final String dataId = "nacos_publishConfig_5";
        String content = "test";
        boolean result = iconfig.publishConfig(dataId, null, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        
        String value = iconfig.getConfig(dataId, null, TIME_OUT);
        assertEquals(content, value);
    }
    
    
    /**
     * Test publishing config to Nacos when content is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testPublishConfigWithNullContent() throws Exception {
        String content = null;
        try {
            boolean result = iconfig.publishConfig(dataId, group, content);
            Thread.sleep(TIME_OUT);
        } catch (Exception e) {
            assertTrue(true);
            return;
        }
        fail();
    }
    
    /**
     * Test publishing configuration to Nacos with content containing Chinese characters.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testPublishConfigWithChineseCharacters() throws Exception {
        String content = "阿里abc";
        boolean result = iconfig.publishConfig(dataId, group, content);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        String value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertEquals(content, value);
    }
    
    /**
     * Test removing config from Nacos when the server has the config.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    public void testRemoveConfigWhenServerHasConfig() throws Exception {
        String content = "test";
        boolean result = iconfig.publishConfig(dataId, group, content);
        
        assertTrue(result);
        Thread.sleep(TIME_OUT);
        
        result = iconfig.removeConfig(dataId, group);
        assertTrue(result);
        Thread.sleep(TIME_OUT);
        String value = iconfig.getConfig(dataId, group, TIME_OUT);
        assertNull(value);
    }
    
    /**
     * Test removing config from Nacos when the server does not have the config.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveConfigWhenServerDoesNotHaveConfig() throws Exception {
        group += "removeConfig2";
        boolean result = iconfig.removeConfig(dataId, group);
        assertTrue(result);
    }
    
    /**
     * Test removing configuration from Nacos when dataId is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveConfigWithNullDataId() throws Exception {
        try {
            boolean result = iconfig.removeConfig(null, group);
            assertTrue(result);
        } catch (Exception e) {
            assertTrue(true);
            return;
        }
        fail();
    }
    
    /**
     * Test removing configuration from Nacos when group is null.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveConfigWithNullGroup() throws Exception {
        boolean result = iconfig.removeConfig(dataId, null);
        assertTrue(result);
    }
    
    /**
     * Test adding listener for dataId in Nacos, retrieving modified configuration after server-side modification.
     *
     * @throws Exception if an error occurs during the test.
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testAddListenerAndRetrieveModifiedConfig() throws Exception {
        final AtomicInteger count = new AtomicInteger(0);
        final String content = "test-abc";
        boolean result = iconfig.publishConfig(dataId, group, content);
        assertTrue(result);
        
        Listener ml = new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("receive23:" + configInfo);
                count.incrementAndGet();
                assertEquals(content, configInfo);
            }
            
            @Override
            public Executor getExecutor() {
                return null;
            }
        };
        iconfig.addListener(dataId, group, ml);
        while (count.get() == 0) {
            Thread.sleep(2000);
        }
        assertTrue(count.get() >= 1);
        iconfig.removeListener(dataId, group, ml);
    }
    
    /**
     * Verify that setting a listener to null throws an IllegalArgumentException.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testAddNullListenerThrowsException() throws Exception {
        assertThrows(IllegalArgumentException.class, () -> {
            iconfig.addListener(dataId, group, null);
        });
    }
    
    
    /**
     * Verify adding a listener for dataId, modifying the server configuration, and ensuring the listener is triggered
     * exactly once.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = Constants.CONFIG_LONG_POLL_TIMEOUT << 2, unit = TimeUnit.MILLISECONDS)
    public void testAddListenerAndModifyConfig() throws InterruptedException, NacosException {
        final AtomicInteger count = new AtomicInteger(0);
        final String dataId = "nacos_addListener_3";
        final String group = "nacos_addListener_3";
        final String content = "test-abc-" + System.currentTimeMillis();
        final String newContent = "nacos_addListener_3-" + System.currentTimeMillis();
        boolean result = iconfig.publishConfig(dataId, group, content);
        assertTrue(result);
        
        // Maximum assurance level notification has been performed
        ThreadUtils.sleep(5000);
        
        Listener ml = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                count.incrementAndGet();
                assertEquals(newContent, configInfo);
            }
        };
        String receive = iconfig.getConfigAndSignListener(dataId, group, 5000L, ml);
        assertEquals(content, receive);
        result = iconfig.publishConfig(dataId, group, newContent);
        assertTrue(result);
        // Get enough sleep to ensure that the monitor is triggered only once
        // during the two long training sessions
        ThreadUtils.sleep(Constants.CONFIG_LONG_POLL_TIMEOUT << 1);
        assertEquals(1, count.get());
        iconfig.removeListener(dataId, group, ml);
    }
    
    /**
     * Verify that setting a null listener in Nacos throws an IllegalArgumentException.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testAddNullListener() throws Exception {
        final AtomicInteger count = new AtomicInteger(0);
        
        iconfig.removeConfig(dataId, group);
        Thread.sleep(TIME_OUT);
        
        Listener ml = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                count.incrementAndGet();
            }
        };
        iconfig.addListener(dataId, group, ml);
        Thread.sleep(TIME_OUT);
        String content = "test-abc";
        boolean result = iconfig.publishConfig(dataId, group, content);
        assertTrue(result);
        
        while (count.get() == 0) {
            Thread.sleep(3000);
        }
        assertEquals(1, count.get());
        iconfig.removeListener(dataId, group, ml);
    }
    
    /**
     * Subscribe to dataId in Nacos, update server-side configuration, and verify that the listener receives the update
     * exactly once.
     *
     * @author chuntaojun
     * @since 3.6.8
     */
    @Test
    @Timeout(value = Constants.CONFIG_LONG_POLL_TIMEOUT << 2, unit = TimeUnit.MILLISECONDS)
    public void testAddListenerAndUpdateConfig() throws InterruptedException, NacosException {
        final AtomicInteger count = new AtomicInteger(0);
        final String dataId = "nacos_addListener_5";
        final String group = "nacos_addListener_5";
        final String content = "test-abc";
        final String newContent = "new-test-def";
        boolean result = iconfig.publishConfig(dataId, group, content);
        assertTrue(result);
        
        Thread.sleep(2000);
        
        Listener ml = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                count.incrementAndGet();
                assertEquals(newContent, configInfo);
            }
        };
        
        String receiveContent = iconfig.getConfigAndSignListener(dataId, group, 1000, ml);
        System.out.println(receiveContent);
        
        result = iconfig.publishConfig(dataId, group, newContent);
        assertTrue(result);
        
        assertEquals(content, receiveContent);
        Thread.sleep(2000);
        
        assertEquals(1, count.get());
        iconfig.removeListener(dataId, group, ml);
    }
    
    /**
     * Verify that the listener is triggered only after the configuration is updated.
     *
     * @author chuntaojun
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testListenerTriggeredAfterConfigUpdate() throws Exception {
        
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1" + ":" + port);
        properties.put(PropertyKeyConst.ENABLE_REMOTE_SYNC_CONFIG, "true");
        properties.put(PropertyKeyConst.CONTEXT_PATH, contextPath);
        ConfigService iconfig = NacosFactory.createConfigService(properties);
        
        final AtomicInteger count = new AtomicInteger(0);
        final String dataId = "nacos_addListener_6";
        final String group = "nacos_addListener_6";
        final String content = "test-abc";
        final String newContent = "new-test-def";
        boolean result = iconfig.publishConfig(dataId, group, content);
        assertTrue(result);
        
        Thread.sleep(2000);
        
        Listener ml = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                count.incrementAndGet();
                System.out.println("Listener receive : [" + configInfo + "]");
                assertEquals(newContent, configInfo);
            }
        };
        
        iconfig.addListener(dataId, group, ml);
        
        String receiveContent = iconfig.getConfig(dataId, group, 1000);
        
        System.out.println(receiveContent);
        
        result = iconfig.publishConfig(dataId, group, newContent);
        assertTrue(result);
        
        Thread.sleep(2000);
        
        receiveContent = iconfig.getConfig(dataId, group, 1000);
        
        assertEquals(newContent, receiveContent);
        
        assertEquals(1, count.get());
        iconfig.removeListener(dataId, group, ml);
    }
    
    /**
     * Verify normal removal of listener in Nacos.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveListener() throws Exception {
        iconfig.addListener(dataId, group, new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                fail();
            }
        });
        Thread.sleep(TIME_OUT);
        try {
            iconfig.removeListener(dataId, group, new AbstractListener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    System.out.println("remove receive:" + configInfo);
                }
            });
        } catch (Exception e) {
            // ignore
        }
    }
    
    /**
     * Verify removing listener for dataId that does not exist in Nacos.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveListenerForNonexistentDataId() {
        group += "test.nacos";
        Assertions.assertDoesNotThrow(() -> {
            iconfig.removeListener(dataId, group, new AbstractListener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                
                }
            });
        });
    }
    
    /**
     * Verify removal of the last listener when multiple listeners exist in Nacos.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveLastListener() throws Exception {
        final String contentRemove = "test-abc-two";
        final AtomicInteger count = new AtomicInteger(0);
        
        Listener ml = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                count.incrementAndGet();
            }
        };
        Listener ml1 = new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                //System.out.println("ml1 remove listener receive:" + configInfo);
                count.incrementAndGet();
                assertEquals(contentRemove, configInfo);
            }
        };
        iconfig.addListener(dataId, group, ml);
        iconfig.addListener(dataId, group, ml1);
        
        iconfig.removeListener(dataId, group, ml);
        Thread.sleep(TIME_OUT);
        
        boolean result = iconfig.publishConfig(dataId, group, contentRemove);
        Thread.sleep(TIME_OUT);
        assertTrue(result);
        
        while (count.get() == 0) {
            Thread.sleep(3000);
        }
        assertNotEquals(0, count.get());
    }
    
    /**
     * Verify exception thrown when listener is null during removal in Nacos.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testRemoveListenerNull() {
        assertThrows(IllegalArgumentException.class, () -> {
            iconfig.removeListener(dataId, group, null);
        });
    }
    
    /**
     * Verify detailed configuration retrieval using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 3 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiDetailConfig() {
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test";
            boolean ret = iconfig.publishConfig(dataId, group, content);
            Thread.sleep(TIME_OUT);
            assertTrue(ret);
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("show", "all");
            HttpRestResult<String> result = agent.httpGet(CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(),
                    TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            
            assertEquals(content, JacksonUtils.toObj(result.getData()).get("content").textValue());
        });
    }
    
    /**
     * Verify catalog information retrieval using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 3 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiCatalog() {
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test";
            boolean ret = iconfig.publishConfig(dataId, group, content);
            Thread.sleep(TIME_OUT);
            assertTrue(ret);
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            HttpRestResult<String> result = agent.httpGet(CONFIG_CONTROLLER_PATH + "/catalog", null, params,
                    agent.getEncode(), TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            
            System.out.println(result.getData());
            assertFalse(JacksonUtils.toObj(result.getData()).get("data").isNull());
            
        });
    }
    
    /**
     * Verify beta configuration query using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 3 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiQueryBeta() {
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test-beta";
            Map<String, String> headers = new HashMap<>();
            headers.put("betaIps", "127.0.0.1");
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("content", content);
            HttpRestResult<String> result = agent.httpPost(CONFIG_CONTROLLER_PATH + "/", headers, params,
                    agent.getEncode(), TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertEquals("true", result.getData());
            params.clear();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("beta", "true");
            result = agent.httpGet(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(), TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertEquals(content, JacksonUtils.toObj(result.getData()).get("data").get("content").textValue());
            // delete data
            result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(), TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
        });
    }
    
    /**
     * Verify beta configuration deletion using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 3 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiQueryBetaDelete() {
        HttpRestResult<String> result = null;
        
        try {
            final String content = "test-beta";
            Map<String, String> headers = new HashMap<>();
            headers.put("betaIps", "127.0.0.1");
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("content", content);
            result = agent.httpPost(CONFIG_CONTROLLER_PATH + "/", headers, params, agent.getEncode(), TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertEquals("true", result.getData());
            
            params.clear();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("beta", "true");
            result = agent.httpDelete(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(), TIME_OUT);
            
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertTrue(JacksonUtils.toObj(result.getData()).get("data").booleanValue());
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }
    
    /**
     * Verify fuzzy search of configuration information using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiFuzzySearchConfig() {
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test123";
            boolean ret = iconfig.publishConfig(dataId, group, content);
            Thread.sleep(TIME_OUT);
            assertTrue(ret);
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("pageNo", "1");
            params.put("pageSize", "10");
            params.put("search", "blur");
            HttpRestResult<String> result = agent.httpGet(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(),
                    TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            
            assertTrue(JacksonUtils.toObj(result.getData()).get("totalCount").intValue() >= 1);
            assertTrue(JacksonUtils.toObj(result.getData()).get("pageItems").get(0).get("content").textValue()
                    .startsWith(content));
        });
    }
    
    /**
     * Verify fuzzy search of configuration information using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiFuzzySearchConfig1() {
        
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test123";
            boolean ret = iconfig.publishConfig(dataId, group, content);
            Thread.sleep(TIME_OUT);
            assertTrue(ret);
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId + "*");
            params.put("group", group + "*");
            params.put("pageNo", "1");
            params.put("pageSize", "10");
            params.put("search", "blur");
            HttpRestResult<String> result = agent.httpGet(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(),
                    TIME_OUT);
            
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertTrue(JacksonUtils.toObj(result.getData()).get("totalCount").intValue() >= 1);
            assertEquals(content,
                    JacksonUtils.toObj(result.getData()).get("pageItems").get(0).get("content").textValue());
            
        });
    }
    
    /**
     * Verify accurate search of configuration information using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiSearchConfig() {
        
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test123";
            boolean ret = iconfig.publishConfig(dataId, group, content);
            assertTrue(ret);
            Thread.sleep(TIME_OUT);
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("pageNo", "1");
            params.put("pageSize", "10");
            params.put("search", "accurate");
            HttpRestResult<String> result = agent.httpGet(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(),
                    TIME_OUT);
            
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertEquals(1, JacksonUtils.toObj(result.getData()).get("totalCount").intValue());
            assertEquals(content,
                    JacksonUtils.toObj(result.getData()).get("pageItems").get(0).get("content").textValue());
            
        });
    }
    
    /**
     * Verify search of configuration information including Chinese characters using Nacos Open API.
     *
     * @author xiaochun.xxc
     * @since 3.6.8
     */
    @Test
    @Timeout(value = 5 * TIME_OUT, unit = TimeUnit.MILLISECONDS)
    public void testOpenApiSearchConfigChinese() {
        
        Assertions.assertDoesNotThrow(() -> {
            final String content = "test测试";
            boolean ret = iconfig.publishConfig(dataId, group, content);
            assertTrue(ret);
            Thread.sleep(TIME_OUT);
            
            Map<String, String> params = new HashMap<>();
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("pageNo", "1");
            params.put("pageSize", "10");
            params.put("search", "accurate");
            HttpRestResult<String> result = agent.httpGet(CONFIG_CONTROLLER_PATH + "/", null, params, agent.getEncode(),
                    TIME_OUT);
            assertEquals(HttpURLConnection.HTTP_OK, result.getCode());
            assertEquals(1, JacksonUtils.toObj(result.getData()).get("totalCount").intValue());
            assertEquals(content,
                    JacksonUtils.toObj(result.getData()).get("pageItems").get(0).get("content").textValue());
        });
    }
    
}
