/**
 * 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.hadoop.security;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.hadoop.conf.Configurable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class TestCompositeGroupMapping {
  public static final Logger LOG =
      LoggerFactory.getLogger(TestCompositeGroupMapping.class);
  private static Configuration conf = new Configuration();
  
  private static class TestUser {
    String name;
    String group;
    String group2;
    
    public TestUser(String name, String group) {
      this.name = name;
      this.group = group;
    }

    public TestUser(String name, String group, String group2) {
      this(name, group);
      this.group2 = group2;
    }
  };
  
  private static TestUser john = new TestUser("John", "user-group");
  private static TestUser hdfs = new TestUser("hdfs", "supergroup");
  private static TestUser jack = new TestUser("Jack", "user-group", "dev-group-1");
  
  private static final String PROVIDER_SPECIFIC_CONF = ".test.prop";
  private static final String PROVIDER_SPECIFIC_CONF_KEY = 
      GroupMappingServiceProvider.GROUP_MAPPING_CONFIG_PREFIX + PROVIDER_SPECIFIC_CONF;
  private static final String PROVIDER_SPECIFIC_CONF_VALUE_FOR_USER = "value-for-user";
  private static final String PROVIDER_SPECIFIC_CONF_VALUE_FOR_CLUSTER = "value-for-cluster";
  
  private static abstract class GroupMappingProviderBase 
    implements GroupMappingServiceProvider, Configurable {
    
    private Configuration conf;
    
    @Override
    public void setConf(Configuration conf) {
      this.conf = conf;
    }

    @Override
    public Configuration getConf() {
      return this.conf;
    }

    @Override
    public void cacheGroupsRefresh() throws IOException {
      
    }

    @Override
    public void cacheGroupsAdd(List<String> groups) throws IOException {
      
    }

    protected List<String> toList(String group) {
      if (group != null) {
        return Arrays.asList(new String[] {group});
      }
      return new ArrayList<String>();
    }

    protected Set<String> toSet(String group) {
      if (group != null) {
        Set<String> result = new HashSet<>();
        result.add(group);
        return result;
      }
      return new HashSet<String>();
    }
    
    protected void checkTestConf(String expectedValue) {
      String configValue = getConf().get(PROVIDER_SPECIFIC_CONF_KEY);
      if (configValue == null || !configValue.equals(expectedValue)) {
        throw new RuntimeException("Failed to find mandatory configuration of " + PROVIDER_SPECIFIC_CONF_KEY);
      }
    }
  };
  
  private static class UserProvider extends GroupMappingProviderBase {
    @Override
    public List<String> getGroups(String user) throws IOException {
      return toList(getGroupInternal(user));
    }

    @Override
    public Set<String> getGroupsSet(String user) throws IOException {
      return toSet(getGroupInternal(user));
    }

    private String getGroupInternal(String user) throws IOException {
      checkTestConf(PROVIDER_SPECIFIC_CONF_VALUE_FOR_USER);

      String group = null;
      if (user.equals(john.name)) {
        group = john.group;
      } else if (user.equals(jack.name)) {
        group = jack.group;
      }
      return group;
    }
  }
  
  private static class ClusterProvider extends GroupMappingProviderBase {    
    @Override
    public List<String> getGroups(String user) throws IOException {
      return toList(getGroupsInternal(user));
    }

    @Override
    public Set<String> getGroupsSet(String user) throws IOException {
      return toSet(getGroupsInternal(user));
    }

    private String getGroupsInternal(String user) throws IOException {
      checkTestConf(PROVIDER_SPECIFIC_CONF_VALUE_FOR_CLUSTER);

      String group = null;
      if (user.equals(hdfs.name)) {
        group = hdfs.group;
      } else if (user.equals(jack.name)) { // jack has another group from clusterProvider
        group = jack.group2;
      }
      return group;

    }
  }
  
  static {
    conf.setClass(CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING,
      CompositeGroupsMapping.class, GroupMappingServiceProvider.class);
    conf.set(CompositeGroupsMapping.MAPPING_PROVIDERS_CONFIG_KEY, "userProvider,clusterProvider");

    conf.setClass(CompositeGroupsMapping.MAPPING_PROVIDER_CONFIG_PREFIX + ".userProvider", 
        UserProvider.class, GroupMappingServiceProvider.class);

    conf.setClass(CompositeGroupsMapping.MAPPING_PROVIDER_CONFIG_PREFIX + ".clusterProvider", 
        ClusterProvider.class, GroupMappingServiceProvider.class);

    conf.set(CompositeGroupsMapping.MAPPING_PROVIDER_CONFIG_PREFIX + 
        ".clusterProvider" + PROVIDER_SPECIFIC_CONF, PROVIDER_SPECIFIC_CONF_VALUE_FOR_CLUSTER);

    conf.set(CompositeGroupsMapping.MAPPING_PROVIDER_CONFIG_PREFIX + 
        ".userProvider" + PROVIDER_SPECIFIC_CONF, PROVIDER_SPECIFIC_CONF_VALUE_FOR_USER);
  }

  @Test
  public void TestMultipleGroupsMapping() throws Exception {
    Groups groups = new Groups(conf);

    assertTrue(groups.getGroups(john.name).get(0).equals(john.group));
    assertTrue(groups.getGroups(hdfs.name).get(0).equals(hdfs.group));
  }

  @Test
  public void TestMultipleGroupsMappingWithCombined() throws Exception {
    conf.set(CompositeGroupsMapping.MAPPING_PROVIDERS_COMBINED_CONFIG_KEY, "true");
    Groups groups = new Groups(conf);

    assertTrue(groups.getGroups(jack.name).size() == 2);
    // the configured providers list in order is "userProvider,clusterProvider"
    // group -> userProvider, group2 -> clusterProvider
    assertTrue(groups.getGroups(jack.name).contains(jack.group));
    assertTrue(groups.getGroups(jack.name).contains(jack.group2));
  }

  @Test
  public void TestMultipleGroupsMappingWithoutCombined() throws Exception {
    conf.set(CompositeGroupsMapping.MAPPING_PROVIDERS_COMBINED_CONFIG_KEY, "false");
    Groups groups = new Groups(conf);

    // the configured providers list in order is "userProvider,clusterProvider"
    // group -> userProvider, group2 -> clusterProvider
    assertTrue(groups.getGroups(jack.name).size() == 1);
    assertTrue(groups.getGroups(jack.name).get(0).equals(jack.group));
  }
}
