/*
 * 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.csp.sentinel.dashboard.controller.gateway;

import com.alibaba.csp.sentinel.dashboard.auth.AuthorizationInterceptor;
import com.alibaba.csp.sentinel.dashboard.auth.FakeAuthServiceImpl;
import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity;
import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement;
import com.alibaba.csp.sentinel.dashboard.discovery.SimpleMachineDiscovery;
import com.alibaba.csp.sentinel.dashboard.domain.Result;
import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo;
import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo;
import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo;
import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import java.util.*;
import java.util.concurrent.CompletableFuture;

import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;

/**
 * Test cases for {@link GatewayApiController}.
 *
 * @author cdfive
 */
@RunWith(SpringRunner.class)
@WebMvcTest(GatewayApiController.class)
@Import({FakeAuthServiceImpl.class, InMemApiDefinitionStore.class, AppManagement.class, SimpleMachineDiscovery.class, AuthorizationInterceptor.class})
public class GatewayApiControllerTest {

    private static final String TEST_APP = "test_app";

    private static final String TEST_IP = "localhost";

    private static final Integer TEST_PORT = 8719;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private InMemApiDefinitionStore repository;

    @MockBean
    private SentinelApiClient sentinelApiClient;

    @Before
    public void before() {
        repository.clearAll();
    }

    @Test
    public void testQueryApis() throws Exception {
        String path = "/gateway/api/list.json";

        List<ApiDefinitionEntity> entities = new ArrayList<>();

        // Mock two entities
        ApiDefinitionEntity entity = new ApiDefinitionEntity();
        entity.setId(1L);
        entity.setApp(TEST_APP);
        entity.setIp(TEST_IP);
        entity.setPort(TEST_PORT);
        entity.setApiName("foo");
        Date date = new Date();
        entity.setGmtCreate(date);
        entity.setGmtModified(date);

        Set<ApiPredicateItemEntity> itemEntities = new LinkedHashSet<>();
        entity.setPredicateItems(itemEntities);
        ApiPredicateItemEntity itemEntity = new ApiPredicateItemEntity();
        itemEntity.setPattern("/aaa");
        itemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT);

        itemEntities.add(itemEntity);
        entities.add(entity);

        ApiDefinitionEntity entity2 = new ApiDefinitionEntity();
        entity2.setId(2L);
        entity2.setApp(TEST_APP);
        entity2.setIp(TEST_IP);
        entity2.setPort(TEST_PORT);
        entity2.setApiName("biz");
        entity.setGmtCreate(date);
        entity.setGmtModified(date);

