/**
 * 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.hive.ql.session;

import static org.junit.Assert.assertEquals;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.conf.HiveConf;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.apache.hadoop.hive.ql.session.SessionState.ResourceType;
import org.apache.hadoop.hive.ql.session.SessionState;
import org.apache.hadoop.util.Shell;

import java.io.BufferedWriter;
import java.io.FileWriter;


public class TestAddResource {

  private static final String TEST_JAR_DIR = System.getProperty("test.tmp.dir", ".") + File.pathSeparator;
  private HiveConf conf;
  private ResourceType t;

  @Before
  public void setup() throws IOException {
    conf = new HiveConf();
    t = ResourceType.JAR;

    //Generate test jar files
    for (int i = 1; i <= 5; i++) {
      Writer output = null;
      String dataFile = TEST_JAR_DIR + "testjar" + i + ".jar";
      File file = new File(dataFile);
      output = new BufferedWriter(new FileWriter(file));
      output.write("sample");
      output.close();
    }
  }

  // Check that all the jars are added to the classpath
  @Test
  public void testSanity() throws URISyntaxException, IOException {
    SessionState ss = Mockito.spy(SessionState.start(conf).get());
    String query = "testQuery";

    // add all the dependencies to a list
    List<URI> list = new LinkedList<URI>();
    List<String> addList = new LinkedList<String>();
    list.add(createURI(TEST_JAR_DIR + "testjar1.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar2.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar5.jar"));

    //return all the dependency urls
    Mockito.when(ss.resolveAndDownload(query, false)).thenReturn(list);
    addList.add(query);
    ss.add_resources(t, addList);
    Set<String> dependencies = ss.list_resource(t, null);
    LinkedList<URI> actual = new LinkedList<URI>();
    for (String dependency : dependencies) {
      actual.add(createURI(dependency));
    }

    // sort both the lists
    Collections.sort(list);
    Collections.sort(actual);

    assertEquals(list, actual);
    ss.close();

  }

  // add same jar multiple times and check that dependencies are added only once.
  @Test
  public void testDuplicateAdds() throws URISyntaxException, IOException {

    SessionState ss = Mockito.spy(SessionState.start(conf).get());

    String query = "testQuery";

    List<URI> list = new LinkedList<URI>();
    List<String> addList = new LinkedList<String>();
    list.add(createURI(TEST_JAR_DIR + "testjar1.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar2.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list.add(createURI(TEST_JAR_DIR + "testjar5.jar"));

    Collections.sort(list);

    Mockito.when(ss.resolveAndDownload(query, false)).thenReturn(list);
    for (int i = 0; i < 10; i++) {
      addList.add(query);
    }
    ss.add_resources(t, addList);
    Set<String> dependencies = ss.list_resource(t, null);
    LinkedList<URI> actual = new LinkedList<URI>();
    for (String dependency : dependencies) {
      actual.add(createURI(dependency));
    }

    Collections.sort(actual);
    assertEquals(list, actual);
    ss.close();

  }

  // test when two jars with shared dependencies are added, the classloader contains union of the dependencies
  @Test
  public void testUnion() throws URISyntaxException, IOException {

    HiveConf conf = new HiveConf();
    SessionState ss = Mockito.spy(SessionState.start(conf).get());
    ResourceType t = ResourceType.JAR;
    String query1 = "testQuery1";
    String query2 = "testQuery2";
    List<String> addList = new LinkedList<String>();
    // add dependencies for the jars
    List<URI> list1 = new LinkedList<URI>();
    List<URI> list2 = new LinkedList<URI>();
    list1.add(createURI(TEST_JAR_DIR + "testjar1.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar2.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar5.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar4.jar"));

    Mockito.when(ss.resolveAndDownload(query1, false)).thenReturn(list1);
    Mockito.when(ss.resolveAndDownload(query2, false)).thenReturn(list2);
    addList.add(query1);
    addList.add(query2);
    ss.add_resources(t, addList);

    Set<String> dependencies = ss.list_resource(t, null);
    LinkedList<URI> actual = new LinkedList<URI>();
    for (String dependency : dependencies) {
      actual.add(createURI(dependency));
    }
    List<URI> expected = union(list1, list2);

    Collections.sort(expected);
    Collections.sort(actual);

    assertEquals(expected, actual);
    ss.close();

  }

  /**
   * @param path
   * @return URI corresponding to the path.
   */
  private static URI createURI(String path) throws URISyntaxException {
    return new URI(path);
  }

  // Test when two jars are added with shared dependencies and one jar is deleted, the shared dependencies should not be deleted
  @Test
  public void testDeleteJar() throws URISyntaxException, IOException {
    SessionState ss = Mockito.spy(SessionState.start(conf).get());

    String query1 = "testQuery1";
    String query2 = "testQuery2";

    List<URI> list1 = new LinkedList<URI>();
    List<URI> list2 = new LinkedList<URI>();
    List<String> addList = new LinkedList<String>();
    list1.add(createURI(TEST_JAR_DIR + "testjar1.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar2.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar5.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar4.jar"));

    Collections.sort(list1);
    Collections.sort(list2);

    Mockito.when(ss.resolveAndDownload(query1, false)).thenReturn(list1);
    Mockito.when(ss.resolveAndDownload(query2, false)).thenReturn(list2);
    addList.add(query1);
    addList.add(query2);
    ss.add_resources(t, addList);
    List<String> deleteList = new LinkedList<String>();
    deleteList.add(list1.get(0).toString());
    // delete jar and its dependencies added using query1
    ss.delete_resources(t, deleteList);

    Set<String> dependencies = ss.list_resource(t, null);
    LinkedList<URI> actual = new LinkedList<URI>();
    for (String dependency : dependencies) {
      actual.add(createURI(dependency));
    }
    List<URI> expected = list2;
    Collections.sort(expected);
    Collections.sort(actual);
    assertEquals(expected, actual);

    deleteList.clear();
    deleteList.add(list2.get(0).toString());
    // delete remaining jars
    ss.delete_resources(t, deleteList);
    dependencies = ss.list_resource(t, null);
    assertEquals(dependencies.isEmpty(), true);

    ss.close();

  }

  // same test as above but with 3 jars sharing dependencies
  @Test
  public void testDeleteJarMultiple() throws URISyntaxException, IOException {
    SessionState ss = Mockito.spy(SessionState.start(conf).get());

    String query1 = "testQuery1";
    String query2 = "testQuery2";
    String query3 = "testQuery3";

    List<URI> list1 = new LinkedList<URI>();
    List<URI> list2 = new LinkedList<URI>();
    List<URI> list3 = new LinkedList<URI>();
    List<String> addList = new LinkedList<String>();
    list1.add(createURI(TEST_JAR_DIR + "testjar1.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar2.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list1.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar5.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar3.jar"));
    list2.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list3.add(createURI(TEST_JAR_DIR + "testjar4.jar"));
    list3.add(createURI(TEST_JAR_DIR + "testjar2.jar"));
    list3.add(createURI(TEST_JAR_DIR + "testjar5.jar"));

    Collections.sort(list1);
    Collections.sort(list2);
    Collections.sort(list3);

    Mockito.when(ss.resolveAndDownload(query1, false)).thenReturn(list1);
    Mockito.when(ss.resolveAndDownload(query2, false)).thenReturn(list2);
    Mockito.when(ss.resolveAndDownload(query3, false)).thenReturn(list3);
    addList.add(query1);
    addList.add(query2);
    addList.add(query3);
    ss.add_resources(t, addList);

    List<String> deleteList = new LinkedList<String>();
    deleteList.add(list1.get(0).toString());
    // delete jar added using query1
    ss.delete_resources(t, deleteList);

    Set<String> dependencies = ss.list_resource(t, null);
    LinkedList<URI> actual = new LinkedList<URI>();
    for (String dependency : dependencies) {
      actual.add(createURI(dependency));
    }
    List<URI> expected = union(list2, list3);
    Collections.sort(expected);
    Collections.sort(actual);
    assertEquals(expected, actual);

    actual.clear();
    expected.clear();

    deleteList.clear();
    deleteList.add(list2.get(0).toString());
    // delete jars added using query2
    ss.delete_resources(t, deleteList);
    dependencies = ss.list_resource(t, null);
    actual = new LinkedList<URI>();
    for (String dependency : dependencies) {
      actual.add(createURI(dependency));
    }
    expected = new LinkedList<URI>(list3);
    Collections.sort(expected);
    Collections.sort(actual);
    assertEquals(expected, actual);

    actual.clear();
    expected.clear();

    // delete remaining jars
    deleteList.clear();
    deleteList.add(list3.get(0).toString());
    ss.delete_resources(t, deleteList);

    dependencies = ss.list_resource(t, null);
    assertEquals(dependencies.isEmpty(), true);

    ss.close();
  }

  @After
  public void tearDown() {
    // delete sample jars
    for (int i = 1; i <= 5; i++) {
      String dataFile = TEST_JAR_DIR + "testjar" + i + ".jar";

      File f = new File(dataFile);
      if (!f.delete()) {
        throw new RuntimeException("Could not delete the data file");
      }
    }
  }

  private <T> List<T> union(List<T> list1, List<T> list2) {
    Set<T> set = new HashSet<T>();

    set.addAll(list1);
    set.addAll(list2);

    return new LinkedList<T>(set);
  }

}
