/*
 * 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.flink.client.cli;

import org.apache.flink.api.dag.Pipeline;
import org.apache.flink.client.FlinkPipelineTranslationUtil;
import org.apache.flink.client.program.PackagedProgram;
import org.apache.flink.client.program.PackagedProgramUtils;
import org.apache.flink.client.program.ProgramInvocationException;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.optimizer.DataStatistics;
import org.apache.flink.optimizer.Optimizer;
import org.apache.flink.optimizer.costs.DefaultCostEstimator;
import org.apache.flink.runtime.jobgraph.SavepointRestoreSettings;
import org.apache.flink.util.TestLogger;

import org.apache.commons.cli.CommandLine;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.FileNotFoundException;
import java.net.URL;
import java.util.Collections;

import static org.apache.flink.client.cli.CliFrontendTestUtils.TEST_JAR_CLASSLOADERTEST_CLASS;
import static org.apache.flink.client.cli.CliFrontendTestUtils.TEST_JAR_MAIN_CLASS;
import static org.apache.flink.client.cli.CliFrontendTestUtils.getNonJarFilePath;
import static org.apache.flink.client.cli.CliFrontendTestUtils.getTestJarPath;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

/**
 * Tests for the RUN command with {@link PackagedProgram PackagedPrograms}.
 */
public class CliFrontendPackageProgramTest extends TestLogger {

	private CliFrontend frontend;

	@BeforeClass
	public static void init() {
		CliFrontendTestUtils.pipeSystemOutToNull();
	}

	@AfterClass
	public static void shutdown() {
		CliFrontendTestUtils.restoreSystemOut();
	}

	@Before
	public void setup() throws Exception {
		final Configuration configuration = new Configuration();
		frontend = new CliFrontend(
			configuration,
			Collections.singletonList(new DefaultCLI(configuration)));
	}

	@Test
	public void testNonExistingJarFile() throws Exception {
		ProgramOptions programOptions = mock(ProgramOptions.class);
		when(programOptions.getJarFilePath()).thenReturn("/some/none/existing/path");

		try {
			frontend.buildProgram(programOptions);
			fail("should throw an exception");
		}
		catch (FileNotFoundException e) {
			// that's what we want
		}
	}

	@Test
	public void testFileNotJarFile() throws Exception {
		ProgramOptions programOptions = mock(ProgramOptions.class);
		when(programOptions.getJarFilePath()).thenReturn(getNonJarFilePath());
		when(programOptions.getProgramArgs()).thenReturn(new String[0]);
		when(programOptions.getSavepointRestoreSettings()).thenReturn(SavepointRestoreSettings.none());

		try {
			frontend.buildProgram(programOptions);
			fail("should throw an exception");
		} catch (ProgramInvocationException e) {
			// that's what we want
		}
	}

	@Test
	public void testVariantWithExplicitJarAndArgumentsOption() throws Exception {
		String[] arguments = {
				"--classpath", "file:///tmp/foo",
				"--classpath", "file:///tmp/bar",
				"-j", getTestJarPath(),
				"-a", "--debug", "true", "arg1", "arg2" };
		URL[] classpath = new URL[] { new URL("file:///tmp/foo"), new URL("file:///tmp/bar") };
		String[] reducedArguments = new String[] {"--debug", "true", "arg1", "arg2"};

		CommandLine commandLine = CliFrontendParser.parse(CliFrontendParser.RUN_OPTIONS, arguments, true);
		ProgramOptions programOptions = ProgramOptions.create(commandLine);

		assertEquals(getTestJarPath(), programOptions.getJarFilePath());
		assertArrayEquals(classpath, programOptions.getClasspaths().toArray());
		assertArrayEquals(reducedArguments, programOptions.getProgramArgs());

		PackagedProgram prog = frontend.buildProgram(programOptions);

		Assert.assertArrayEquals(reducedArguments, prog.getArguments());
		Assert.assertEquals(TEST_JAR_MAIN_CLASS, prog.getMainClassName());
	}

