/*
 * 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.zeppelin.spark;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.zeppelin.interpreter.Interpreter;
import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterException;
import org.apache.zeppelin.interpreter.InterpreterGroup;
import org.apache.zeppelin.interpreter.InterpreterOutput;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.apache.zeppelin.interpreter.InterpreterResultMessage;
import org.apache.zeppelin.interpreter.LazyOpenInterpreter;
import org.apache.zeppelin.interpreter.remote.RemoteInterpreterEventClient;
import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
import org.apache.zeppelin.python.IPythonInterpreterTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;

public class IPySparkInterpreterTest extends IPythonInterpreterTest {

  private RemoteInterpreterEventClient mockIntpEventClient = mock(RemoteInterpreterEventClient.class);

  @Override
  protected Properties initIntpProperties() {
    Properties p = new Properties();
    p.setProperty(SparkStringConstants.MASTER_PROP_NAME, "local[4]");
    p.setProperty("spark.submit.deployMode", "client");
    p.setProperty(SparkStringConstants.APP_NAME_PROP_NAME, "Zeppelin Test");
    p.setProperty("zeppelin.spark.useHiveContext", "false");
    p.setProperty("zeppelin.spark.maxResult", "3");
    p.setProperty("zeppelin.spark.importImplicit", "true");
    p.setProperty("zeppelin.pyspark.python", "python");
    try {
      p.setProperty("zeppelin.dep.localrepo", Files.createTempDirectory("localrepo").toAbsolutePath().toString());
    } catch (IOException e) {
      fail(ExceptionUtils.getStackTrace(e));
    }

    p.setProperty("zeppelin.python.gatewayserver_address", "127.0.0.1");
    p.setProperty("zeppelin.spark.deprecatedMsg.show", "false");
    return p;
  }

  @Override
  protected void startInterpreter(Properties properties) throws InterpreterException {
    InterpreterContext context = getInterpreterContext();
    context.setIntpEventClient(mockIntpEventClient);
    InterpreterContext.set(context);

    LazyOpenInterpreter sparkInterpreter = new LazyOpenInterpreter(
        new SparkInterpreter(properties));
    intpGroup = new InterpreterGroup();
    intpGroup.put("session_1", new ArrayList<Interpreter>());
    intpGroup.get("session_1").add(sparkInterpreter);
    sparkInterpreter.setInterpreterGroup(intpGroup);

    LazyOpenInterpreter pySparkInterpreter =
        new LazyOpenInterpreter(new PySparkInterpreter(properties));
    intpGroup.get("session_1").add(pySparkInterpreter);
    pySparkInterpreter.setInterpreterGroup(intpGroup);

    interpreter = new LazyOpenInterpreter(new IPySparkInterpreter(properties));
    intpGroup.get("session_1").add(interpreter);
    interpreter.setInterpreterGroup(intpGroup);

    pySparkInterpreter.open();
    interpreter.open();
  }


  @Override
  @AfterEach
  public void tearDown() throws InterpreterException {
    intpGroup.close();
    interpreter = null;
    intpGroup = null;
  }

  //@Test
  public void testIPySpark() throws InterruptedException, InterpreterException, IOException {
    testPySpark(interpreter, mockIntpEventClient);
  }

  public static void testPySpark(final Interpreter interpreter, RemoteInterpreterEventClient mockIntpEventClient)
      throws InterpreterException, IOException, InterruptedException {
    reset(mockIntpEventClient);
    // rdd
    InterpreterContext context = createInterpreterContext(mockIntpEventClient);
    InterpreterResult result = interpreter.interpret("sc.version", context);
    Thread.sleep(100);
    assertEquals(InterpreterResult.Code.SUCCESS, result.code());
    String sparkVersion = context.out.toInterpreterResultMessage().get(0).getData();

    context = createInterpreterContext(mockIntpEventClient);
    result = interpreter.interpret("sc.range(1,10).sum()", context);
    Thread.sleep(100);
    assertEquals(InterpreterResult.Code.SUCCESS, result.code());
    List<InterpreterResultMessage> interpreterResultMessages = context.out.toInterpreterResultMessage();
    assertEquals("45", interpreterResultMessages.get(0).getData().trim());
    // spark job url is sent
    verify(mockIntpEventClient).onParaInfosReceived(any(Map.class));

    // spark sql
    context = createInterpreterContext(mockIntpEventClient);
    result = interpreter.interpret("df = spark.createDataFrame([(1,'a'),(2,'b')])\ndf.show()", context);
    assertEquals(InterpreterResult.Code.SUCCESS, result.code());
    interpreterResultMessages = context.out.toInterpreterResultMessage();
    assertEquals(
        "+---+---+\n" +
            "| _1| _2|\n" +
            "+---+---+\n" +
            "|  1|  a|\n" +
            "|  2|  b|\n" +
            "+---+---+", interpreterResultMessages.get(0).getData().trim());

    context = createInterpreterContext(mockIntpEventClient);
    result = interpreter.interpret("z.show(df)", context);
    assertEquals(InterpreterResult.Code.SUCCESS, result.code());
    interpreterResultMessages = context.out.toInterpreterResultMessage();
    assertEquals(
        "_1	_2\n" +
            "1	a\n" +
            "2	b", interpreterResultMessages.get(0).getData().trim());

    // spark sql python API bindings
    result = interpreter.interpret("df.explain()", context);
    assertEquals(InterpreterResult.Code.SUCCESS, result.code());

    // cancel
    if (interpreter instanceof IPySparkInterpreter) {
      final InterpreterContext context2 = createInterpreterContext(mockIntpEventClient);

      Thread thread = new Thread() {
        @Override
        public void run() {
          InterpreterResult result = null;
          try {
            result = interpreter.interpret("import time\nsc.range(1,10).foreach(lambda x: time.sleep(1))", context2);
          } catch (InterpreterException e) {
            e.printStackTrace();
          }
          assertEquals(InterpreterResult.Code.ERROR, result.code());
          List<InterpreterResultMessage> interpreterResultMessages = null;
          try {
            interpreterResultMessages = context2.out.toInterpreterResultMessage();
            assertTrue(interpreterResultMessages.get(0).getData().contains("KeyboardInterrupt"));
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      };
      thread.start();

      // sleep 1 second to wait for the spark job starts
      Thread.sleep(1000);
      interpreter.cancel(context);
      thread.join();
    }

    // completions
    List<InterpreterCompletion> completions = interpreter.completion("sc.ran", 6, createInterpreterContext(mockIntpEventClient));
    assertEquals(1, completions.size());
    assertEquals("range", completions.get(0).getValue());

    completions = interpreter.completion("sc.", 3, createInterpreterContext(mockIntpEventClient));
    assertTrue(completions.size() > 0);
    completions.contains(new InterpreterCompletion("range", "range", ""));

    completions = interpreter.completion("1+1\nsc.", 7, createInterpreterContext(mockIntpEventClient));
    assertTrue(completions.size() > 0);
    completions.contains(new InterpreterCompletion("range", "range", ""));

    completions = interpreter.completion("s", 1, createInterpreterContext(mockIntpEventClient));
    assertTrue(completions.size() > 0);
    completions.contains(new InterpreterCompletion("sc", "sc", ""));

    // python call java via py4j
    context = createInterpreterContext(mockIntpEventClient);
    result = interpreter.interpret("sc._jvm.java.lang.System.out.println(\"hello world\")", context);
    assertEquals(InterpreterResult.Code.SUCCESS, result.code());
    interpreterResultMessages = context.out.toInterpreterResultMessage();
    assertEquals(1, interpreterResultMessages.size());
    assertEquals("hello world\n", interpreterResultMessages.get(0).getData());

    // pyspark streaming TODO(zjffdu) disable pyspark streaming test temporary
    context = createInterpreterContext(mockIntpEventClient);
    //    result = interpreter.interpret(
    //            "from pyspark.streaming import StreamingContext\n" +
    //                    "import time\n" +
    //                    "ssc = StreamingContext(sc, 1)\n" +
    //                    "rddQueue = []\n" +
    //                    "for i in range(5):\n" +
    //                    "    rddQueue += [ssc.sparkContext.parallelize([j for j in range(1, 1001)], 10)]\n" +
    //                    "inputStream = ssc.queueStream(rddQueue)\n" +
    //                    "mappedStream = inputStream.map(lambda x: (x % 10, 1))\n" +
    //                    "reducedStream = mappedStream.reduceByKey(lambda a, b: a + b)\n" +
    //                    "reducedStream.pprint()\n" +
    //                    "ssc.start()\n" +
    //                    "time.sleep(6)\n" +
    //                    "ssc.stop(stopSparkContext=False, stopGraceFully=True)", context);
    //    Thread.sleep(1000);
    //    assertEquals(InterpreterResult.Code.SUCCESS, result.code());
    //    interpreterResultMessages = context.out.toInterpreterResultMessage();
    //    assertEquals(1, interpreterResultMessages.size());
    //    assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)"));
  }

  @Test
  @Override
  public void testIPythonFailToLaunch() throws InterpreterException {
    tearDown();

    Properties properties = initIntpProperties();
    properties.setProperty("spark.pyspark.python", "invalid_python");
    try {
      startInterpreter(properties);
      fail("Should not be able to start IPythonInterpreter");
    } catch (InterpreterException e) {
      String exceptionMsg = ExceptionUtils.getStackTrace(e);
      assertTrue(exceptionMsg.contains("No such file or directory"), exceptionMsg);
    }
  }

  private static InterpreterContext createInterpreterContext(RemoteInterpreterEventClient mockRemoteEventClient) {
    return InterpreterContext.builder()
        .setNoteId("noteId")
        .setParagraphId("paragraphId")
        .setIntpEventClient(mockRemoteEventClient)
        .setInterpreterOut(new InterpreterOutput())
        .build();
  }
}
