/*
 * Copyright 1999-2022 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.controller.v2;

import com.alibaba.nacos.api.model.Page;
import com.alibaba.nacos.api.model.v2.ErrorCode;
import com.alibaba.nacos.api.model.v2.Result;
import com.alibaba.nacos.auth.config.NacosAuthConfig;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.config.server.constant.Constants;
import com.alibaba.nacos.config.server.controller.ConfigServletInner;
import com.alibaba.nacos.config.server.enums.ApiVersionEnum;
import com.alibaba.nacos.config.server.model.ConfigInfo;
import com.alibaba.nacos.config.server.model.ConfigRequestInfo;
import com.alibaba.nacos.config.server.model.form.ConfigForm;
import com.alibaba.nacos.config.server.service.ConfigDetailService;
import com.alibaba.nacos.config.server.service.ConfigOperationService;
import com.alibaba.nacos.config.server.service.repository.ConfigInfoPersistService;
import com.alibaba.nacos.core.auth.AuthFilter;
import com.alibaba.nacos.core.code.ControllerMethodsCache;
import com.alibaba.nacos.sys.env.EnvUtil;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
// todo remove this
@MockitoSettings(strictness = Strictness.LENIENT)
class ConfigControllerV2Test {
    
    private static final String TEST_DATA_ID = "test";
    
    private static final String TEST_GROUP = "test";
    
    private static final String TEST_NAMESPACE_ID = "";
    
    private static final String TEST_NAMESPACE_ID_PUBLIC = "public";
    
    private static final String TEST_TAG = "";
    
    private static final String TEST_CONTENT = "test config";
    
    private static final String TEST_ENCRYPTED_DATA_KEY = "test_encrypted_data_key";
    
    @InjectMocks
    private AuthFilter authFilter;
    
    @Mock
    private NacosAuthConfig authConfig;
    
    @Mock
    private ControllerMethodsCache controllerMethodsCache;
    
    private ConfigControllerV2 configControllerV2;
    
    private MockMvc mockmvc;
    
    @Mock
    private ConfigServletInner inner;
    
    @Mock
    private ConfigOperationService configOperationService;
    
    @Mock
    private ServletContext servletContext;
    
    @Mock
    private ConfigInfoPersistService configInfoPersistService;
    
    private ConfigDetailService configDetailService;
    
    @BeforeEach
    void setUp() {
        EnvUtil.setEnvironment(new StandardEnvironment());
        configDetailService = new ConfigDetailService(configInfoPersistService);
        configControllerV2 = new ConfigControllerV2(inner, configOperationService, configDetailService);
        mockmvc = MockMvcBuilders.standaloneSetup(configControllerV2).addFilter(authFilter).build();
        when(authConfig.isAuthEnabled()).thenReturn(false);
    }
    
    @Test
    void testGetConfig() throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        Result<String> stringResult = Result.success(TEST_CONTENT);
        
        doAnswer(x -> {
            x.getArgument(1, HttpServletResponse.class).setStatus(200);
            x.getArgument(1, HttpServletResponse.class)
                    .setContentType(com.alibaba.nacos.common.http.param.MediaType.APPLICATION_JSON);
            x.getArgument(1, HttpServletResponse.class).getWriter().print(JacksonUtils.toJson(stringResult));
            return null;
        }).when(inner).doGetConfig(any(HttpServletRequest.class), any(HttpServletResponse.class), eq(TEST_DATA_ID),
                eq(TEST_GROUP), eq(TEST_NAMESPACE_ID_PUBLIC), eq(TEST_TAG), eq(null), anyString(),
                eq(ApiVersionEnum.V2));
        
        configControllerV2.getConfig(request, response, TEST_DATA_ID, TEST_GROUP, TEST_NAMESPACE_ID, TEST_TAG);
        
        verify(inner).doGetConfig(eq(request), eq(response), eq(TEST_DATA_ID), eq(TEST_GROUP),
                eq(TEST_NAMESPACE_ID_PUBLIC), eq(TEST_TAG), eq(null), anyString(), eq(ApiVersionEnum.V2));
        JsonNode resNode = JacksonUtils.toObj(response.getContentAsString());
        Integer errCode = JacksonUtils.toObj(resNode.get("code").toString(), Integer.class);
        String actContent = JacksonUtils.toObj(resNode.get("data").toString(), String.class);
        assertEquals(200, response.getStatus());
        assertEquals(ErrorCode.SUCCESS.getCode(), errCode);
        assertEquals(TEST_CONTENT, actContent);
    }
    
    @Test
    void testPublishConfig() throws Exception {
        
        ConfigForm configForm = new ConfigForm();
        configForm.setDataId(TEST_DATA_ID);
        configForm.setGroup(TEST_GROUP);
        configForm.setNamespaceId(TEST_NAMESPACE_ID);
        configForm.setContent(TEST_CONTENT);
        MockHttpServletRequest request = new MockHttpServletRequest();
        
        when(configOperationService.publishConfig(any(ConfigForm.class), any(ConfigRequestInfo.class),
                anyString())).thenReturn(true);
        
        Result<Boolean> booleanResult = configControllerV2.publishConfig(configForm, request);
        
        verify(configOperationService).publishConfig(any(ConfigForm.class), any(ConfigRequestInfo.class), anyString());
        
        assertEquals(ErrorCode.SUCCESS.getCode(), booleanResult.getCode());
        assertTrue(booleanResult.getData());
    }
    
    @Test
    void testPublishConfigWithEncryptedDataKey() throws Exception {
        
        ConfigForm configForm = new ConfigForm();
        configForm.setDataId(TEST_DATA_ID);
        configForm.setGroup(TEST_GROUP);
        configForm.setNamespaceId(TEST_NAMESPACE_ID);
        configForm.setContent(TEST_CONTENT);
        configForm.setEncryptedDataKey(TEST_ENCRYPTED_DATA_KEY);
        MockHttpServletRequest request = new MockHttpServletRequest();
        
        when(configOperationService.publishConfig(any(ConfigForm.class), any(ConfigRequestInfo.class),
                eq(TEST_ENCRYPTED_DATA_KEY))).thenReturn(true);
        
        Result<Boolean> booleanResult = configControllerV2.publishConfig(configForm, request);
        
        verify(configOperationService).publishConfig(any(ConfigForm.class), any(ConfigRequestInfo.class), anyString());
        
        assertEquals(ErrorCode.SUCCESS.getCode(), booleanResult.getCode());
        assertTrue(booleanResult.getData());
    }
    
    @Test
    void testPublishConfigWhenNameSpaceIsPublic() throws Exception {
        
        ConfigForm configForm = new ConfigForm();
        configForm.setDataId(TEST_DATA_ID);
        configForm.setGroup(TEST_GROUP);
        configForm.setNamespaceId(TEST_NAMESPACE_ID_PUBLIC);
        configForm.setContent(TEST_CONTENT);
        MockHttpServletRequest request = new MockHttpServletRequest();
        Answer<Boolean> answer = invocationOnMock -> {
            if (invocationOnMock.getArgument(0, ConfigForm.class).getNamespaceId().equals(TEST_NAMESPACE_ID_PUBLIC)) {
                return true;
            }
            return false;
        };
        when(configOperationService.publishConfig(any(ConfigForm.class), any(ConfigRequestInfo.class),
                anyString())).thenAnswer(answer);
        
        Result<Boolean> booleanResult = configControllerV2.publishConfig(configForm, request);
        
        verify(configOperationService).publishConfig(any(ConfigForm.class), any(ConfigRequestInfo.class), anyString());
        
        assertEquals(ErrorCode.SUCCESS.getCode(), booleanResult.getCode());
        assertTrue(booleanResult.getData());
    }
    
    @Test
    void testDeleteConfigWhenNameSpaceIsPublic() throws Exception {
        
        MockHttpServletRequest request = new MockHttpServletRequest();
        
        when(configOperationService.deleteConfig(eq(TEST_DATA_ID), eq(TEST_GROUP), eq(TEST_NAMESPACE_ID_PUBLIC),
                eq(TEST_TAG), any(), any(), any())).thenReturn(true);
        Result<Boolean> booleanResult = configControllerV2.deleteConfig(request, TEST_DATA_ID, TEST_GROUP,
                TEST_NAMESPACE_ID, TEST_TAG);
        
        verify(configOperationService).deleteConfig(eq(TEST_DATA_ID), eq(TEST_GROUP), eq(TEST_NAMESPACE_ID_PUBLIC),
                eq(TEST_TAG), any(), any(), eq("http"));
        
        assertEquals(ErrorCode.SUCCESS.getCode(), booleanResult.getCode());
        assertTrue(booleanResult.getData());
    }
    
    @Test
    void testDeleteConfig() throws Exception {
        
        MockHttpServletRequest request = new MockHttpServletRequest();
        
        when(configOperationService.deleteConfig(eq(TEST_DATA_ID), eq(TEST_GROUP), eq(TEST_NAMESPACE_ID_PUBLIC),
                eq(TEST_TAG), any(), any(), eq("http"))).thenReturn(true);
        
        Result<Boolean> booleanResult = configControllerV2.deleteConfig(request, TEST_DATA_ID, TEST_GROUP,
                TEST_NAMESPACE_ID, TEST_TAG);
        
        verify(configOperationService).deleteConfig(eq(TEST_DATA_ID), eq(TEST_GROUP), eq(TEST_NAMESPACE_ID_PUBLIC),
                eq(TEST_TAG), any(), any(), eq("http"));
        
        assertEquals(ErrorCode.SUCCESS.getCode(), booleanResult.getCode());
        assertTrue(booleanResult.getData());
    }
    
    @Test
    void testGetConfigByDetail() throws Exception {
        List<ConfigInfo> configInfoList = new ArrayList<>();
        ConfigInfo configInfo = new ConfigInfo("test", "test", "test");
        configInfoList.add(configInfo);
        
        Page<ConfigInfo> page = new Page<>();
        page.setTotalCount(15);
        page.setPageNumber(1);
        page.setPagesAvailable(2);
        page.setPageItems(configInfoList);
        Map<String, Object> configAdvanceInfo = new HashMap<>(8);
        configAdvanceInfo.put("content", "server.port");
        
        when(configInfoPersistService.findConfigInfo4Page(1, 10, "test", "test", "public",
                configAdvanceInfo)).thenReturn(page);
        
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(
                        Constants.CONFIG_CONTROLLER_V2_PATH + "/searchDetail").param("search", "accurate")
                .param("dataId", "test").param("group", "test").param("appName", "").param("tenant", "")
                .param("config_tags", "").param("pageNo", "1").param("pageSize", "10")
                .param("config_detail", "server.port");
        MockHttpServletResponse response = mockmvc.perform(builder).andReturn().getResponse();
        String actualValue = response.getContentAsString();
        
        JsonNode pageItemsNode = JacksonUtils.toObj(actualValue).get("pageItems");
        List resultList = JacksonUtils.toObj(pageItemsNode.toString(), List.class);
        ConfigInfo resConfigInfo = JacksonUtils.toObj(pageItemsNode.get(0).toString(), ConfigInfo.class);
        
        assertEquals(configInfoList.size(), resultList.size());
        assertEquals(configInfo.getDataId(), resConfigInfo.getDataId());
        assertEquals(configInfo.getGroup(), resConfigInfo.getGroup());
        assertEquals(configInfo.getContent(), resConfigInfo.getContent());
    }
    
    @Test
    void testGetConfigFuzzyByDetail() throws Exception {
        List<ConfigInfo> configInfoList = new ArrayList<>();
        ConfigInfo configInfo = new ConfigInfo("test", "test", "test");
        configInfoList.add(configInfo);
        
        Page<ConfigInfo> page = new Page<>();
        page.setTotalCount(15);
        page.setPageNumber(1);
        page.setPagesAvailable(2);
        page.setPageItems(configInfoList);
        Map<String, Object> configAdvanceInfo = new HashMap<>(8);
        configAdvanceInfo.put("content", "server.port");
        
        when(configInfoPersistService.findConfigInfoLike4Page(1, 10, "test", "test", "public",
                configAdvanceInfo)).thenReturn(page);
        
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(
                        Constants.CONFIG_CONTROLLER_V2_PATH + "/searchDetail").param("search", "blur").param("dataId", "test")
                .param("group", "test").param("appName", "").param("tenant", "").param("config_tags", "")
                .param("pageNo", "1").param("pageSize", "10").param("config_detail", "server.port");
        MockHttpServletResponse response = mockmvc.perform(builder).andReturn().getResponse();
        String actualValue = response.getContentAsString();
        
        JsonNode pageItemsNode = JacksonUtils.toObj(actualValue).get("pageItems");
        List resultList = JacksonUtils.toObj(pageItemsNode.toString(), List.class);
        ConfigInfo resConfigInfo = JacksonUtils.toObj(pageItemsNode.get(0).toString(), ConfigInfo.class);
        
        assertEquals(configInfoList.size(), resultList.size());
        assertEquals(configInfo.getDataId(), resConfigInfo.getDataId());
        assertEquals(configInfo.getGroup(), resConfigInfo.getGroup());
        assertEquals(configInfo.getContent(), resConfigInfo.getContent());
    }
    
    @Test
    void testGetConfigAuthFilter() throws Exception {
        when(authConfig.isAuthEnabled()).thenReturn(true);
        Method method = Arrays.stream(ConfigControllerV2.class.getMethods())
                .filter(m -> m.getName().equals("searchConfigByDetails")).findFirst().get();
        when(controllerMethodsCache.getMethod(any(HttpServletRequest.class))).thenReturn(method);
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(
                        Constants.CONFIG_CONTROLLER_V2_PATH + "/searchDetail").param("search", "accurate")
                .param("dataId", "test").param("group", "test").param("appName", "").param("tenant", "")
                .param("config_tags", "").param("pageNo", "1").param("pageSize", "10")
                .param("config_detail", "server.port");
        MockHttpServletResponse response = mockmvc.perform(builder).andReturn().getResponse();
        
        assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
        assertEquals(response.getErrorMessage(),
                "Invalid server identity key or value, Please make sure set `nacos.core.auth.server.identity.key`"
                        + " and `nacos.core.auth.server.identity.value`, or open `nacos.core.auth.enable.userAgentAuthWhite`");
    }
}
