/*
 * 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.serveraction.kerberos;

import static org.easymock.EasyMock.anyBoolean;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.newCapture;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

import jakarta.persistence.EntityManager;

import org.apache.ambari.server.actionmanager.ActionDBAccessor;
import org.apache.ambari.server.actionmanager.ActionManager;
import org.apache.ambari.server.actionmanager.HostRoleCommandFactory;
import org.apache.ambari.server.actionmanager.RequestFactory;
import org.apache.ambari.server.actionmanager.StageFactory;
import org.apache.ambari.server.agent.CommandReport;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.audit.AuditLogger;
import org.apache.ambari.server.configuration.AmbariServerConfiguration;
import org.apache.ambari.server.controller.AbstractRootServiceResponseFactory;
import org.apache.ambari.server.controller.AmbariManagementController;
import org.apache.ambari.server.controller.KerberosHelper;
import org.apache.ambari.server.controller.KerberosHelperImpl;
import org.apache.ambari.server.controller.UpdateConfigurationPolicy;
import org.apache.ambari.server.events.AgentConfigsUpdateEvent;
import org.apache.ambari.server.hooks.HookContextFactory;
import org.apache.ambari.server.hooks.HookService;
import org.apache.ambari.server.ldap.service.AmbariLdapConfigurationProvider;
import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
import org.apache.ambari.server.mpack.MpackManagerFactory;
import org.apache.ambari.server.orm.dao.HostRoleCommandDAO;
import org.apache.ambari.server.orm.dao.KerberosKeytabPrincipalDAO;
import org.apache.ambari.server.scheduler.ExecutionScheduler;
import org.apache.ambari.server.security.encryption.CredentialStoreService;
import org.apache.ambari.server.security.encryption.Encryptor;
import org.apache.ambari.server.stack.StackManagerFactory;
import org.apache.ambari.server.stageplanner.RoleGraphFactory;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.ConfigFactory;
import org.apache.ambari.server.state.ConfigHelper;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.Service;
import org.apache.ambari.server.state.ServiceComponentFactory;
import org.apache.ambari.server.state.ServiceComponentHost;
import org.apache.ambari.server.state.ServiceComponentHostFactory;
import org.apache.ambari.server.state.configgroup.ConfigGroupFactory;
import org.apache.ambari.server.state.kerberos.KerberosComponentDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosDescriptorFactory;
import org.apache.ambari.server.state.kerberos.KerberosServiceDescriptor;
import org.apache.ambari.server.state.scheduler.RequestExecutionFactory;
import org.apache.ambari.server.state.stack.OsFamily;
import org.apache.ambari.server.topology.PersistedState;
import org.apache.ambari.server.topology.tasks.ConfigureClusterTaskFactory;
import org.easymock.Capture;
import org.easymock.CaptureType;
import org.easymock.EasyMockSupport;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;

public class AbstractPrepareKerberosServerActionTest extends EasyMockSupport {
  private static final String KERBEROS_DESCRIPTOR_JSON = "" +
      "{" +
      "  \"identities\": [" +
      "    {" +
      "      \"keytab\": {" +
      "        \"file\": \"${keytab_dir}/spnego.service.keytab\"," +
      "        \"group\": {" +
      "          \"access\": \"r\"," +
      "          \"name\": \"${cluster-env/user_group}\"" +
      "        }," +
      "        \"owner\": {" +
      "          \"access\": \"r\"," +
      "          \"name\": \"root\"" +
      "        }" +
      "      }," +
      "      \"name\": \"spnego\"," +
      "      \"principal\": {" +
      "        \"configuration\": null," +
      "        \"local_username\": null," +
      "        \"type\": \"service\"," +
      "        \"value\": \"HTTP/_HOST@${realm}\"" +
      "      }" +
      "    }" +
      "  ]," +
      "  \"services\": [" +
      "    {" +
      "      \"components\": [" +
      "        {" +
      "          \"identities\": [" +
      "            {" +
      "              \"name\": \"service_master_spnego_identity\"," +
      "              \"reference\": \"/spnego\"" +
      "            }" +
      "          ]," +
      "          \"name\": \"SERVICE_MASTER\"" +
      "        }" +
      "      ]," +
      "      \"configurations\": [" +
      "        {" +
      "          \"service-site\": {" +
      "            \"property1\": \"property1_updated_value\"," +
      "            \"property2\": \"property2_updated_value\"" +
      "          }" +
      "        }" +
      "      ]," +
      "      \"identities\": [" +
      "        {" +
      "          \"name\": \"service_identity\"," +
      "          \"keytab\": {" +
      "            \"configuration\": \"service-site/keytab_file_path\"," +
      "            \"file\": \"${keytab_dir}/service.service.keytab\"," +
      "            \"group\": {" +
      "              \"access\": \"r\"," +
      "              \"name\": \"${cluster-env/user_group}\"" +
      "            }," +
      "            \"owner\": {" +
      "              \"access\": \"r\"," +
      "              \"name\": \"${service-env/service_user}\"" +
      "            }" +
      "          }," +
      "          \"principal\": {" +
      "            \"configuration\": \"service-site/principal_name\"," +
      "            \"local_username\": \"${service-env/service_user}\"," +
      "            \"type\": \"service\"," +
      "            \"value\": \"${service-env/service_user}/_HOST@${realm}\"" +
      "          }" +
      "        }" +
      "      ]," +
      "      \"name\": \"SERVICE\"" +
      "    }" +
      "  ]," +
      "  \"properties\": {" +
      "    \"additional_realms\": \"\"," +
      "    \"keytab_dir\": \"/etc/security/keytabs\"," +
      "    \"principal_suffix\": \"-${cluster_name|toLower()}\"," +
      "    \"realm\": \"${kerberos-env/realm}\"" +
      "  }" +
      "}";

  private class TestKerberosServerAction extends AbstractPrepareKerberosServerAction {

    @Override
    protected String getClusterName() {
      return "c1";
    }

    @Override
    public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) {
      return null;
    }
  }

  private Injector injector;
  private final AbstractPrepareKerberosServerAction testKerberosServerAction = new TestKerberosServerAction();

  @Before
  public void setUp() throws Exception {
    injector = Guice.createInjector(new AbstractModule() {
      @Override
      protected void configure() {
        bind(AmbariMetaInfo.class).toInstance(createNiceMock(AmbariMetaInfo.class));
        bind(KerberosHelper.class).to(KerberosHelperImpl.class);
        bind(KerberosIdentityDataFileWriterFactory.class).toInstance(createNiceMock(KerberosIdentityDataFileWriterFactory.class));
        bind(KerberosConfigDataFileWriterFactory.class).toInstance(createNiceMock(KerberosConfigDataFileWriterFactory.class));
        bind(Clusters.class).toInstance(createNiceMock(Clusters.class));
        bind(AuditLogger.class).toInstance(createNiceMock(AuditLogger.class));
        bind(ConfigHelper.class).toInstance(createNiceMock(ConfigHelper.class));
        bind(HostRoleCommandDAO.class).toInstance(createNiceMock(HostRoleCommandDAO.class));
        bind(ActionManager.class).toInstance(createNiceMock(ActionManager.class));
        bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
        bind(ExecutionScheduler.class).toInstance(createNiceMock(ExecutionScheduler.class));
        bind(AmbariManagementController.class).toInstance(createNiceMock(AmbariManagementController.class));
        bind(ActionDBAccessor.class).toInstance(createNiceMock(ActionDBAccessor.class));
        bind(StackManagerFactory.class).toInstance(createNiceMock(StackManagerFactory.class));
        bind(ConfigFactory.class).toInstance(createNiceMock(ConfigFactory.class));
        bind(ConfigGroupFactory.class).toInstance(createNiceMock(ConfigGroupFactory.class));
        bind(CredentialStoreService.class).toInstance(createNiceMock(CredentialStoreService.class));
        bind(RequestExecutionFactory.class).toInstance(createNiceMock(RequestExecutionFactory.class));
        bind(RequestFactory.class).toInstance(createNiceMock(RequestFactory.class));
        bind(RoleCommandOrderProvider.class).toInstance(createNiceMock(RoleCommandOrderProvider.class));
        bind(RoleGraphFactory.class).toInstance(createNiceMock(RoleGraphFactory.class));
        bind(AbstractRootServiceResponseFactory.class).toInstance(createNiceMock(AbstractRootServiceResponseFactory.class));
        bind(ServiceComponentFactory.class).toInstance(createNiceMock(ServiceComponentFactory.class));
        bind(ServiceComponentHostFactory.class).toInstance(createNiceMock(ServiceComponentHostFactory.class));
        bind(StageFactory.class).toInstance(createNiceMock(StageFactory.class));
        bind(HostRoleCommandFactory.class).toInstance(createNiceMock(HostRoleCommandFactory.class));
        bind(HookContextFactory.class).toInstance(createNiceMock(HookContextFactory.class));
        bind(HookService.class).toInstance(createNiceMock(HookService.class));
        bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
        bind(PersistedState.class).toInstance(createNiceMock(PersistedState.class));
        bind(ConfigureClusterTaskFactory.class).toInstance(createNiceMock(ConfigureClusterTaskFactory.class));
        Provider<EntityManager> entityManagerProvider = createNiceMock(Provider.class);
        bind(EntityManager.class).toProvider(entityManagerProvider);
        bind(MpackManagerFactory.class).toInstance(createNiceMock(MpackManagerFactory.class));
        bind(AmbariLdapConfigurationProvider.class).toInstance(createMock(AmbariLdapConfigurationProvider.class));
        bind(KerberosKeytabPrincipalDAO.class).toInstance(createNiceMock(KerberosKeytabPrincipalDAO.class));
        bind(new TypeLiteral<Encryptor<AgentConfigsUpdateEvent>>() {}).annotatedWith(Names.named("AgentConfigEncryptor")).toInstance(Encryptor.NONE);
        bind(new TypeLiteral<Encryptor<AmbariServerConfiguration>>() {}).annotatedWith(Names.named("AmbariServerConfigurationEncryptor")).toInstance(Encryptor.NONE);
      }
    });

    injector.injectMembers(testKerberosServerAction);
  }

  /**
   * Test checks that {@code KerberosHelper.applyStackAdvisorUpdates} would be called with
   * full list of the services and not only list of services with KerberosDescriptior.
   * In this test HDFS service will have KerberosDescriptor, while Zookeeper not.
   *
   * @throws Exception
   */
  @Test
  @SuppressWarnings("unchecked")
  public void testProcessServiceComponentHosts() throws Exception {
    final Cluster cluster = createNiceMock(Cluster.class);
    final KerberosIdentityDataFileWriter kerberosIdentityDataFileWriter = createNiceMock(KerberosIdentityDataFileWriter.class);
    final KerberosDescriptor kerberosDescriptor = createNiceMock(KerberosDescriptor.class);
    final ServiceComponentHost serviceComponentHostHDFS = createNiceMock(ServiceComponentHost.class);
    final ServiceComponentHost serviceComponentHostZK = createNiceMock(ServiceComponentHost.class);
    final KerberosServiceDescriptor serviceDescriptor = createNiceMock(KerberosServiceDescriptor.class);
    final KerberosComponentDescriptor componentDescriptor = createNiceMock(KerberosComponentDescriptor.class);

    final String hdfsService = "HDFS";
    final String zookeeperService = "ZOOKEEPER";
    final String hostName = "host1";
    final String hdfsComponent = "DATANODE";
    final String zkComponent = "ZK";

    Collection<String> identityFilter = new ArrayList<>();
    Map<String, Map<String, String>> kerberosConfigurations = new HashMap<>();
    Map<String, Set<String>> propertiesToIgnore = new HashMap<>();
    Map<String, Map<String, String>> configurations = new HashMap<>();

    List<ServiceComponentHost> serviceComponentHosts = new ArrayList<ServiceComponentHost>() {{
      add(serviceComponentHostHDFS);
      add(serviceComponentHostZK);
    }};
    Map<String, Service> clusterServices = new HashMap<String, Service>() {{
      put(hdfsService, null);
      put(zookeeperService, null);
    }};

    KerberosIdentityDataFileWriterFactory kerberosIdentityDataFileWriterFactory = injector.getInstance(KerberosIdentityDataFileWriterFactory.class);
    expect(kerberosIdentityDataFileWriterFactory.createKerberosIdentityDataFileWriter(anyObject(File.class))).andReturn(kerberosIdentityDataFileWriter);

    // it's important to pass a copy of clusterServices
    expect(cluster.getServices()).andReturn(new HashMap<>(clusterServices)).atLeastOnce();

    expect(serviceComponentHostHDFS.getHostName()).andReturn(hostName).atLeastOnce();
    expect(serviceComponentHostHDFS.getServiceName()).andReturn(hdfsService).atLeastOnce();
    expect(serviceComponentHostHDFS.getServiceComponentName()).andReturn(hdfsComponent).atLeastOnce();
    expect(serviceComponentHostHDFS.getHost()).andReturn(createNiceMock(Host.class)).atLeastOnce();

    expect(serviceComponentHostZK.getHostName()).andReturn(hostName).atLeastOnce();
    expect(serviceComponentHostZK.getServiceName()).andReturn(zookeeperService).atLeastOnce();
    expect(serviceComponentHostZK.getServiceComponentName()).andReturn(zkComponent).atLeastOnce();
    expect(serviceComponentHostZK.getHost()).andReturn(createNiceMock(Host.class)).atLeastOnce();

    expect(kerberosDescriptor.getService(hdfsService)).andReturn(serviceDescriptor).once();

    expect(serviceDescriptor.getComponent(hdfsComponent)).andReturn(componentDescriptor).once();
    expect(componentDescriptor.getConfigurations(anyBoolean())).andReturn(null);

    replayAll();

    injector.getInstance(AmbariMetaInfo.class).init();

    testKerberosServerAction.processServiceComponentHosts(cluster,
        kerberosDescriptor,
        serviceComponentHosts,
        identityFilter,
        "",
        configurations, kerberosConfigurations,
        false, propertiesToIgnore);

    verifyAll();

    // Ensure the host and hostname values were set in the configuration context
    Assert.assertEquals("host1", configurations.get("").get("host"));
    Assert.assertEquals("host1", configurations.get("").get("hostname"));
  }

  @Test
  public void testProcessConfigurationChanges() throws Exception {
    // Existing property map....
    Map<String, String> serviceSiteProperties = new HashMap<>();
    serviceSiteProperties.put("property1", "property1_value");
    serviceSiteProperties.put("principal_name", "principal_name_value");
    serviceSiteProperties.put("keytab_file_path", "keytab_file_path_value");

    Map<String, Map<String, String>> effectiveProperties = new HashMap<>();
    effectiveProperties.put("service-site", serviceSiteProperties);

    // Updated property map....
    Map<String, String> updatedServiceSiteProperties = new HashMap<>();
    updatedServiceSiteProperties.put("property1", "property1_updated_value");
    updatedServiceSiteProperties.put("property2", "property2_updated_value");
    updatedServiceSiteProperties.put("principal_name", "principal_name_updated_value");
    updatedServiceSiteProperties.put("keytab_file_path", "keytab_file_path_updated_value");

    Map<String, Map<String, String>> kerberosConfigurations = new HashMap<>();
    kerberosConfigurations.put("service-site", updatedServiceSiteProperties);

    KerberosDescriptor kerberosDescriptor = new KerberosDescriptorFactory().createInstance(KERBEROS_DESCRIPTOR_JSON);

    ConfigHelper configHelper = injector.getInstance(ConfigHelper.class);
    expect(configHelper.getEffectiveConfigProperties(eq("c1"), eq(null)))
        .andReturn(effectiveProperties).anyTimes();

    KerberosConfigDataFileWriterFactory factory = injector.getInstance(KerberosConfigDataFileWriterFactory.class);

    ConfigWriterData dataCaptureAll = setupConfigWriter(factory);
    ConfigWriterData dataCaptureIdentitiesOnly = setupConfigWriter(factory);
    ConfigWriterData dataCaptureNewAndIdentities = setupConfigWriter(factory);
    ConfigWriterData dataCaptureNone = setupConfigWriter(factory);

    replayAll();

    injector.getInstance(AmbariMetaInfo.class).init();

    Map<String, String> expectedProperties;

    // Update all configurations
    testKerberosServerAction.processConfigurationChanges("test_directory",
        kerberosConfigurations, Collections.emptyMap(), kerberosDescriptor, UpdateConfigurationPolicy.ALL);

    expectedProperties = new HashMap<>();
    expectedProperties.put("property1", "property1_updated_value");
    expectedProperties.put("property2", "property2_updated_value");
    expectedProperties.put("principal_name", "principal_name_updated_value");
    expectedProperties.put("keytab_file_path", "keytab_file_path_updated_value");

    verifyDataCapture(dataCaptureAll, Collections.singletonMap("service-site", expectedProperties));

    // Update only identity configurations
    testKerberosServerAction.processConfigurationChanges("test_directory",
        kerberosConfigurations, Collections.emptyMap(), kerberosDescriptor, UpdateConfigurationPolicy.IDENTITIES_ONLY);

    expectedProperties = new HashMap<>();
    expectedProperties.put("principal_name", "principal_name_updated_value");
    expectedProperties.put("keytab_file_path", "keytab_file_path_updated_value");

    verifyDataCapture(dataCaptureIdentitiesOnly, Collections.singletonMap("service-site", expectedProperties));

    // Update new and identity configurations
    testKerberosServerAction.processConfigurationChanges("test_directory",
        kerberosConfigurations, Collections.emptyMap(), kerberosDescriptor, UpdateConfigurationPolicy.NEW_AND_IDENTITIES);

    expectedProperties = new HashMap<>();
    expectedProperties.put("property2", "property2_updated_value");
    expectedProperties.put("principal_name", "principal_name_updated_value");
    expectedProperties.put("keytab_file_path", "keytab_file_path_updated_value");

    verifyDataCapture(dataCaptureNewAndIdentities, Collections.singletonMap("service-site", expectedProperties));

    // Update no configurations
    testKerberosServerAction.processConfigurationChanges("test_directory",
        kerberosConfigurations, Collections.emptyMap(), kerberosDescriptor, UpdateConfigurationPolicy.NONE);

    verifyDataCapture(dataCaptureNone, Collections.emptyMap());

    verifyAll();

  }

  private void verifyDataCapture(ConfigWriterData configWriterData, Map<String, Map<String, String>> expectedConfigurations) {

    int expectedCaptures = 0;
    Collection<Map<String, String>> expectedValuesCollection = expectedConfigurations.values();
    for (Map<String, String> expectedValues : expectedValuesCollection) {
      expectedCaptures += expectedValues.size();
    }

    Capture<String> captureConfigType = configWriterData.getCaptureConfigType();
    if (expectedCaptures > 0) {
      Assert.assertTrue(captureConfigType.hasCaptured());
      List<String> valuesConfigType = captureConfigType.getValues();
      Assert.assertEquals(expectedCaptures, valuesConfigType.size());
    } else {
      Assert.assertFalse(captureConfigType.hasCaptured());
    }

    Capture<String> capturePropertyName = configWriterData.getCapturePropertyName();
    if (expectedCaptures > 0) {
      Assert.assertTrue(capturePropertyName.hasCaptured());
      List<String> valuesPropertyName = capturePropertyName.getValues();
      Assert.assertEquals(expectedCaptures, valuesPropertyName.size());
    } else {
      Assert.assertFalse(capturePropertyName.hasCaptured());
    }

    Capture<String> capturePropertyValue = configWriterData.getCapturePropertyValue();
    if (expectedCaptures > 0) {
      Assert.assertTrue(capturePropertyValue.hasCaptured());
      List<String> valuesPropertyValue = capturePropertyValue.getValues();
      Assert.assertEquals(expectedCaptures, valuesPropertyValue.size());
    } else {
      Assert.assertFalse(capturePropertyValue.hasCaptured());
    }

    if (expectedCaptures > 0) {
      int i = 0;
      List<String> valuesConfigType = captureConfigType.getValues();
      List<String> valuesPropertyName = capturePropertyName.getValues();
      List<String> valuesPropertyValue = capturePropertyValue.getValues();

      for (Map.Entry<String, Map<String, String>> entry : expectedConfigurations.entrySet()) {
        String configType = entry.getKey();
        Map<String, String> properties = entry.getValue();

        for(Map.Entry<String, String> property:properties.entrySet()) {
          Assert.assertEquals(configType, valuesConfigType.get(i));
          Assert.assertEquals(property.getKey(), valuesPropertyName.get(i));
          Assert.assertEquals(property.getValue(), valuesPropertyValue.get(i));
          i++;
        }
      }
    }

  }

  private ConfigWriterData setupConfigWriter(KerberosConfigDataFileWriterFactory factory) throws IOException {
    Capture<String> captureConfigType = newCapture(CaptureType.ALL);
    Capture<String> capturePropertyName = newCapture(CaptureType.ALL);
    Capture<String> capturePropertyValue = newCapture(CaptureType.ALL);

    KerberosConfigDataFileWriter mockWriter = createMock(KerberosConfigDataFileWriter.class);
    mockWriter.addRecord(capture(captureConfigType), capture(capturePropertyName), capture(capturePropertyValue), eq(KerberosConfigDataFileWriter.OPERATION_TYPE_SET));
    expectLastCall().anyTimes();
    mockWriter.close();
    expectLastCall().anyTimes();

    expect(factory.createKerberosConfigDataFileWriter(anyObject(File.class))).andReturn(mockWriter).once();

    return new ConfigWriterData(captureConfigType, capturePropertyName, capturePropertyValue);
  }

  private class ConfigWriterData {
    private final Capture<String> captureConfigType;
    private final Capture<String> capturePropertyName;
    private final Capture<String> capturePropertyValue;

    private ConfigWriterData(Capture<String> captureConfigType, Capture<String> capturePropertyName, Capture<String> capturePropertyValue) {
      this.captureConfigType = captureConfigType;
      this.capturePropertyName = capturePropertyName;
      this.capturePropertyValue = capturePropertyValue;
    }

    Capture<String> getCaptureConfigType() {
      return captureConfigType;
    }

    Capture<String> getCapturePropertyName() {
      return capturePropertyName;
    }

    Capture<String> getCapturePropertyValue() {
      return capturePropertyValue;
    }
  }
}
