/*
 * 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.ambari.server.api.services.stackadvisor;

import static org.easymock.EasyMock.anyString;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.createNiceMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.partialMockBuilder;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRequest.StackAdvisorRequestBuilder;
import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRequest.StackAdvisorRequestType;
import org.apache.ambari.server.api.services.stackadvisor.commands.ComponentLayoutRecommendationCommand;
import org.apache.ambari.server.api.services.stackadvisor.commands.ComponentLayoutValidationCommand;
import org.apache.ambari.server.api.services.stackadvisor.commands.ConfigurationDependenciesRecommendationCommand;
import org.apache.ambari.server.api.services.stackadvisor.commands.ConfigurationRecommendationCommand;
import org.apache.ambari.server.api.services.stackadvisor.commands.ConfigurationValidationCommand;
import org.apache.ambari.server.api.services.stackadvisor.commands.StackAdvisorCommand;
import org.apache.ambari.server.api.services.stackadvisor.commands.StackAdvisorCommandType;
import org.apache.ambari.server.api.services.stackadvisor.recommendations.RecommendationResponse;
import org.apache.ambari.server.api.services.stackadvisor.validations.ValidationResponse;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.internal.AmbariServerConfigurationHandler;
import org.apache.ambari.server.state.ServiceInfo;
import org.junit.Test;
import org.mockito.Mockito;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.google.gson.Gson;

/**
 * StackAdvisorHelper unit tests.
 */
public class StackAdvisorHelperTest {

  @Test
  @SuppressWarnings("unchecked")
  public void testValidate_returnsCommandResult() throws StackAdvisorException, IOException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = stackAdvisorHelperSpy(configuration, saRunner, metaInfo);

    StackAdvisorCommand<ValidationResponse> command = mock(StackAdvisorCommand.class);
    ValidationResponse expected = mock(ValidationResponse.class);

