/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.dubbo.admin.controller;

import org.apache.dubbo.admin.AbstractSpringIntegrationTest;
import org.apache.dubbo.admin.common.util.ConvertUtil;
import org.apache.dubbo.admin.common.util.YamlParser;
import org.apache.dubbo.admin.model.dto.ConditionRouteDTO;
import org.apache.dubbo.admin.model.store.RoutingRule;
import org.apache.dubbo.admin.service.ConsumerService;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;

public class ConditionRoutesControllerTest extends AbstractSpringIntegrationTest {
  private final String env = "whatever";

  @MockBean
  private ConsumerService consumerService;

  @After
  public void tearDown() throws Exception {
    if (zkClient.checkExists().forPath("/dubbo/config/dubbo") != null) {
      zkClient.delete().deletingChildrenIfNeeded().forPath("/dubbo/config/dubbo");
    }
  }

  @Test
  public void shouldThrowWhenParamInvalid() {
    String uuid = UUID.randomUUID().toString();

    ConditionRouteDTO dto = new ConditionRouteDTO();
    ResponseEntity<String> responseEntity = restTemplate.postForEntity(
        url("/api/{env}/rules/route/condition"), dto, String.class, env
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.BAD_REQUEST));
    assertThat(responseEntity.getBody(), containsString("serviceName and app is Empty!"));