        Set<ApiPredicateItemEntity> itemEntities2 = new LinkedHashSet<>();
        entity2.setPredicateItems(itemEntities2);
        ApiPredicateItemEntity itemEntity2 = new ApiPredicateItemEntity();
        itemEntity2.setPattern("/bbb");
        itemEntity2.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX);

        itemEntities2.add(itemEntity2);
        entities.add(entity2);

        CompletableFuture<List<ApiDefinitionEntity>> completableFuture = mock(CompletableFuture.class);
        given(completableFuture.get()).willReturn(entities);
        given(sentinelApiClient.fetchApis(TEST_APP, TEST_IP, TEST_PORT)).willReturn(completableFuture);

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(path);
        requestBuilder.param("app", TEST_APP);
        requestBuilder.param("ip", TEST_IP);
        requestBuilder.param("port", String.valueOf(TEST_PORT));

        // Do controller logic
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();

        // Verify the fetchApis method has been called
        verify(sentinelApiClient).fetchApis(TEST_APP, TEST_IP, TEST_PORT);

        // Verify if two same entities are got
        Result<List<ApiDefinitionEntity>> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference<Result<List<ApiDefinitionEntity>>>(){});
        assertTrue(result.isSuccess());

        List<ApiDefinitionEntity> data = result.getData();
        assertEquals(2, data.size());
        assertEquals(entities, data);

        // Verify the entities are add into memory repository
        List<ApiDefinitionEntity> entitiesInMem = repository.findAllByApp(TEST_APP);
        assertEquals(2, entitiesInMem.size());
        assertEquals(entities, entitiesInMem);
    }

    @Test
    public void testAddApi() throws Exception {
        String path = "/gateway/api/new.json";

        AddApiReqVo reqVo = new AddApiReqVo();
        reqVo.setApp(TEST_APP);
        reqVo.setIp(TEST_IP);
        reqVo.setPort(TEST_PORT);

        reqVo.setApiName("customized_api");

        List<ApiPredicateItemVo> itemVos = new ArrayList<>();
        ApiPredicateItemVo itemVo = new ApiPredicateItemVo();
        itemVo.setMatchStrategy(URL_MATCH_STRATEGY_EXACT);
        itemVo.setPattern("/product");
        itemVos.add(itemVo);
        reqVo.setPredicateItems(itemVos);

        given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true);

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path);
        requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON);

        // Do controller logic
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print()).andReturn();

        // Verify the modifyApis method has been called
        verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any());

        Result<ApiDefinitionEntity> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference<Result<ApiDefinitionEntity>>() {});
        assertTrue(result.isSuccess());

        // Verify the result
        ApiDefinitionEntity entity = result.getData();
        assertNotNull(entity);
        assertEquals(TEST_APP, entity.getApp());
        assertEquals(TEST_IP, entity.getIp());
        assertEquals(TEST_PORT, entity.getPort());
        assertEquals("customized_api", entity.getApiName());
        assertNotNull(entity.getId());
        assertNotNull(entity.getGmtCreate());
        assertNotNull(entity.getGmtModified());

        Set<ApiPredicateItemEntity> predicateItemEntities = entity.getPredicateItems();
        assertEquals(1, predicateItemEntities.size());
        ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next();
        assertEquals(URL_MATCH_STRATEGY_EXACT, predicateItemEntity.getMatchStrategy().intValue());
        assertEquals("/product", predicateItemEntity.getPattern());

        // Verify the entity which is add in memory repository
        List<ApiDefinitionEntity> entitiesInMem = repository.findAllByApp(TEST_APP);
        assertEquals(1, entitiesInMem.size());
        assertEquals(entity, entitiesInMem.get(0));
    }

    @Test
    public void testUpdateApi() throws Exception {
        String path = "/gateway/api/save.json";

        // Add one entity to memory repository for update
        ApiDefinitionEntity addEntity = new ApiDefinitionEntity();
        addEntity.setApp(TEST_APP);
        addEntity.setIp(TEST_IP);
        addEntity.setPort(TEST_PORT);
        addEntity.setApiName("bbb");
        Date date = new Date();
        // To make the gmtModified different when do update
        date = DateUtils.addSeconds(date, -1);
        addEntity.setGmtCreate(date);
        addEntity.setGmtModified(date);
        Set<ApiPredicateItemEntity> addRedicateItemEntities = new HashSet<>();
        addEntity.setPredicateItems(addRedicateItemEntities);
        ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity();
        addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT);
        addPredicateItemEntity.setPattern("/order");
        addEntity = repository.save(addEntity);

        UpdateApiReqVo reqVo = new UpdateApiReqVo();
        reqVo.setId(addEntity.getId());
        reqVo.setApp(TEST_APP);
        List<ApiPredicateItemVo> itemVos = new ArrayList<>();
        ApiPredicateItemVo itemVo = new ApiPredicateItemVo();
        itemVo.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX);
        itemVo.setPattern("/my_order");
        itemVos.add(itemVo);
        reqVo.setPredicateItems(itemVos);

        given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true);

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path);
        requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON);

        // Do controller logic
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print()).andReturn();

        // Verify the modifyApis method has been called
        verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any());

        Result<ApiDefinitionEntity> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference<Result<ApiDefinitionEntity>>() {});
        assertTrue(result.isSuccess());

        ApiDefinitionEntity entity = result.getData();
        assertNotNull(entity);
        assertEquals("bbb", entity.getApiName());
        assertEquals(date, entity.getGmtCreate());
        // To make sure gmtModified has been set and it's different from gmtCreate
        assertNotNull(entity.getGmtModified());
        assertNotEquals(entity.getGmtCreate(), entity.getGmtModified());

        Set<ApiPredicateItemEntity> predicateItemEntities = entity.getPredicateItems();
        assertEquals(1, predicateItemEntities.size());
        ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next();
        assertEquals(URL_MATCH_STRATEGY_PREFIX, predicateItemEntity.getMatchStrategy().intValue());
        assertEquals("/my_order", predicateItemEntity.getPattern());

        // Verify the entity which is update in memory repository
        List<ApiDefinitionEntity> entitiesInMem = repository.findAllByApp(TEST_APP);
        assertEquals(1, entitiesInMem.size());
        assertEquals(entity, entitiesInMem.get(0));
    }

    @Test
    public void testDeleteApi() throws Exception {
        String path = "/gateway/api/delete.json";

        // Add one entity into memory repository for delete
        ApiDefinitionEntity addEntity = new ApiDefinitionEntity();
        addEntity.setApp(TEST_APP);
        addEntity.setIp(TEST_IP);
        addEntity.setPort(TEST_PORT);
        addEntity.setApiName("ccc");
        Date date = new Date();
        addEntity.setGmtCreate(date);
        addEntity.setGmtModified(date);
        Set<ApiPredicateItemEntity> addRedicateItemEntities = new HashSet<>();
        addEntity.setPredicateItems(addRedicateItemEntities);
        ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity();
        addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT);
        addPredicateItemEntity.setPattern("/user/add");
        addEntity = repository.save(addEntity);

        given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true);

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path);
        requestBuilder.param("id", String.valueOf(addEntity.getId()));

        // Do controller logic
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();

        // Verify the modifyApis method has been called
        verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any());

        // Verify the result
        Result<Long> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference<Result<Long>>() {});
        assertTrue(result.isSuccess());

        assertEquals(addEntity.getId(), result.getData());

        // Now no entities in memory
        List<ApiDefinitionEntity> entitiesInMem = repository.findAllByApp(TEST_APP);
        assertEquals(0, entitiesInMem.size());
    }
}
