/*
 * 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.geode.management.internal.rest;

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.test.junit.assertions.ClusterManagementRealizationResultAssert.assertManagementResult;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Collections;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;

import org.apache.geode.management.api.ClusterManagementResult;
import org.apache.geode.management.api.ClusterManagementService;
import org.apache.geode.management.api.RestTemplateClusterManagementServiceTransport;
import org.apache.geode.management.cluster.client.ClusterManagementServiceBuilder;
import org.apache.geode.management.configuration.Index;
import org.apache.geode.management.configuration.IndexType;
import org.apache.geode.management.configuration.Region;
import org.apache.geode.management.configuration.Region.Expiration;
import org.apache.geode.management.configuration.Region.ExpirationAction;
import org.apache.geode.management.configuration.Region.ExpirationType;
import org.apache.geode.management.configuration.RegionType;
import org.apache.geode.util.internal.GeodeJsonMapper;

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = {"classpath*:WEB-INF/management-servlet.xml"},
    loader = PlainLocatorContextLoader.class)
@WebAppConfiguration
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class RegionManagementIntegrationTest {

  @Autowired
  private WebApplicationContext webApplicationContext;

  // needs to be used together with any BaseLocatorContextLoader
  private LocatorWebContext context;

  private ClusterManagementService client;

  private Index index;
  private Region region;
  private static final ObjectMapper mapper = GeodeJsonMapper.getMapper();

  @Before
  public void before() {
    context = new LocatorWebContext(webApplicationContext);
    client = new ClusterManagementServiceBuilder().setTransport(
        new RestTemplateClusterManagementServiceTransport(
            new RestTemplate(context.getRequestFactory())))
        .build();
    region = new Region();
    index = new Index();
  }

  @Test
  @WithMockUser
  public void sanityCheck() {
    region.setName("customers");
    region.setType(RegionType.PARTITION);
    region.setDiskStoreName("diskStore");
    region.setKeyConstraint("keyConstraint");
    region.setValueConstraint("valueConstraint");
    region.setRedundantCopies(1);
    Expiration expiration = new Expiration();
    expiration.setType(ExpirationType.ENTRY_IDLE_TIME);
    expiration.setAction(ExpirationAction.DESTROY);
    expiration.setTimeInSeconds(1);
    region.setExpirations(Collections.singletonList(expiration));

    assertManagementResult(client.create(region))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
    assertManagementResult(client.delete(region))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }

  @Test
  public void invalidGroup() {
    region.setName("customers");
    region.setGroup("cluster");

    assertThatThrownBy(() -> client.create(region))
        .hasMessageContaining("ILLEGAL_ARGUMENT: 'cluster' is a reserved group name");
  }

  @Test
  @WithMockUser
  public void ping() throws Exception {
    context.perform(get("/v1/ping"))
        .andExpect(content().string("pong"));
  }

  @Test
  public void createIndexOnNonExistentRegion() throws Exception {
    index.setName("index1");
    index.setRegionPath("regionA");
    index.setExpression("id");
    String postUrl = index.getLinks().getList();
    context.perform(post("/v1" + postUrl).content(mapper.writeValueAsString(index)))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.statusCode", Matchers.is("ENTITY_NOT_FOUND")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers.containsString("Region provided does not exist: regionA.")));
  }

  @Test
  public void createDuplicateIndex() throws Exception {
    createClusterRegion();

    createClusterIndex();

    // trying to create a duplicate index, reusing existing
    String postUrl = index.getLinks().getList();
    context.perform(post("/v1" + postUrl).content(mapper.writeValueAsString(index)))
        .andExpect(status().isConflict())
        .andExpect(jsonPath("$.statusCode", Matchers.is("ENTITY_EXISTS")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers.containsString("Index 'index1' already exists.")));

    // trying to create another index in this region
    index.setName("index2");
    index.setRegionPath("region1");
    index.setExpression("key");
    context.perform(post("/v1/regions/region1/indexes").content(mapper.writeValueAsString(index)))
        .andExpect(status().isCreated());

    assertManagementResult(client.delete(region))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }

  @Test
  public void postToIndexRegionEndPoint() throws Exception {
    index.setName("index");
    index.setRegionPath(SEPARATOR + "customers");
    index.setExpression("id");
    context.perform(post("/v1/regions/products/indexes").content(mapper.writeValueAsString(index)))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.statusCode", Matchers.is("ILLEGAL_ARGUMENT")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Region name in path must match Region name in configuration.")));

    index.setRegionPath(null);
    context.perform(post("/v1/regions/products/indexes").content(mapper.writeValueAsString(index)))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.statusCode", Matchers.is("ENTITY_NOT_FOUND")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers.containsString("Region provided does not exist: products.")));

  }

  private void createClusterRegion() {
    region.setName("region1");
    region.setType(RegionType.PARTITION);
    assertManagementResult(client.create(region))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }

  @Test
  public void createIndex_succeedsForSpecificRegionAndGroup() throws Exception {
    createGroupRegion();

    index.setRegionPath("region1");
    index.setExpression("i am le tired");
    index.setName("itworks");

    context.perform(
        post("/v1/regions/region1/indexes").content(mapper.writeValueAsString(index)))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.statusCode", Matchers.is("OK")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Successfully updated configuration for group1.")));

    deleteRegion();
  }

  @Test
  public void deleteIndex_succeeds() throws Exception {
    createClusterRegion();

    createClusterIndex();

    context.perform(delete("/v1/regions/region1/indexes/index1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.statusCode", Matchers.is("OK")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Successfully updated configuration for cluster.")));

    deleteRegion();
  }

  @Test
  public void deleteIndex_succeeds_with_group() throws Exception {
    createGroupRegion();

    createGroupIndex();

    context.perform(delete("/v1/regions/region1/indexes/index1").param("group", "group1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.statusCode", Matchers.is("OK")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Successfully updated configuration for group1.")));

    deleteRegion();
  }


  @Test
  public void deleteIndex_in_cluster_group_success() throws Exception {
    createClusterRegion();

    createClusterIndex();

    context.perform(delete("/v1/regions/region1/indexes/index1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.statusCode", Matchers.is("OK")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Successfully updated configuration for cluster")));

    deleteRegion();
  }

  @Test
  public void deleteIndex_Index_in_group_success() throws Exception {
    createGroupRegion();

    createGroupIndex();

    context.perform(delete("/v1/regions/region1/indexes/index1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.statusCode", Matchers.is("OK")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Successfully updated configuration for group1")));

    deleteRegion();
  }

  @Test
  public void deleteIndex_fails_index_not_found() throws Exception {
    createClusterRegion();

    context.perform(delete("/v1/regions/region1/indexes/index1"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.statusCode", Matchers.is("ENTITY_NOT_FOUND")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Index 'index1' does not exist.")));

    deleteRegion();
  }

  @Test
  public void deleteIndex_fails_index_not_found_with_group() throws Exception {
    createGroupRegion();

    context.perform(delete("/v1/regions/region1/indexes/index1").param("group", "group1"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.statusCode", Matchers.is("ENTITY_NOT_FOUND")))
        .andExpect(jsonPath("$.statusMessage",
            Matchers
                .containsString("Index 'index1' does not exist.")));

    deleteRegion();
  }

  private void createGroupRegion() {
    region.setName("region1");
    region.setType(RegionType.PARTITION);
    region.setGroup("group1");
    assertManagementResult(client.create(region))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }

  private void deleteRegion() {
    region.setGroup(null);
    assertManagementResult(client.delete(region))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }

  private void createGroupIndex() {
    index.setRegionPath("region1");
    index.setIndexType(IndexType.RANGE);
    index.setName("index1");
    index.setExpression("some expression");
    assertManagementResult(client.create(index))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }

  private void createClusterIndex() {
    index.setRegionPath("region1");
    index.setIndexType(IndexType.RANGE);
    index.setName("index1");
    index.setExpression("some expression");
    assertManagementResult(client.create(index))
        .hasStatusCode(ClusterManagementResult.StatusCode.OK);
  }
}