    dto.setApplication("application" + uuid);
    when(consumerService.findVersionInApplication(dto.getApplication())).thenReturn("2.6");
    responseEntity = restTemplate.postForEntity(
        url("/api/{env}/rules/route/condition"), dto, String.class, env
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR));
    assertThat(responseEntity.getBody(), containsString("dubbo 2.6 does not support application scope routing rule"));
  }

  @Test
  public void shouldCreateRule() {
    String uuid = UUID.randomUUID().toString();
    String application = "application" + uuid;
    String service = "service" + uuid;
    String serviceVersion = "version" + uuid;
    String serviceGroup = "group" + uuid;
    List<String> conditions = Collections.singletonList("=> host != 172.22.3.91");

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setService(service);
    dto.setConditions(conditions);

    ResponseEntity<String> responseEntity = restTemplate.postForEntity(
            url("/api/{env}/rules/route/condition" + "?serviceVersion=" + serviceVersion + "&serviceGroup=" + serviceGroup), dto, String.class, env
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.CREATED));

    dto.setApplication(application);
    when(consumerService.findVersionInApplication(dto.getApplication())).thenReturn("2.7");

    responseEntity = restTemplate.postForEntity(
        url("/api/{env}/rules/route/condition"), dto, String.class, env
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.CREATED));
  }

  @Test
  public void shouldUpdateRule() throws Exception {
    String service = "org.apache.dubbo.demo.DemoService";
    String content = "conditions:\n"
        + "- => host != 172.22.3.111\n"
        + "- => host != 172.22.3.112\n"
        + "enabled: true\n"
        + "force: true\n"
        + "key: " + service + "\n"
        + "priority: 0\n"
        + "runtime: false\n"
        + "scope: service";
    String path = "/dubbo/config/dubbo/" + service + "::.condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    List<String> newConditions = Arrays.asList("=> host != 172.22.3.211", "=> host != 172.22.3.212");

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setConditions(newConditions);
    dto.setService(service);

    String id = ConvertUtil.getIdFromDTO(dto);
    ResponseEntity<String> responseEntity = restTemplate.exchange(
        url("/api/{env}/rules/route/condition/{id}"), HttpMethod.PUT,
        new HttpEntity<>(dto, null), String.class, env, id
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));

    byte[] bytes = zkClient.getData().forPath(path);
    String updatedConfig = new String(bytes);
    RoutingRule rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertThat(rule.getConditions(), containsInAnyOrder(newConditions.toArray()));
  }

  @Test
  public void shouldGetServiceRule() throws Exception {
    String service = "org.apache.dubbo.demo.DemoService";
    String content = "conditions:\n"
        + "- => host != 172.22.3.111\n"
        + "- => host != 172.22.3.112\n"
        + "enabled: true\n"
        + "force: true\n"
        + "key: " + service + "\n"
        + "priority: 0\n"
        + "runtime: false\n"
        + "scope: service";
    String path = "/dubbo/config/dubbo/" + service + "::.condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    ResponseEntity<List<ConditionRouteDTO>> responseEntity = restTemplate.exchange(
        url("/api/{env}/rules/route/condition/?service={service}"), HttpMethod.GET,
        null, new ParameterizedTypeReference<List<ConditionRouteDTO>>() {
        }, env, service
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
    assertThat(responseEntity.getBody(), hasSize(1));
    List<String> conditions = responseEntity.getBody()
        .stream()
        .flatMap(it -> it.getConditions().stream())
        .collect(Collectors.toList());
    assertThat(conditions, hasSize(2));
    assertThat(conditions, containsInAnyOrder("=> host != 172.22.3.111", "=> host != 172.22.3.112"));
  }

  @Test
  public void serviceShouldDeleteRule() throws Exception {
    String service = "org.apache.dubbo.demo.DemoService";
    String serviceContent = "conditions:\n"
        + "- => host != 172.22.3.111\n"
        + "- => host != 172.22.3.112\n"
        + "enabled: true\n"
        + "force: true\n"
        + "key: " + service + "\n"
        + "priority: 0\n"
        + "runtime: false\n"
        + "scope: service";
    String path = "/dubbo/config/dubbo/" + service + "::.condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, serviceContent.getBytes());

    assertNotNull("zk path should not be null before deleting", zkClient.checkExists().forPath(path));

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setService(service);
    String id = ConvertUtil.getIdFromDTO(dto);
    ResponseEntity<String> responseEntity = restTemplate.exchange(
            url("/api/{env}/rules/route/condition/{id}" + "?scope=service"), HttpMethod.DELETE,
            null, String.class, env, id
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));

    assertNull(zkClient.checkExists().forPath(path));
  }

  @Test
  public void applicationShouldDeleteRule() throws Exception {
    String application = "test-application";
    String serviceContent = "conditions:\n"
            + "- => host != 172.22.3.111\n"
            + "- => host != 172.22.3.112\n"
            + "enabled: true\n"
            + "force: true\n"
            + "key: " + application + "\n"
            + "priority: 0\n"
            + "runtime: false\n"
            + "scope: application";
    String path = "/dubbo/config/dubbo/" + application + ".condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, serviceContent.getBytes());

    assertNotNull("zk path should not be null before deleting", zkClient.checkExists().forPath(path));

    ResponseEntity<String> responseEntity = restTemplate.exchange(
            url("/api/{env}/rules/route/condition/{service}" + "?scope=application"), HttpMethod.DELETE,
            null, String.class, env, application
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));

    assertNull(zkClient.checkExists().forPath(path));
  }

  @Test
  public void shouldThrowWhenDetailRouteWithUnknownId() {
    ResponseEntity<String> responseEntity = restTemplate.getForEntity(
            url("/api/{env}/rules/route/condition/{id}" + "?scope=service"), String.class, env, "non-existed-service"
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.NOT_FOUND));
  }

  @Test
  public void serviceShouldGetRouteDetail() throws Exception {
    String service = "org.apache.dubbo.demo.DemoService";
    String content = "conditions:\n"
        + "- => host != 172.22.3.111\n"
        + "- => host != 172.22.3.112\n"
        + "enabled: true\n"
        + "force: true\n"
        + "key: " + service + "\n"
        + "priority: 0\n"
        + "runtime: false\n"
        + "scope: service";
    String path = "/dubbo/config/dubbo/" + service + "::.condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setService(service);
    String id = ConvertUtil.getIdFromDTO(dto);
    ResponseEntity<ConditionRouteDTO> responseEntity = restTemplate.getForEntity(
        url("/api/{env}/rules/route/condition/{id}" + "?scope=service"), ConditionRouteDTO.class, env, id
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));

    ConditionRouteDTO conditionRouteDTO = responseEntity.getBody();
    assertNotNull(conditionRouteDTO);
    assertThat(conditionRouteDTO.getConditions(), hasSize(2));
    assertThat(conditionRouteDTO.getConditions(), containsInAnyOrder("=> host != 172.22.3.111", "=> host != 172.22.3.112"));
  }

  @Test
  public void applicationShouldGetRouteDetail() throws Exception {
    String application = "test-application";
    String content = "conditions:\n"
            + "- => host != 172.22.3.111\n"
            + "- => host != 172.22.3.112\n"
            + "enabled: true\n"
            + "force: true\n"
            + "key: " + application + "\n"
            + "priority: 0\n"
            + "runtime: false\n"
            + "scope: application";
    String path = "/dubbo/config/dubbo/" + application + ".condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    ResponseEntity<ConditionRouteDTO> responseEntity = restTemplate.getForEntity(
            url("/api/{env}/rules/route/condition/{id}" + "?scope=application"), ConditionRouteDTO.class, env, application
    );
    assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));

    ConditionRouteDTO conditionRouteDTO = responseEntity.getBody();
    assertNotNull(conditionRouteDTO);
    assertThat(conditionRouteDTO.getConditions(), hasSize(2));
    assertThat(conditionRouteDTO.getConditions(), containsInAnyOrder("=> host != 172.22.3.111", "=> host != 172.22.3.112"));
  }

  @Test
  public void serviceShouldEnableRoute() throws Exception {
    String service = "org.apache.dubbo.demo.DemoService";
    String content = "conditions:\n"
        + "- => host != 172.22.3.111\n"
        + "- => host != 172.22.3.112\n"
        + "enabled: false\n"
        + "force: true\n"
        + "key: " + service + "\n"
        + "priority: 0\n"
        + "runtime: false\n"
        + "scope: service";
    String path = "/dubbo/config/dubbo/" + service + "::.condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    byte[] bytes = zkClient.getData().forPath(path);
    String updatedConfig = new String(bytes);
    RoutingRule rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertFalse(rule.isEnabled());

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setService(service);
    String id = ConvertUtil.getIdFromDTO(dto);
    restTemplate.put(url("/api/{env}/rules/route/condition/enable/{id}" + "?scope=service"), null, env, id);

    bytes = zkClient.getData().forPath(path);
    updatedConfig = new String(bytes);
    rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertTrue(rule.isEnabled());
  }

  @Test
  public void applicationShouldEnableRoute() throws Exception {
    String application = "test-application";
    String content = "conditions:\n"
            + "- => host != 172.22.3.111\n"
            + "- => host != 172.22.3.112\n"
            + "enabled: false\n"
            + "force: true\n"
            + "key: " + application + "\n"
            + "priority: 0\n"
            + "runtime: false\n"
            + "scope: service";
    String path = "/dubbo/config/dubbo/" + application + ".condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    byte[] bytes = zkClient.getData().forPath(path);
    String updatedConfig = new String(bytes);
    RoutingRule rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertFalse(rule.isEnabled());

    restTemplate.put(url("/api/{env}/rules/route/condition/enable/{id}" + "?scope=application"), null, env, application);

    bytes = zkClient.getData().forPath(path);
    updatedConfig = new String(bytes);
    rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertTrue(rule.isEnabled());
  }

  @Test
  public void serviceShouldDisableRoute() throws Exception {
    String service = "org.apache.dubbo.demo.DemoService";
    String content = "conditions:\n"
        + "- => host != 172.22.3.111\n"
        + "- => host != 172.22.3.112\n"
        + "enabled: true\n"
        + "force: false\n"
        + "key: " + service + "\n"
        + "priority: 0\n"
        + "runtime: false\n"
        + "scope: service";
    String path = "/dubbo/config/dubbo/" + service + "::.condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    byte[] bytes = zkClient.getData().forPath(path);
    String updatedConfig = new String(bytes);
    RoutingRule rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertTrue(rule.isEnabled());

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setService(service);
    String id = ConvertUtil.getIdFromDTO(dto);
    restTemplate.put(url("/api/{env}/rules/route/condition/disable/{id}" + "?scope=service"), null, env, id);

    bytes = zkClient.getData().forPath(path);
    updatedConfig = new String(bytes);
    rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertFalse(rule.isEnabled());
  }

  @Test
  public void applicationShouldDisableRoute() throws Exception {
    String application = "test-application";
    String content = "conditions:\n"
            + "- => host != 172.22.3.111\n"
            + "- => host != 172.22.3.112\n"
            + "enabled: true\n"
            + "force: false\n"
            + "key: " + application + "\n"
            + "priority: 0\n"
            + "runtime: false\n"
            + "scope: application";
    String path = "/dubbo/config/dubbo/" + application + ".condition-router";
    zkClient.create().creatingParentContainersIfNeeded().forPath(path);
    zkClient.setData().forPath(path, content.getBytes());

    byte[] bytes = zkClient.getData().forPath(path);
    String updatedConfig = new String(bytes);
    RoutingRule rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertTrue(rule.isEnabled());

    ConditionRouteDTO dto = new ConditionRouteDTO();
    dto.setApplication(application);
    String id = ConvertUtil.getIdFromDTO(dto);
    restTemplate.put(url("/api/{env}/rules/route/condition/disable/{id}" + "?scope=application"), null, env, id);

    bytes = zkClient.getData().forPath(path);
    updatedConfig = new String(bytes);
    rule = YamlParser.loadObject(updatedConfig, RoutingRule.class);
    assertFalse(rule.isEnabled());
  }

}