    StackAdvisorRequestType requestType = StackAdvisorRequestType.HOST_GROUPS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    when(command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON)).thenReturn(expected);
    doReturn(command).when(helper).createValidationCommand("ZOOKEEPER", request);
    ValidationResponse response = helper.validate(request);
    assertEquals(expected, response);
  }

  @Test(expected = StackAdvisorException.class)
  @SuppressWarnings("unchecked")
  public void testValidate_commandThrowsException_throwsException() throws StackAdvisorException,
      IOException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = stackAdvisorHelperSpy(configuration, saRunner, metaInfo);

    StackAdvisorCommand<ValidationResponse> command = mock(StackAdvisorCommand.class);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.HOST_GROUPS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    when(command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON)).thenThrow(new StackAdvisorException("message"));
    doReturn(command).when(helper).createValidationCommand("ZOOKEEPER", request);
    helper.validate(request);

    fail();
  }

  @Test
  @SuppressWarnings("unchecked")
  public void testRecommend_returnsCommandResult() throws StackAdvisorException, IOException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = stackAdvisorHelperSpy(configuration, saRunner, metaInfo);

    StackAdvisorCommand<RecommendationResponse> command = mock(StackAdvisorCommand.class);
    RecommendationResponse expected = mock(RecommendationResponse.class);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.HOST_GROUPS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    when(command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON)).thenReturn(expected);
    doReturn(command).when(helper).createRecommendationCommand("ZOOKEEPER", request);
    RecommendationResponse response = helper.recommend(request);

    assertEquals(expected, response);
  }

  @Test(expected = StackAdvisorException.class)
  @SuppressWarnings("unchecked")
  public void testRecommend_commandThrowsException_throwsException() throws StackAdvisorException,
      IOException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = stackAdvisorHelperSpy(configuration, saRunner, metaInfo);

    StackAdvisorCommand<RecommendationResponse> command = mock(StackAdvisorCommand.class);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.HOST_GROUPS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    when(command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON)).thenThrow(new StackAdvisorException("message"));
    doReturn(command).when(helper).createRecommendationCommand("ZOOKEEPER", request);
    helper.recommend(request);

    fail("Expected StackAdvisorException to be thrown");
  }

  @Test
  public void testCreateRecommendationCommand_returnsComponentLayoutRecommendationCommand()
      throws IOException, StackAdvisorException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = new StackAdvisorHelper(configuration, saRunner, metaInfo, null, null);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.HOST_GROUPS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    StackAdvisorCommand<RecommendationResponse> command = helper
        .createRecommendationCommand("ZOOKEEPER", request);

    assertEquals(ComponentLayoutRecommendationCommand.class, command.getClass());
  }

  @Test
  public void testCreateRecommendationCommand_returnsConfigurationRecommendationCommand() throws IOException, StackAdvisorException {
    testCreateConfigurationRecommendationCommand(StackAdvisorRequestType.CONFIGURATIONS, StackAdvisorCommandType.RECOMMEND_CONFIGURATIONS);
  }

  @Test
  public void testCreateRecommendationCommand_returnsSingleSignOnConfigurationRecommendationCommand() throws IOException, StackAdvisorException {
    testCreateConfigurationRecommendationCommand(StackAdvisorRequestType.SSO_CONFIGURATIONS, StackAdvisorCommandType.RECOMMEND_CONFIGURATIONS_FOR_SSO);
  }

  @Test
  public void testCreateRecommendationCommand_returnsLDAPConfigurationRecommendationCommand() throws IOException, StackAdvisorException {
    testCreateConfigurationRecommendationCommand(StackAdvisorRequestType.LDAP_CONFIGURATIONS, StackAdvisorCommandType.RECOMMEND_CONFIGURATIONS_FOR_LDAP);
  }

  @Test
  public void testCreateRecommendationCommand_returnsKerberosConfigurationRecommendationCommand() throws IOException, StackAdvisorException {
    testCreateConfigurationRecommendationCommand(StackAdvisorRequestType.KERBEROS_CONFIGURATIONS, StackAdvisorCommandType.RECOMMEND_CONFIGURATIONS_FOR_KERBEROS);
  }

  @Test
  public void testCreateValidationCommand_returnsComponentLayoutValidationCommand()
      throws IOException, StackAdvisorException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = new StackAdvisorHelper(configuration, saRunner, metaInfo, null, null);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.HOST_GROUPS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    StackAdvisorCommand<ValidationResponse> command = helper.createValidationCommand("ZOOKEEPER", request);

    assertEquals(ComponentLayoutValidationCommand.class, command.getClass());
  }

  @Test
  public void testCreateValidationCommand_returnsConfigurationValidationCommand()
      throws IOException, StackAdvisorException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = new StackAdvisorHelper(configuration, saRunner, metaInfo, null, null);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.CONFIGURATIONS;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    StackAdvisorCommand<ValidationResponse> command = helper.createValidationCommand("ZOOKEEPER", request);

    assertEquals(ConfigurationValidationCommand.class, command.getClass());
  }

  @Test
  public void testCreateRecommendationDependencyCommand_returnsConfigurationDependencyRecommendationCommand()
      throws IOException, StackAdvisorException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = new StackAdvisorHelper(configuration, saRunner, metaInfo, null, null);
    StackAdvisorRequestType requestType = StackAdvisorRequestType.CONFIGURATION_DEPENDENCIES;
    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    StackAdvisorCommand<RecommendationResponse> command = helper.createRecommendationCommand("ZOOKEEPER", request);

    assertEquals(ConfigurationDependenciesRecommendationCommand.class, command.getClass());
  }

  @Test
  public void testClearCacheAndHost() throws IOException, NoSuchFieldException, IllegalAccessException {
    Field hostInfoCacheField = StackAdvisorHelper.class.getDeclaredField("hostInfoCache");
    Field configsRecommendationResponseField = StackAdvisorHelper.class.getDeclaredField("configsRecommendationResponse");
    StackAdvisorHelper helper = testClearCachesSetup(hostInfoCacheField, configsRecommendationResponseField);

    helper.clearCaches("hostName1");

    Map<String, JsonNode> hostInfoCache = (Map<String, JsonNode>) hostInfoCacheField.get(helper);
    Map<String, RecommendationResponse> configsRecommendationResponse =
        (Map<String, RecommendationResponse>) configsRecommendationResponseField.get(helper);

    assertEquals(2, hostInfoCache.size());
    assertTrue(hostInfoCache.containsKey("hostName2"));
    assertTrue(hostInfoCache.containsKey("hostName3"));
    assertTrue(configsRecommendationResponse.isEmpty());
  }

  @Test
  public void testClearCacheAndHosts() throws IOException, NoSuchFieldException, IllegalAccessException {

    Field hostInfoCacheField = StackAdvisorHelper.class.getDeclaredField("hostInfoCache");
    Field configsRecommendationResponseField = StackAdvisorHelper.class.getDeclaredField("configsRecommendationResponse");
    StackAdvisorHelper helper = testClearCachesSetup(hostInfoCacheField, configsRecommendationResponseField);

    helper.clearCaches(new HashSet<>(Arrays.asList(new String[]{"hostName1", "hostName2"})));

    Map<String, JsonNode> hostInfoCache = (Map<String, JsonNode>) hostInfoCacheField.get(helper);
    Map<String, RecommendationResponse> configsRecommendationResponse =
        (Map<String, RecommendationResponse>) configsRecommendationResponseField.get(helper);

    assertEquals(1, hostInfoCache.size());
    assertTrue(hostInfoCache.containsKey("hostName3"));
    assertTrue(configsRecommendationResponse.isEmpty());
  }

  @Test
  public void testCacheRecommendations() throws IOException, StackAdvisorException {
    Configuration configuration = createNiceMock(Configuration.class);
    StackAdvisorRunner stackAdvisorRunner = createNiceMock(StackAdvisorRunner.class);
    AmbariMetaInfo ambariMetaInfo = createNiceMock(AmbariMetaInfo.class);
    AmbariServerConfigurationHandler ambariServerConfigurationHandler = createNiceMock(AmbariServerConfigurationHandler.class);

    expect(configuration.getRecommendationsArtifactsRolloverMax()).andReturn(1);

    replay(configuration, stackAdvisorRunner, ambariMetaInfo, ambariServerConfigurationHandler);

    StackAdvisorHelper helper = partialMockBuilder(StackAdvisorHelper.class).withConstructor(Configuration.class,
        StackAdvisorRunner.class, AmbariMetaInfo.class, AmbariServerConfigurationHandler.class, Gson.class).
        withArgs(configuration, stackAdvisorRunner, ambariMetaInfo, ambariServerConfigurationHandler, new Gson()).
        addMockedMethod("createRecommendationCommand").
        createMock();

    verify(configuration, stackAdvisorRunner, ambariMetaInfo, ambariServerConfigurationHandler);
    reset(ambariMetaInfo);

    ServiceInfo serviceInfo = new ServiceInfo();
    serviceInfo.setServiceAdvisorType(ServiceInfo.ServiceAdvisorType.PYTHON);
    expect(ambariMetaInfo.getService(anyString(), anyString(), anyString())).andReturn(serviceInfo).atLeastOnce();

    ConfigurationRecommendationCommand command = createMock(ConfigurationRecommendationCommand.class);

    StackAdvisorRequest request = StackAdvisorRequestBuilder.
        forStack(null, null).ofType(StackAdvisorRequestType.CONFIGURATIONS).
        build();

    expect(helper.createRecommendationCommand(eq("ZOOKEEPER"), eq(request))).andReturn(command).times(2);

    // populate response with dummy info to check equivalence
    RecommendationResponse response = new RecommendationResponse();
    response.setServices(new HashSet<String>(){{add("service1"); add("service2"); add("service3");}});

    // invoke() should be fired for first recommend() execution only
    expect(command.invoke(eq(request), eq(ServiceInfo.ServiceAdvisorType.PYTHON))).andReturn(response).once();

    replay(ambariMetaInfo, helper, command);

    RecommendationResponse calculatedResponse = helper.recommend(request);
    RecommendationResponse cachedResponse = helper.recommend(request);

    verify(ambariMetaInfo, helper, command);

    assertEquals(response.getServices(), calculatedResponse.getServices());
    assertEquals(response.getServices(), cachedResponse.getServices());
  }

  private StackAdvisorHelper testClearCachesSetup(Field hostInfoCacheField,
                                                  Field configsRecommendationResponseField) throws IOException,
      NoSuchFieldException, IllegalAccessException {
    Configuration configuration = createNiceMock(Configuration.class);
    StackAdvisorRunner stackAdvisorRunner = createNiceMock(StackAdvisorRunner.class);
    AmbariMetaInfo ambariMetaInfo = createNiceMock(AmbariMetaInfo.class);
    AmbariServerConfigurationHandler ambariServerConfigurationHandler = createNiceMock(AmbariServerConfigurationHandler.class);

    replay(configuration, stackAdvisorRunner, ambariMetaInfo, ambariServerConfigurationHandler);

    StackAdvisorHelper helper = new StackAdvisorHelper(configuration, stackAdvisorRunner,
        ambariMetaInfo, ambariServerConfigurationHandler, null);

    verify(configuration, stackAdvisorRunner, ambariMetaInfo, ambariServerConfigurationHandler);

    hostInfoCacheField.setAccessible(true);
    configsRecommendationResponseField.setAccessible(true);

    Map<String, JsonNode> hostInfoCache = new ConcurrentHashMap<>();
    JsonNodeFactory jnf = JsonNodeFactory.instance;
    hostInfoCache.put("hostName1", jnf.nullNode());
    hostInfoCache.put("hostName2", jnf.nullNode());
    hostInfoCache.put("hostName3", jnf.nullNode());
    Map<String, RecommendationResponse> configsRecommendationResponse = new ConcurrentHashMap<>();
    configsRecommendationResponse.put("111", new RecommendationResponse());
    configsRecommendationResponse.put("222", new RecommendationResponse());

    hostInfoCacheField.set(helper, hostInfoCache);
    configsRecommendationResponseField.set(helper, configsRecommendationResponse);
    return helper;
  }

  private void testCreateConfigurationRecommendationCommand(StackAdvisorRequestType requestType, StackAdvisorCommandType expectedCommandType)
      throws IOException, StackAdvisorException {
    Configuration configuration = mock(Configuration.class);
    when(configuration.getRecommendationsArtifactsRolloverMax()).thenReturn(100);
    StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
    AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
    ServiceInfo service = mock(ServiceInfo.class);
    when(metaInfo.getService(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(service);
    when(service.getServiceAdvisorType()).thenReturn(ServiceInfo.ServiceAdvisorType.PYTHON);
    StackAdvisorHelper helper = new StackAdvisorHelper(configuration, saRunner, metaInfo, null, null);

    StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
        .ofType(requestType).build();

    StackAdvisorCommand<RecommendationResponse> command = helper.createRecommendationCommand("ZOOKEEPER", request);

    assertTrue(command instanceof ConfigurationRecommendationCommand);
    assertEquals(expectedCommandType, ((ConfigurationRecommendationCommand) command).getCommandType());

  }

  private static StackAdvisorHelper stackAdvisorHelperSpy(Configuration configuration, StackAdvisorRunner saRunner, AmbariMetaInfo metaInfo) throws IOException {
    return spy(new StackAdvisorHelper(configuration, saRunner, metaInfo, null, null));
  }
}