	@Test
	public void testVariantWithExplicitJarAndNoArgumentsOption() throws Exception {
		String[] arguments = {
				"--classpath", "file:///tmp/foo",
				"--classpath", "file:///tmp/bar",
				"-j", getTestJarPath(),
				"--debug", "true", "arg1", "arg2" };
		URL[] classpath = new URL[] { new URL("file:///tmp/foo"), new URL("file:///tmp/bar") };
		String[] reducedArguments = new String[] {"--debug", "true", "arg1", "arg2"};

		CommandLine commandLine = CliFrontendParser.parse(CliFrontendParser.RUN_OPTIONS, arguments, true);
		ProgramOptions programOptions = ProgramOptions.create(commandLine);

		assertEquals(getTestJarPath(), programOptions.getJarFilePath());
		assertArrayEquals(classpath, programOptions.getClasspaths().toArray());
		assertArrayEquals(reducedArguments, programOptions.getProgramArgs());

		PackagedProgram prog = frontend.buildProgram(programOptions);

		Assert.assertArrayEquals(reducedArguments, prog.getArguments());
		Assert.assertEquals(TEST_JAR_MAIN_CLASS, prog.getMainClassName());
	}

	@Test
	public void testValidVariantWithNoJarAndNoArgumentsOption() throws Exception {
		String[] arguments = {
				"--classpath", "file:///tmp/foo",
				"--classpath", "file:///tmp/bar",
				getTestJarPath(),
				"--debug", "true", "arg1", "arg2" };
		URL[] classpath = new URL[] { new URL("file:///tmp/foo"), new URL("file:///tmp/bar") };
		String[] reducedArguments = {"--debug", "true", "arg1", "arg2"};

		CommandLine commandLine = CliFrontendParser.parse(CliFrontendParser.RUN_OPTIONS, arguments, true);
		ProgramOptions programOptions = ProgramOptions.create(commandLine);

		assertEquals(getTestJarPath(), programOptions.getJarFilePath());
		assertArrayEquals(classpath, programOptions.getClasspaths().toArray());
		assertArrayEquals(reducedArguments, programOptions.getProgramArgs());

		PackagedProgram prog = frontend.buildProgram(programOptions);

		Assert.assertArrayEquals(reducedArguments, prog.getArguments());
		Assert.assertEquals(TEST_JAR_MAIN_CLASS, prog.getMainClassName());
	}

	@Test(expected = CliArgsException.class)
	public void testNoJarNoArgumentsAtAll() throws Exception {
		frontend.run(new String[0]);
	}

	@Test
	public void testNonExistingFileWithArguments() throws Exception {
		String[] arguments = {
				"--classpath", "file:///tmp/foo",
				"--classpath", "file:///tmp/bar",
				"/some/none/existing/path",
				"--debug", "true", "arg1", "arg2"  };
		URL[] classpath = new URL[] { new URL("file:///tmp/foo"), new URL("file:///tmp/bar") };
		String[] reducedArguments = {"--debug", "true", "arg1", "arg2"};

		CommandLine commandLine = CliFrontendParser.parse(CliFrontendParser.RUN_OPTIONS, arguments, true);
		ProgramOptions programOptions = ProgramOptions.create(commandLine);

		assertEquals(arguments[4], programOptions.getJarFilePath());
		assertArrayEquals(classpath, programOptions.getClasspaths().toArray());
		assertArrayEquals(reducedArguments, programOptions.getProgramArgs());

		try {
			frontend.buildProgram(programOptions);
			fail("Should fail with an exception");
		}
		catch (FileNotFoundException e) {
			// that's what we want
		}
	}

	@Test
	public void testNonExistingFileWithoutArguments() throws Exception {
		String[] arguments = {"/some/none/existing/path"};

		CommandLine commandLine = CliFrontendParser.parse(CliFrontendParser.RUN_OPTIONS, arguments, true);
		ProgramOptions programOptions = ProgramOptions.create(commandLine);

		assertEquals(arguments[0], programOptions.getJarFilePath());
		assertArrayEquals(new String[0], programOptions.getProgramArgs());

		try {
			frontend.buildProgram(programOptions);
		}
		catch (FileNotFoundException e) {
			// that's what we want
		}
	}

