content: /*
   * 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.commands;
  
  import static java.util.Collections.emptyMap;
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertNotNull;
  import static org.junit.Assert.assertTrue;
  import static org.mockito.ArgumentMatchers.nullable;
  import static org.mockito.Matchers.any;
  import static org.mockito.Mockito.doAnswer;
  import static org.mockito.Mockito.doReturn;
  import static org.mockito.Mockito.doThrow;
  import static org.mockito.Mockito.mock;
  import static org.mockito.Mockito.spy;
  import static org.mockito.Mockito.when;
  
  import java.io.File;
  import java.io.IOException;
  import java.nio.charset.Charset;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
  import jakarta.ws.rs.WebApplicationException;
  import jakarta.ws.rs.core.HttpHeaders;
  import jakarta.ws.rs.core.MediaType;
  import jakarta.ws.rs.core.Response;
  import jakarta.ws.rs.core.UriInfo;
  
  import org.apache.ambari.server.api.resources.ResourceInstance;
  import org.apache.ambari.server.api.services.AmbariMetaInfo;
  import org.apache.ambari.server.api.services.Request;
  import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorException;
  import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRequest;
  import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRequest.StackAdvisorRequestBuilder;
  import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRequestException;
  import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorResponse;
  import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRunner;
  import org.apache.ambari.server.api.services.stackadvisor.commands.StackAdvisorCommand.StackAdvisorData;
  import org.apache.ambari.server.controller.internal.AmbariServerConfigurationHandler;
  import org.apache.ambari.server.state.ServiceInfo;
  import org.apache.commons.io.FileUtils;
  import org.junit.After;
  import org.junit.Before;
  import org.junit.Test;
  import org.junit.rules.TemporaryFolder;
  import org.junit.runner.RunWith;
  import org.mockito.Mock;
  import org.mockito.runners.MockitoJUnitRunner;
  
  import com.fasterxml.jackson.annotation.JsonProperty;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import com.fasterxml.jackson.databind.node.ArrayNode;
  import com.fasterxml.jackson.databind.node.ObjectNode;
  import com.google.common.collect.Lists;
  
  /**
   * StackAdvisorCommand unit tests.
   */
  @RunWith(MockitoJUnitRunner.class)
  public class StackAdvisorCommandTest {
    private static final String SINGLE_HOST_RESPONSE = "{\"href\":\"/api/v1/hosts?fields=Hosts/*&Hosts/host_name.in(%1$s)\",\"items\":[{\"href\":\"/api/v1/hosts/%1$s\",\"Hosts\":{\"host_name\":\"%1$s\"}}]}";
    private static final String TWO_HOST_RESPONSE = "{\"href\":\"/api/v1/hosts?fields=Hosts/*&Hosts/host_name.in(%1$s,%2$s)\",\"items\":[{\"href\":\"/api/v1/hosts/%1$s\",\"Hosts\":{\"host_name\":\"%1$s\"}},{\"href\":\"/api/v1/hosts/%2$s\",\"Hosts\":{\"host_name\":\"%2$s\"}}]}";
  
    private TemporaryFolder temp = new TemporaryFolder();
    @Mock
    AmbariServerConfigurationHandler ambariServerConfigurationHandler;
  
    @Before
    public void setUp() throws IOException {
      temp.create();
    }
  
    @After
    public void tearDown() throws IOException {
      temp.delete();
    }
  
    @Test(expected = StackAdvisorException.class)
    public void testInvoke_invalidRequest_throwsException() throws StackAdvisorException {
      File recommendationsDir = temp.newFolder("recommendationDir");
      String recommendationsArtifactsLifetime = "1w";
      int requestId = 0;
      StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
      StackAdvisorCommand<TestResource> command = spy(new TestStackAdvisorCommand(recommendationsDir, recommendationsArtifactsLifetime,
          ServiceInfo.ServiceAdvisorType.PYTHON, requestId, saRunner, metaInfo, null));
  
      StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
          .build();
  
      doThrow(new StackAdvisorException("message")).when(command).validate(request);
      command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON);
  
      assertTrue(false);
    }
  
    @Test(expected = StackAdvisorException.class)
    public void testInvoke_saRunnerNotSucceed_throwsException() throws StackAdvisorException {
      File recommendationsDir = temp.newFolder("recommendationDir");
      String recommendationsArtifactsLifetime = "1w";
      int requestId = 0;
      StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
      StackAdvisorCommand<TestResource> command = spy(new TestStackAdvisorCommand(recommendationsDir, recommendationsArtifactsLifetime,
          ServiceInfo.ServiceAdvisorType.PYTHON, requestId, saRunner, metaInfo, null));
  
      StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
          .build();
  
      String hostsJSON = "{\"hosts\" : \"localhost\"";
      String servicesJSON = "{\"services\" : \"HDFS\"";
      StackAdvisorData data = new StackAdvisorData(hostsJSON, servicesJSON);
      doReturn(hostsJSON).when(command).getHostsInformation(request);
      doReturn(servicesJSON).when(command).getServicesInformation(request);
      doReturn(data).when(command)
          .adjust(any(StackAdvisorData.class), any(StackAdvisorRequest.class));
  
      doThrow(new StackAdvisorRequestException("error")).when(saRunner)
          .runScript(any(ServiceInfo.ServiceAdvisorType.class), any(StackAdvisorCommandType.class), any(File.class));
      command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON);
      assertTrue(false);
    }
  
    @Test(expected = WebApplicationException.class)
    public void testInvoke_adjustThrowsException_throwsException() throws StackAdvisorException {
      File recommendationsDir = temp.newFolder("recommendationDir");
      String recommendationsArtifactsLifetime = "1w";
      int requestId = 0;
      StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
      StackAdvisorCommand<TestResource> command = spy(new TestStackAdvisorCommand(recommendationsDir, recommendationsArtifactsLifetime,
          ServiceInfo.ServiceAdvisorType.PYTHON, requestId, saRunner, metaInfo, null));
  
      StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
          .build();
  
      doReturn("{\"hosts\" : \"localhost\"").when(command).getHostsInformation(request);
      doReturn("{\"services\" : \"HDFS\"").when(command).getServicesInformation(request);
      doThrow(new WebApplicationException()).when(command).adjust(any(StackAdvisorData.class),
          any(StackAdvisorRequest.class));
  
      command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON);
  
  
      assertTrue(false);
    }
  
    @Test
    public void testInvoke_success() throws StackAdvisorException {
      String expected = "success";
      final String testResourceString = String.format("{\"type\": \"%s\"}", expected);
      final File recommendationsDir = temp.newFolder("recommendationDir");
      String recommendationsArtifactsLifetime = "1w";
      final int requestId = 2;
      StackAdvisorRunner saRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo metaInfo = mock(AmbariMetaInfo.class);
      final StackAdvisorCommand<TestResource> command = spy(new TestStackAdvisorCommand(
          recommendationsDir, recommendationsArtifactsLifetime, ServiceInfo.ServiceAdvisorType.PYTHON, requestId,
          saRunner, metaInfo, null));
  
      StackAdvisorRequest request = StackAdvisorRequestBuilder.forStack("stackName", "stackVersion")
          .build();
  
      String hostsJSON = "{\"hosts\" : \"localhost\"";
      String servicesJSON = "{\"services\" : \"HDFS\"";
      StackAdvisorData data = new StackAdvisorData(hostsJSON, servicesJSON);
      doReturn(hostsJSON).when(command).getHostsInformation(request);
      doReturn(servicesJSON).when(command).getServicesInformation(request);
      doReturn(data).when(command)
          .adjust(any(StackAdvisorData.class), any(StackAdvisorRequest.class));
  
      doAnswer(invocation -> {
        String resultFilePath = String.format("%s/%s", requestId, command.getResultFileName());
        File resultFile = new File(recommendationsDir, resultFilePath);
        resultFile.getParentFile().mkdirs();
        FileUtils.writeStringToFile(resultFile, testResourceString, Charset.defaultCharset());
        return null;
      }).when(saRunner).runScript(any(ServiceInfo.ServiceAdvisorType.class),
              any(StackAdvisorCommandType.class), any(File.class));
  
      TestResource result = command.invoke(request, ServiceInfo.ServiceAdvisorType.PYTHON);
  
      assertEquals(expected, result.getType());
      assertEquals(requestId, result.getId());
    }
  
    @Test
    public void testPopulateStackHierarchy() throws Exception {
      File file = mock(File.class);
      String recommendationsArtifactsLifetime = "1w";
      StackAdvisorRunner stackAdvisorRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo ambariMetaInfo = mock(AmbariMetaInfo.class);
      StackAdvisorCommand<TestResource> cmd = new TestStackAdvisorCommand(file, recommendationsArtifactsLifetime,
          ServiceInfo.ServiceAdvisorType.PYTHON, 1, stackAdvisorRunner, ambariMetaInfo, null);
      ObjectNode objectNode = (ObjectNode) cmd.mapper.readTree("{\"Versions\": " +
          "{\"stack_name\": \"stack\", \"stack_version\":\"1.0.0\"}}");
  
      doReturn(Arrays.asList("0.9", "0.8")).when(ambariMetaInfo).getStackParentVersions("stack", "1.0.0");
  
      cmd.populateStackHierarchy(objectNode);
  
      JsonNode stackHierarchy = objectNode.get("Versions").get("stack_hierarchy");
      assertNotNull(stackHierarchy);
      JsonNode stackName = stackHierarchy.get("stack_name");
      assertNotNull(stackName);
      assertEquals("stack", stackName.asText());
      ArrayNode stackVersions = (ArrayNode) stackHierarchy.get("stack_versions");
      assertNotNull(stackVersions);
      assertEquals(2, stackVersions.size());
      Iterator<JsonNode> stackVersionsElements = stackVersions.elements();
      assertEquals("0.9", stackVersionsElements.next().asText());
      assertEquals("0.8", stackVersionsElements.next().asText());
    }
  
    @Test
    public void testPopulateAmbariServerProperties() throws Exception {
      File file = mock(File.class);
      String recommendationsArtifactsLifetime = "1w";
      StackAdvisorRunner stackAdvisorRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo ambariMetaInfo = mock(AmbariMetaInfo.class);
      StackAdvisorCommand<TestResource> cmd = new TestStackAdvisorCommand(file, recommendationsArtifactsLifetime, ServiceInfo.ServiceAdvisorType.PYTHON, 1,
        stackAdvisorRunner, ambariMetaInfo, null);
      ObjectNode objectNode = (ObjectNode) cmd.mapper.readTree("{\"Versions\": " +
        "{\"stack_name\": \"stack\", \"stack_version\":\"1.0.0\"}}");
  
      Map<String, String> props = Collections.singletonMap("a", "b");
  
      doReturn(props).when(ambariMetaInfo).getAmbariServerProperties();
  
      cmd.populateAmbariServerInfo(objectNode);
  
      JsonNode serverProperties = objectNode.get("ambari-server-properties");
      assertNotNull(serverProperties);
      assertEquals("b", serverProperties.iterator().next().textValue());
    }
  
    @Test
    public void testPopulateStackHierarchy_noParents() throws Exception {
      File file = mock(File.class);
      String recommendationsArtifactsLifetime = "1w";
      StackAdvisorRunner stackAdvisorRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo ambariMetaInfo = mock(AmbariMetaInfo.class);
      StackAdvisorCommand<TestResource> cmd = new TestStackAdvisorCommand(file, recommendationsArtifactsLifetime, ServiceInfo.ServiceAdvisorType.PYTHON, 1,
          stackAdvisorRunner, ambariMetaInfo, null);
      ObjectNode objectNode = (ObjectNode) cmd.mapper.readTree("{\"Versions\": " +
          "{\"stack_name\": \"stack\", \"stack_version\":\"1.0.0\"}}");
  
      doReturn(Collections.emptyList()).when(ambariMetaInfo).getStackParentVersions("stack", "1.0.0");
  
      cmd.populateStackHierarchy(objectNode);
  
      JsonNode stackHierarchy = objectNode.get("Versions").get("stack_hierarchy");
      assertNotNull(stackHierarchy);
      JsonNode stackName = stackHierarchy.get("stack_name");
      assertNotNull(stackName);
      assertEquals("stack", stackName.asText());
      ArrayNode stackVersions = (ArrayNode) stackHierarchy.get("stack_versions");
      assertNotNull(stackVersions);
      assertEquals(0, stackVersions.size());
    }
  
    @Test
    public void testPopulateLdapConfig() throws Exception {
      Map<String, Map<String, String>> storedConfig = Collections.singletonMap("ldap-configuration",
          Collections.singletonMap("authentication.ldap.secondaryUrl", "localhost:333"));
      TestStackAdvisorCommand command = new TestStackAdvisorCommand(
        temp.newFolder("recommendationDir"),
        "1w",
        ServiceInfo.ServiceAdvisorType.PYTHON,
        0,
        mock(StackAdvisorRunner.class),
        mock(AmbariMetaInfo.class), null);
      when(ambariServerConfigurationHandler.getConfigurations()).thenReturn(storedConfig);
      JsonNode servicesRootNode = json("{}");
      command.populateAmbariConfiguration((ObjectNode)servicesRootNode);
      JsonNode expectedLdapConfig = json("{\"ambari-server-configuration\":{\"ldap-configuration\":{\"authentication.ldap.secondaryUrl\":\"localhost:333\"}}}");
      assertEquals(expectedLdapConfig, servicesRootNode);
    }
  
    @Test
    public void testPopulateLdapConfig_NoConfigs() throws Exception {
      TestStackAdvisorCommand command = new TestStackAdvisorCommand(
        temp.newFolder("recommendationDir"),
        "1w",
        ServiceInfo.ServiceAdvisorType.PYTHON,
        0,
        mock(StackAdvisorRunner.class),
        mock(AmbariMetaInfo.class), null);
      when(ambariServerConfigurationHandler.getConfigurations()).thenReturn(emptyMap());
      JsonNode servicesRootNode = json("{}");
      command.populateAmbariConfiguration((ObjectNode)servicesRootNode);
      JsonNode expectedLdapConfig = json("{\"ambari-server-configuration\":{}}");
      assertEquals(expectedLdapConfig, servicesRootNode);
    }
  
    /**
     * Try to retrieve host info twice. The inner cache should be populated with first usage (with handleRequest method calling).
     * And for next info retrieving for the same host the saved value should be used.
     */
    @Test
    public void testHostInfoCachingSingleHost() throws StackAdvisorException {
      File file = mock(File.class);
      String recommendationsArtifactsLifetime = "1w";
      StackAdvisorRunner stackAdvisorRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo ambariMetaInfo = mock(AmbariMetaInfo.class);
      Map<String, JsonNode> hostInfoCache = new HashMap<>();
      TestStackAdvisorCommand command = spy(new TestStackAdvisorCommand(file, recommendationsArtifactsLifetime,
          ServiceInfo.ServiceAdvisorType.PYTHON, 1,
          stackAdvisorRunner, ambariMetaInfo, hostInfoCache));
  
      // in second handling case NPE will be fired during result processing
      doReturn(Response.status(200).entity(String.format(SINGLE_HOST_RESPONSE, "hostName1")).build())
          .doReturn(null)
          .when(command).handleRequest(nullable(HttpHeaders.class), nullable(String.class), any(UriInfo.class), any(Request.Type.class),
                      nullable(MediaType.class), any(ResourceInstance.class));
  
      StackAdvisorRequest request = StackAdvisorRequestBuilder.
          forStack(null, null).ofType(StackAdvisorRequest.StackAdvisorRequestType.CONFIGURATIONS).
          forHosts(Arrays.asList(new String[]{"hostName1"})).
          build();
      String firstResponse = command.getHostsInformation(request);
      assertEquals(String.format(SINGLE_HOST_RESPONSE, "hostName1"), firstResponse);
  
      String secondResponse = command.getHostsInformation(request);
      assertEquals(String.format(SINGLE_HOST_RESPONSE, "hostName1"), secondResponse);
    }
  
    /**
     * Try to retrieve multiple hosts info twice. The inner cache should be populated with first usage for first host (hostName1).
     * For the next usage with the both hosts handleRequest should be used for second host only.
     */
    @Test
    public void testHostInfoCachingTwoHost() throws StackAdvisorException {
      File file = mock(File.class);
      String recommendationsArtifactsLifetime = "1w";
      StackAdvisorRunner stackAdvisorRunner = mock(StackAdvisorRunner.class);
      AmbariMetaInfo ambariMetaInfo = mock(AmbariMetaInfo.class);
      Map<String, JsonNode> hostInfoCache = new HashMap<>();
      TestStackAdvisorCommand command = spy(new TestStackAdvisorCommand(file, recommendationsArtifactsLifetime,
          ServiceInfo.ServiceAdvisorType.PYTHON, 1,
          stackAdvisorRunner, ambariMetaInfo, hostInfoCache));
      doReturn(Response.status(200).entity(String.format(SINGLE_HOST_RESPONSE, "hostName1")).build())
          .doReturn(Response.status(200).entity(String.format(SINGLE_HOST_RESPONSE, "hostName2")).build())
          .doReturn(null)
          .when(command).handleRequest(
                  nullable(HttpHeaders.class),
                  nullable(String.class),
                  any(UriInfo.class),
                  any(Request.Type.class),
                  nullable(MediaType.class),
                  any(ResourceInstance.class)
          );
  
      StackAdvisorRequest request = StackAdvisorRequestBuilder.
          forStack(null, null).ofType(StackAdvisorRequest.StackAdvisorRequestType.CONFIGURATIONS).
          forHosts(Arrays.asList(new String[]{"hostName1"})).
          build();
      String firstResponse = command.getHostsInformation(request);
      assertEquals(String.format(SINGLE_HOST_RESPONSE, "hostName1"), firstResponse);
  
      request = StackAdvisorRequestBuilder.
          forStack(null, null).ofType(StackAdvisorRequest.StackAdvisorRequestType.CONFIGURATIONS).
          forHosts(Arrays.asList(new String[]{"hostName1", "hostName2"})).
          build();
      String secondResponse = command.getHostsInformation(request);
      assertEquals(String.format(TWO_HOST_RESPONSE, "hostName1", "hostName2"), secondResponse);
    }
  
    private static String jsonString(Object obj) throws IOException {
      return new ObjectMapper().writeValueAsString(obj);
    }
  
    private static JsonNode json(Object obj) throws IOException {
      return new ObjectMapper().convertValue(obj, JsonNode.class);
    }
  
    private static JsonNode json(String jsonString) throws IOException {
      return new ObjectMapper().readTree(jsonString);
    }
  
    private static List<Object> list(Object... items) {
      return Lists.newArrayList(items);
    }
  
    private static Map<String, Object> map(Object... keysAndValues) {
      Map<String, Object> map = new HashMap<>();
      Iterator<Object> iterator = Arrays.asList(keysAndValues).iterator();
      while (iterator.hasNext()) {
        map.put(iterator.next().toString(), iterator.next());
      }
      return map;
    }
  
    class TestStackAdvisorCommand extends StackAdvisorCommand<TestResource> {
      public TestStackAdvisorCommand(File recommendationsDir, String recommendationsArtifactsLifetime, ServiceInfo.ServiceAdvisorType serviceAdvisorType,
                                     int requestId, StackAdvisorRunner saRunner, AmbariMetaInfo metaInfo, Map<String, JsonNode> hostInfoCache) {
        super(recommendationsDir, recommendationsArtifactsLifetime, serviceAdvisorType, requestId, saRunner, metaInfo,
            ambariServerConfigurationHandler, hostInfoCache);
      }
  
      @Override
      protected void validate(StackAdvisorRequest request) throws StackAdvisorException {
        // do nothing
      }
  
      @Override
      protected String getResultFileName() {
        return "result.json";
      }
  
      @Override
      protected StackAdvisorCommandType getCommandType() {
        return StackAdvisorCommandType.RECOMMEND_COMPONENT_LAYOUT;
      }
  
      @Override
      protected TestResource updateResponse(StackAdvisorRequest request, TestResource response) {
        return response;
      }
  
      // Overridden to ensure visiblity in tests
      @Override
      public jakarta.ws.rs.core.Response handleRequest(HttpHeaders headers, String body,
                                                                    UriInfo uriInfo, Request.Type requestType,
                                                                    MediaType mediaType, ResourceInstance resource) {
        return super.handleRequest(headers, body, uriInfo, requestType, mediaType, resource);
      }
    }
  
    public static class TestResource extends StackAdvisorResponse {
      @JsonProperty
      private String type;
  
      public String getType() {
        return type;
      }
  
      public void setType(String type) {
        this.type = type;
      }
    }
  
  }
  