/**
 * 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.metron.rest.controller;

import org.apache.hadoop.hbase.shaded.com.google.common.collect.ImmutableList;
import org.apache.metron.common.configuration.SensorParserConfig;
import org.apache.metron.integration.utils.TestUtils;
import org.apache.metron.rest.MetronRestConstants;
import org.apache.metron.rest.model.TopologyStatusCode;
import org.apache.metron.rest.service.GlobalConfigService;
import org.apache.metron.rest.service.SensorParserConfigService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.HashMap;
import java.util.Map;

import static org.apache.metron.rest.MetronRestConstants.TEST_PROFILE;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(TEST_PROFILE)
public class StormControllerIntegrationTest {

  @Autowired
  private Environment environment;

  @Autowired
  private WebApplicationContext wac;

  @Autowired
  private GlobalConfigService globalConfigService;

  @Autowired
  private SensorParserConfigService sensorParserConfigService;

  private MockMvc mockMvc;

  private String stormUrl = "/api/v1/storm";
  private String user = "user";
  private String password = "password";

  private String metronVersion;

  @BeforeEach
  public void setup() {
    this.metronVersion = this.environment.getProperty("metron.version");
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).apply(springSecurity()).build();
  }

  @Test
  public void testSecurity() throws Exception {
    this.mockMvc.perform(get(stormUrl))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/supervisors"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/broTest"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/parser/start/broTest"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/parser/stop/broTest"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/parser/activate/broTest"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/parser/deactivate/broTest"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get("/enrichment"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/enrichment/start"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/enrichment/stop"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/enrichment/activate"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/enrichment/deactivate"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get("/indexing"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/indexing/start"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/indexing/stop"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/indexing/activate"))
            .andExpect(status().isUnauthorized());

    this.mockMvc.perform(get(stormUrl + "/indexing/deactivate"))
            .andExpect(status().isUnauthorized());
  }

  @Test
  public void test() throws Exception {
    this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(0)));

    this.mockMvc.perform(get(stormUrl + "/broTest").with(httpBasic(user,password)))
            .andExpect(status().isNotFound());

    Map<String, Object> globalConfig = globalConfigService.get();
    if (globalConfig == null) {
      globalConfig = new HashMap<>();
    }
    globalConfigService.delete();
    sensorParserConfigService.delete("broTest");

    this.mockMvc.perform(get(stormUrl + "/parser/stop/broTest?stopNow=true").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STOP_ERROR.toString()));

    this.mockMvc.perform(get(stormUrl + "/parser/activate/broTest").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.TOPOLOGY_NOT_FOUND.name()));

    this.mockMvc.perform(get(stormUrl + "/parser/deactivate/broTest").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.TOPOLOGY_NOT_FOUND.name()));

    this.mockMvc.perform(get(stormUrl + "/parser/start/broTest").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.GLOBAL_CONFIG_MISSING.name()));

    globalConfigService.save(globalConfig);
    {
      final Map<String, Object> expectedGlobalConfig = globalConfig;
      //we must wait for the config to find its way into the config.
      TestUtils.assertEventually(() -> assertEquals(expectedGlobalConfig, globalConfigService.get()));
    }

    this.mockMvc.perform(get(stormUrl + "/parser/start/broTest").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.SENSOR_PARSER_CONFIG_MISSING.name()));

    SensorParserConfig sensorParserConfig = new SensorParserConfig();
    sensorParserConfig.setParserClassName("org.apache.metron.parsers.bro.BasicBroParser");
    sensorParserConfig.setSensorTopic("broTest");
    sensorParserConfigService.save("broTest", sensorParserConfig);
    {
      final SensorParserConfig expectedSensorParserConfig = sensorParserConfig;
      //we must wait for the config to find its way into the config.
      TestUtils.assertEventually(() -> assertEquals(expectedSensorParserConfig, sensorParserConfigService.findOne("broTest")));
    }

    this.mockMvc.perform(get(stormUrl + "/parser/start/broTest").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STARTED.name()));

    this.mockMvc.perform(get(stormUrl + "/supervisors").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(jsonPath("$.supervisors[0]").exists())
            .andExpect(jsonPath("$.supervisors[0].id").exists())
            .andExpect(jsonPath("$.supervisors[0].host").exists())
            .andExpect(jsonPath("$.supervisors[0].uptime").exists())
            .andExpect(jsonPath("$.supervisors[0].slotsTotal").exists())
            .andExpect(jsonPath("$.supervisors[0].slotsUsed").exists());

    this.mockMvc.perform(get(stormUrl + "/broTest").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(jsonPath("$.name").value("broTest"))
            .andExpect(jsonPath("$.id", containsString("broTest")))
            .andExpect(jsonPath("$.status").value("ACTIVE"))
            .andExpect(jsonPath("$.latency").exists())
            .andExpect(jsonPath("$.throughput").exists())
            .andExpect(jsonPath("$.emitted").exists())
            .andExpect(jsonPath("$.acked").exists());

    this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(jsonPath("$[?(@.name == 'broTest' && @.status == 'ACTIVE')]").exists());

    this.mockMvc.perform(get(stormUrl + "/parser/stop/broTest?stopNow=true").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STOPPED.name()));

    this.mockMvc.perform(get(stormUrl + "/enrichment").with(httpBasic(user,password)))
            .andExpect(status().isNotFound());

    this.mockMvc.perform(get(stormUrl + "/enrichment/activate").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.TOPOLOGY_NOT_FOUND.name()));

    this.mockMvc.perform(get(stormUrl + "/enrichment/deactivate").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.TOPOLOGY_NOT_FOUND.name()));

    this.mockMvc.perform(get(stormUrl + "/enrichment/stop?stopNow=true").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("ERROR"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STOP_ERROR.toString()));

    this.mockMvc.perform(get(stormUrl + "/enrichment/start").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STARTED.toString()));

    this.mockMvc.perform(get(stormUrl + "/enrichment/deactivate").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.INACTIVE.name()));

    this.mockMvc.perform(get(stormUrl + "/enrichment/deactivate").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.INACTIVE.name()));

    this.mockMvc.perform(get(stormUrl + "/enrichment/activate").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.ACTIVE.name()));

    this.mockMvc.perform(get(stormUrl + "/enrichment").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(jsonPath("$.name").value("enrichment"))
            .andExpect(jsonPath("$.id", containsString("enrichment")))
            .andExpect(jsonPath("$.status").value("ACTIVE"))
            .andExpect(jsonPath("$.latency").exists())
            .andExpect(jsonPath("$.throughput").exists())
            .andExpect(jsonPath("$.emitted").exists())
            .andExpect(jsonPath("$.acked").exists());

    this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(jsonPath("$[?(@.name == 'enrichment' && @.status == 'ACTIVE')]").exists());

    this.mockMvc.perform(get(stormUrl + "/enrichment/stop").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STOPPED.name()));

    for(String type : ImmutableList.of("randomaccess", "batch")) {
      this.mockMvc.perform(get(stormUrl + "/indexing/" + type).with(httpBasic(user,password)))
              .andExpect(status().isNotFound());
      this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/activate").with(httpBasic(user, password)))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.status").value("ERROR"))
              .andExpect(jsonPath("$.message").value(TopologyStatusCode.TOPOLOGY_NOT_FOUND.name()));

      this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/deactivate").with(httpBasic(user, password)))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.status").value("ERROR"))
              .andExpect(jsonPath("$.message").value(TopologyStatusCode.TOPOLOGY_NOT_FOUND.name()));

      this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/stop?stopNow=true").with(httpBasic(user, password)))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.status").value("ERROR"))
              .andExpect(jsonPath("$.message").value(TopologyStatusCode.STOP_ERROR.toString()));

      this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/start").with(httpBasic(user, password)))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.status").value("SUCCESS"))
              .andExpect(jsonPath("$.message").value(TopologyStatusCode.STARTED.toString()));

      ResultActions actions = this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/deactivate").with(httpBasic(user, password)));
      actions.andExpect(status().isOk())
              .andExpect(jsonPath("$.status").value("SUCCESS"))
              .andExpect(jsonPath("$.message").value(TopologyStatusCode.INACTIVE.name()));

      this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/activate").with(httpBasic(user, password)))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.status").value("SUCCESS"))
              .andExpect(jsonPath("$.message").value(TopologyStatusCode.ACTIVE.name()));
      String topologyName = type.equals("randomaccess")? MetronRestConstants.RANDOM_ACCESS_INDEXING_TOPOLOGY_NAME:MetronRestConstants.BATCH_INDEXING_TOPOLOGY_NAME;
      this.mockMvc.perform(get(stormUrl + "/indexing/" + type).with(httpBasic(user, password)))
              .andExpect(status().isOk())
              .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
              .andExpect(jsonPath("$.name").value(topologyName))
              .andExpect(jsonPath("$.id", containsString("indexing")))
              .andExpect(jsonPath("$.status").value("ACTIVE"))
              .andExpect(jsonPath("$.latency").exists())
              .andExpect(jsonPath("$.throughput").exists())
              .andExpect(jsonPath("$.emitted").exists())
              .andExpect(jsonPath("$.acked").exists());
      this.mockMvc.perform(get(stormUrl).with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(jsonPath("$[?(@.name == '" + topologyName + "' && @.status == 'ACTIVE')]").exists());

      this.mockMvc.perform(get(stormUrl + "/indexing/" + type + "/stop").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("SUCCESS"))
            .andExpect(jsonPath("$.message").value(TopologyStatusCode.STOPPED.name()));

    }


    this.mockMvc.perform(get(stormUrl + "/client/status").with(httpBasic(user,password)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.stormClientVersionInstalled").value("1.0.1"))
            .andExpect(jsonPath("$.parserScriptPath").value("/usr/metron/" + metronVersion + "/bin/start_parser_topology.sh"))
            .andExpect(jsonPath("$.enrichmentScriptPath").value("/usr/metron/" + metronVersion + "/bin/start_enrichment_topology.sh"))
            .andExpect(jsonPath("$.randomAccessIndexingScriptPath").value("/usr/metron/" + metronVersion + "/bin/start_elasticsearch_topology.sh"))
            .andExpect(jsonPath("$.batchIndexingScriptPath").value("/usr/metron/" + metronVersion + "/bin/start_hdfs_topology.sh"));

    globalConfigService.delete();
    sensorParserConfigService.delete("broTest");
  }
}