	/**
	 * Ensure that we will never have the following error.
	 *
	 * <pre>
	 * 	org.apache.flink.client.program.ProgramInvocationException: The main method caused an error.
	 *		at org.apache.flink.client.program.PackagedProgram.callMainMethod(PackagedProgram.java:398)
	 *		at org.apache.flink.client.program.PackagedProgram.invokeInteractiveModeForExecution(PackagedProgram.java:301)
	 *		at org.apache.flink.client.program.Client.getOptimizedPlan(Client.java:140)
	 *		at org.apache.flink.client.program.Client.getOptimizedPlanAsJson(Client.java:125)
	 *		at org.apache.flink.client.cli.CliFrontend.info(CliFrontend.java:439)
	 *		at org.apache.flink.client.cli.CliFrontend.parseParameters(CliFrontend.java:931)
	 *		at org.apache.flink.client.cli.CliFrontend.main(CliFrontend.java:951)
	 *	Caused by: java.io.IOException: java.lang.RuntimeException: java.lang.ClassNotFoundException: org.apache.hadoop.hive.ql.io.RCFileInputFormat
	 *		at org.apache.hcatalog.mapreduce.HCatInputFormat.setInput(HCatInputFormat.java:102)
	 *		at org.apache.hcatalog.mapreduce.HCatInputFormat.setInput(HCatInputFormat.java:54)
	 *		at tlabs.CDR_In_Report.createHCatInputFormat(CDR_In_Report.java:322)
	 *		at tlabs.CDR_Out_Report.main(CDR_Out_Report.java:380)
	 *		at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	 *		at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	 *		at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	 *		at java.lang.reflect.Method.invoke(Method.java:622)
	 *		at org.apache.flink.client.program.PackagedProgram.callMainMethod(PackagedProgram.java:383)
	 * </pre>
	 *
	 * <p>The test works as follows:
	 *
	 * <ul>
	 *   <li> Use the CliFrontend to invoke a jar file that loads a class which is only available
	 * 	      in the jarfile itself (via a custom classloader)
	 *   <li> Change the Usercode classloader of the PackagedProgram to a special classloader for this test
	 *   <li> the classloader will accept the special class (and return a String.class)
	 * </ul>
	 */
	@Test
	public void testPlanWithExternalClass() throws Exception {
		final boolean[] callme = { false }; // create a final object reference, to be able to change its val later

		try {
			String[] arguments = {
					"--classpath", "file:///tmp/foo",
					"--classpath", "file:///tmp/bar",
					"-c", TEST_JAR_CLASSLOADERTEST_CLASS, getTestJarPath(),
					"true", "arg1", "arg2" };
			URL[] classpath = new URL[] { new URL("file:///tmp/foo"), new URL("file:///tmp/bar") };
			String[] reducedArguments = { "true", "arg1", "arg2" };

			CommandLine commandLine = CliFrontendParser.parse(CliFrontendParser.RUN_OPTIONS, arguments, true);
			ProgramOptions programOptions = ProgramOptions.create(commandLine);

			assertEquals(getTestJarPath(), programOptions.getJarFilePath());
			assertArrayEquals(classpath, programOptions.getClasspaths().toArray());
			assertEquals(TEST_JAR_CLASSLOADERTEST_CLASS, programOptions.getEntryPointClassName());
			assertArrayEquals(reducedArguments, programOptions.getProgramArgs());

			PackagedProgram prog = spy(frontend.buildProgram(programOptions));

			ClassLoader testClassLoader = new ClassLoader(prog.getUserCodeClassLoader()) {
				@Override
				public Class<?> loadClass(String name) throws ClassNotFoundException {
					if ("org.apache.hadoop.hive.ql.io.RCFileInputFormat".equals(name)) {
						callme[0] = true;
						return String.class; // Intentionally return the wrong class.
					} else {
						return super.loadClass(name);
					}
				}
			};
			when(prog.getUserCodeClassLoader()).thenReturn(testClassLoader);

			assertEquals(TEST_JAR_CLASSLOADERTEST_CLASS, prog.getMainClassName());
			assertArrayEquals(reducedArguments, prog.getArguments());

			Configuration c = new Configuration();
			Optimizer compiler = new Optimizer(new DataStatistics(), new DefaultCostEstimator(), c);

			// we expect this to fail with a "ClassNotFoundException"
			Pipeline pipeline = PackagedProgramUtils.getPipelineFromProgram(prog, c, 666, true);
			FlinkPipelineTranslationUtil.translateToJSONExecutionPlan(pipeline);
			fail("Should have failed with a ClassNotFoundException");
		}
		catch (ProgramInvocationException e) {
			if (!(e.getCause() instanceof ClassNotFoundException)) {
				e.printStackTrace();
				fail("Program didn't throw ClassNotFoundException");
			}
			assertTrue("Classloader was not called", callme[0]);
		}
	}
}