/*
 * 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.gobblin.config.client;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.net.URI;
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 org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValue;

import org.apache.gobblin.config.client.api.VersionStabilityPolicy;
import org.apache.gobblin.config.common.impl.SingleLinkedListConfigKeyPath;
import org.apache.gobblin.config.store.api.ConfigKeyPath;
import org.apache.gobblin.config.store.api.ConfigStore;
import org.apache.gobblin.config.store.api.ConfigStoreFactory;

@Test(groups = { "gobblin.config.common.impl" })

public class TestConfigClient {
  private ConfigStore mockConfigStore;
  private final String version = "V1.0";
  private final ConfigKeyPath data = SingleLinkedListConfigKeyPath.ROOT.createChild("data");
  private final ConfigKeyPath tag = SingleLinkedListConfigKeyPath.ROOT.createChild("tag");
  private final ConfigKeyPath tag2 = SingleLinkedListConfigKeyPath.ROOT.createChild("tag2");

  private final ConfigKeyPath databases = data.createChild("databases");
  private final ConfigKeyPath identity = databases.createChild("identity");

  private final ConfigKeyPath highPriorityTag = tag.createChild("highPriorityTag");
  private final ConfigKeyPath espressoTag = tag.createChild("espressoTag");

  private final ConfigKeyPath nertzTag2 = tag2.createChild("nertzTag2");

  private void printConfig(Config config){
    Set<Map.Entry<String,ConfigValue>> entrySet = config.entrySet();
    for(Map.Entry<String,ConfigValue> entry: entrySet){
      System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
    }
  }

  @BeforeClass
  public void setup(){
    // Topology for mock up config store

    //    ├── data
    //    │   └── databases
    //    │       └── identity
    //    ├── tag
    //    │   ├── espressoTag
    //    │   └── highPriorityTag
    //    └── tag2
    //        └── nertzTag2


    mockConfigStore = mock(ConfigStore.class, Mockito.RETURNS_SMART_NULLS);

    when(mockConfigStore.getCurrentVersion()).thenReturn(version);

    List<ConfigKeyPath> emptyList = Collections.emptyList();
    // mock up parent/children topology
    List<ConfigKeyPath> rootChildren = new ArrayList<ConfigKeyPath>();
    rootChildren.add(data);
    rootChildren.add(tag);
    rootChildren.add(tag2);
    when(mockConfigStore.getChildren(SingleLinkedListConfigKeyPath.ROOT, version)).thenReturn(rootChildren);

    List<ConfigKeyPath> dataChildren = new ArrayList<ConfigKeyPath>();
    dataChildren.add(databases);
    when(mockConfigStore.getChildren(data, version)).thenReturn(dataChildren);

    List<ConfigKeyPath> databasesChildren = new ArrayList<ConfigKeyPath>();
    databasesChildren.add(identity);
    when(mockConfigStore.getChildren(databases, version)).thenReturn(databasesChildren);

    when(mockConfigStore.getChildren(identity, version)).thenReturn(emptyList);

    List<ConfigKeyPath> tagChildren = new ArrayList<ConfigKeyPath>();
    tagChildren.add(highPriorityTag);
    tagChildren.add(espressoTag);
    when(mockConfigStore.getChildren(tag, version)).thenReturn(tagChildren);

    when(mockConfigStore.getChildren(highPriorityTag, version)).thenReturn(emptyList);
    when(mockConfigStore.getChildren(espressoTag, version)).thenReturn(emptyList);

    List<ConfigKeyPath> tag2Children = new ArrayList<ConfigKeyPath>();
    tag2Children.add(nertzTag2);
    when(mockConfigStore.getChildren(tag2, version)).thenReturn(tag2Children);

    when(mockConfigStore.getChildren(nertzTag2, version)).thenReturn(emptyList);

    // mock up import links
    // identity import espressoTag and highPriorityTag
    List<ConfigKeyPath> identityImports = new ArrayList<ConfigKeyPath>();
    identityImports.add(espressoTag);
    identityImports.add(highPriorityTag);
    when(mockConfigStore.getOwnImports(identity, version)).thenReturn(identityImports);

    // espressoTag imports nertzTag2
    List<ConfigKeyPath> espressoImports = new ArrayList<ConfigKeyPath>();
    espressoImports.add(nertzTag2);
    when(mockConfigStore.getOwnImports(espressoTag, version)).thenReturn(espressoImports);

    mockupConfigValues();
  }

  private void mockupConfigValues(){
    /**
     * each node will have a common key "generalKey" with value as "generalValue_${node}"
     * this key will be overwrite
     *
     * each node will have own key "keyOf_${node}" with value "valueOf_${node}"
     * this key will be inherent
     */
    // mock up the configuration values for root
    Map<String, String> rootMap = new HashMap<>();
    rootMap.put("keyOf_Root", "valueOf_Root");
    rootMap.put("generalKey", "generalValue_root"); // keys will be overwrite
    when(mockConfigStore.getOwnConfig(SingleLinkedListConfigKeyPath.ROOT, version)).thenReturn(ConfigFactory.parseMap(rootMap));

    // mock up the configuration values for /data
    Map<String, String> dataMap = new HashMap<>();
    dataMap.put("keyOf_data", "valueOf_data");
    dataMap.put("generalKey", "generalValue_data");
    when(mockConfigStore.getOwnConfig(data, version)).thenReturn(ConfigFactory.parseMap(dataMap));

    // mock up the configuration values for /data/databases
    Map<String, String> databasesMap = new HashMap<>();
    databasesMap.put("keyOf_databases", "valueOf_databases");
    databasesMap.put("generalKey", "generalValue_data_databases");
    when(mockConfigStore.getOwnConfig(databases, version)).thenReturn(ConfigFactory.parseMap(databasesMap));

    // mock up the configuration values for /data/databases/identity
    Map<String, String> identityMap = new HashMap<>();
    identityMap.put("keyOf_identity", "valueOf_identity");
    identityMap.put("generalKey", "generalValue_data_databases_identity");
    when(mockConfigStore.getOwnConfig(identity, version)).thenReturn(ConfigFactory.parseMap(identityMap));

    // mock up the configuration values for /tag
    Map<String, String> tagMap = new HashMap<>();
    tagMap.put("keyOf_tag", "valueOf_tag");
    tagMap.put("generalKey", "generalValue_tag");
    when(mockConfigStore.getOwnConfig(tag, version)).thenReturn(ConfigFactory.parseMap(tagMap));

    // mock up the configuration values for /tag/espressoTag
    Map<String, String> espressoTagMap = new HashMap<>();
    espressoTagMap.put("keyOf_espressoTag", "valueOf_espressoTag");
    espressoTagMap.put("generalKey", "generalValue_tag_espressoTag");
    when(mockConfigStore.getOwnConfig(espressoTag, version)).thenReturn(ConfigFactory.parseMap(espressoTagMap));

    // mock up the configuration values for /tag/highPriorityTag
    Map<String, String> highPriorityTagMap = new HashMap<>();
    highPriorityTagMap.put("keyOf_highPriorityTag", "valueOf_highPriorityTag");
    highPriorityTagMap.put("generalKey", "generalValue_tag_highPriorityTag");
    when(mockConfigStore.getOwnConfig(highPriorityTag, version)).thenReturn(ConfigFactory.parseMap(highPriorityTagMap));

    // mock up the configuration values for /tag2
    Map<String, String> tag2Map = new HashMap<>();
    tag2Map.put("keyOf_tag2", "valueOf_tag2");
    tag2Map.put("generalKey", "generalValue_tag2");
    when(mockConfigStore.getOwnConfig(tag2, version)).thenReturn(ConfigFactory.parseMap(tag2Map));

    // mock up the configuration values for /tag2/nertzTag2
    Map<String, String> nertzTag2Map = new HashMap<>();
    nertzTag2Map.put("keyOf_nertzTag2", "valueOf_nertzTag2");
    nertzTag2Map.put("generalKey", "generalValue_tag2_nertzTag2");
    when(mockConfigStore.getOwnConfig(nertzTag2, version)).thenReturn(ConfigFactory.parseMap(nertzTag2Map));
  }

  private void checkValuesForIdentity(Config resolvedConfig){
    Assert.assertTrue(resolvedConfig.getString("keyOf_data").equals("valueOf_data"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_identity").equals("valueOf_identity"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_espressoTag").equals("valueOf_espressoTag"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_Root").equals("valueOf_Root"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_nertzTag2").equals("valueOf_nertzTag2"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_highPriorityTag").equals("valueOf_highPriorityTag"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_tag2").equals("valueOf_tag2"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_tag").equals("valueOf_tag"));
    Assert.assertTrue(resolvedConfig.getString("keyOf_databases").equals("valueOf_databases"));

    Assert.assertTrue(resolvedConfig.getString("generalKey").equals("generalValue_data_databases_identity"));
  }

  @Test
  private void testFromClient() throws Exception{
    ConfigStoreFactoryRegister mockConfigStoreFactoryRegister;
    ConfigStoreFactory mockConfigStoreFactory;

    URI relativeURI = new URI("etl-hdfs:///data/databases/identity");
    URI absoluteURI = new URI("etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/data/databases/identity");
    when(mockConfigStore.getStoreURI()).thenReturn(new URI("etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest"));

    mockConfigStoreFactory = mock(ConfigStoreFactory.class, Mockito.RETURNS_SMART_NULLS);
    when(mockConfigStoreFactory.getScheme()).thenReturn("etl-hdfs");
    when(mockConfigStoreFactory.createConfigStore(absoluteURI)).thenReturn(mockConfigStore);
    when(mockConfigStoreFactory.createConfigStore(relativeURI)).thenReturn(mockConfigStore);

    mockConfigStoreFactoryRegister = mock(ConfigStoreFactoryRegister.class, Mockito.RETURNS_SMART_NULLS);
    when(mockConfigStoreFactoryRegister.getConfigStoreFactory("etl-hdfs")).thenReturn(mockConfigStoreFactory);

    ConfigClient client = new ConfigClient(VersionStabilityPolicy.STRONG_LOCAL_STABILITY, mockConfigStoreFactoryRegister);
    Config resolved = client.getConfig(relativeURI);
    checkValuesForIdentity(resolved);

    resolved = client.getConfig(absoluteURI);
    checkValuesForIdentity(resolved);

    // importedBy using relative URI
    String[] expectedImportedBy = {"etl-hdfs:/tag/espressoTag", "etl-hdfs:/data/databases/identity"};
    URI nertzTagURI = new URI("etl-hdfs:///tag2/nertzTag2");
    Collection<URI> importedBy = client.getImportedBy(nertzTagURI, false);
    Assert.assertEquals(importedBy.size(), 1);
    Assert.assertEquals(importedBy.iterator().next().toString(), expectedImportedBy[0]);

    importedBy = client.getImportedBy(nertzTagURI, true);
    Assert.assertEquals(importedBy.size(), 2);
    for(URI u: importedBy){
      Assert.assertTrue(u.toString().equals(expectedImportedBy[0]) ||
          u.toString().equals(expectedImportedBy[1]));
    }

    // importedBy using abs URI
    String[] expectedImportedBy_abs = {"etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/tag/espressoTag",
        "etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/data/databases/identity"};
    nertzTagURI = new URI("etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/tag2/nertzTag2");
    importedBy = client.getImportedBy(nertzTagURI, false);
    Assert.assertEquals(importedBy.size(), 1);
    Assert.assertEquals(importedBy.iterator().next().toString(), expectedImportedBy_abs[0]);

    importedBy = client.getImportedBy(nertzTagURI, true);
    Assert.assertEquals(importedBy.size(), 2);
    for(URI u: importedBy){
      Assert.assertTrue(u.toString().equals(expectedImportedBy_abs[0]) ||
          u.toString().equals(expectedImportedBy_abs[1]));
    }
  }
}
