/*
 * Copyright 2014 Pivotal Software Inc. and contributors
 *
 * Licensed 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
 *
 *      https://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.springsource.loaded.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import org.junit.Ignore;
import org.junit.Test;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.TypeRegistry;
import org.springsource.loaded.test.infra.Result;

/**
 * Test reloading of Java 8.
 *
 * @author Andy Clement
 * @since 1.2
 */
public class Java8Tests extends SpringLoadedTests {

	@Test
	public void theBasics() {
		String t = "basic.FirstClass";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = new ReloadableType(t, sc, 1, typeRegistry, null);

		assertEquals(1, rtype.getId());
		assertEquals(t, rtype.getName());
		assertEquals(slashed(t), rtype.getSlashedName());
		assertNotNull(rtype.getTypeDescriptor());
		assertEquals(typeRegistry, rtype.getTypeRegistry());
	}


	@Test
	public void issue104() throws Exception {
		String t = "bugs.Issue104";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> clazz = rtype.getClazz();
		@SuppressWarnings("unused")
		Result r = runUnguarded(clazz, "run");

		r = runUnguarded(clazz, "run");

		rtype.loadNewVersion("002", rtype.bytesInitial);

		r = runUnguarded(clazz, "run");

		// TODO should assert something but the issue is that the JVM crashes...
	}

	@Test
	public void issue173() throws Exception {
		String t = "bugs.Issue173";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> clazz = rtype.getClazz();
		@SuppressWarnings("unused")
		Result r = runUnguarded(clazz, "run");

		r = runUnguarded(clazz, "run");

		rtype.loadNewVersion("002", rtype.bytesInitial);

		r = runUnguarded(clazz, "run");

		assertEquals("https://www.redacted.com/path", r.returnValue);
	}

	@Test
	public void callBasicType() throws Exception {
		String t = "basic.FirstClass";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(8, r.returnValue);

		rtype.loadNewVersion("002", rtype.bytesInitial);

		r = runUnguarded(simpleClass, "run");
		assertEquals(8, r.returnValue);
	}

	@Test
	public void lambdaA() throws Exception {
		String t = "basic.LambdaA";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(77, r.returnValue);

		rtype.loadNewVersion("002", rtype.bytesInitial);
		r = runUnguarded(simpleClass, "run");
		assertEquals(77, r.returnValue);
	}

	@Test
	public void changingALambda() throws Exception {
		String t = "basic.LambdaA";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(77, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Foo:" + t + "$Foo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(88, r.returnValue);
	}

	@Test
	public void lambdaWithParameter() throws Exception {
		String t = "basic.LambdaB";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(99L, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Foo:" + t + "$Foo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(176L, r.returnValue);
	}


	@Test
	public void lambdaWithTwoParameters() throws Exception {
		String t = "basic.LambdaC";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(6L, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Boo:" + t + "$Boo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(5L, r.returnValue);
	}

	@Test
	public void lambdaWithThreeMixedTypeParameters() throws Exception {
		String t = "basic.LambdaD";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("true342abc", r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Boo:" + t + "$Boo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals("def264true", r.returnValue);
	}

	@Test
	public void lambdaWithCapturedVariable() throws Exception {
		String t = "basic.LambdaE";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("aaaa", r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Boo:" + t + "$Boo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals("aaaaaaaa", r.returnValue);
	}

	@Test
	public void lambdaWithThis() throws Exception {
		String t = "basic.LambdaF";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("aaaaaaa", r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Boo:" + t + "$Boo");
		rtype.loadNewVersion("002", renamed);

		r = runUnguarded(simpleClass, "run");
		assertEquals("a:a:a:", r.returnValue);
	}

	@Test
	public void lambdaWithNonPublicInnerInterface() throws Exception {
		String t = "basic.LambdaG";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		// Since Boo needs promoting to public, have to ensure it is directly loaded:
		typeRegistry.addType(t + "$Boo", loadBytesForClass(t + "$Boo"));

		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(99, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Boo:" + t + "$Boo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(44, r.returnValue);
	}

	// A class implements an interface containing a default method and invokes it.
	// That default method is reloaded and modified to do something else.
	@Ignore
	@Test
	public void defaultMethods() throws Exception {
		String t = "basic.DefaultMethodsI1A";
		String t2 = "basic.DefaultMethodsC1A";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		byte[] ia = loadBytesForClass(t);
		ReloadableType rtypeI = typeRegistry.addType(t, ia);
		byte[] ca = loadBytesForClass(t2);
		ReloadableType rtypeC = typeRegistry.addType(t2, ca);

		Class<?> simpleClass = rtypeC.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(42, r.returnValue);

		//		byte[] renamed = retrieveRename(t, t + "2");
		rtypeI.loadNewVersion("002", ia);
		//		ClassPrinter.print(rtypeI.getBytesLoaded());

		r = runUnguarded(simpleClass, "run");
		assertEquals(42, r.returnValue);

		byte[] renamed2 = retrieveRename(t, t + "2");
		rtypeI.loadNewVersion(renamed2);
		r = runUnguarded(simpleClass, "run");
		assertEquals(42, r.returnValue);
		assertEquals("FOO", r.stdout);
	}


	// A class implements an interface. A default method is added to the interface on
	// a reload and invoked from a reloaded version of the class.
	@Ignore
	@Test
	public void defaultMethods2() throws Exception {
		String t = "basic.DefaultMethodsI2A";
		String t2 = "basic.DefaultMethodsC2A";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		byte[] ia = loadBytesForClass(t);
		ReloadableType rtypeI = typeRegistry.addType(t, ia);
		byte[] ca = loadBytesForClass(t2);
		ReloadableType rtypeC = typeRegistry.addType(t2, ca);

		Class<?> simpleClass = rtypeC.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(42, r.returnValue);

		//		byte[] renamed = retrieveRename(t, t + "2");
		rtypeI.loadNewVersion("002", ia);
		//		ClassPrinter.print(rtypeI.getBytesLoaded());

		r = runUnguarded(simpleClass, "run");
		assertEquals(42, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2");
		rtypeI.loadNewVersion(renamed);

		renamed = retrieveRename(t2, t2 + "2", t + "2:" + t);
		rtypeC.loadNewVersion(renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(42, r.returnValue);
		assertEquals("FOO", r.stdout);
	}

	@Test
	public void multipleLambdasInOneMethod() throws Exception {
		String t = "basic.LambdaH";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		// Since Foo needs promoting to public, have to ensure it is directly loaded:
		typeRegistry.addType(t + "$Foo", loadBytesForClass(t + "$Foo"));

		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(56, r.returnValue);

		rtype.loadNewVersion("002", rtype.bytesInitial);
		r = runUnguarded(simpleClass, "run");
		assertEquals(56, r.returnValue);
	}

	@Test
	public void lambdaSignatureChange() throws Exception {
		String t = "basic.LambdaI";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		// Since Foo needs promoting to public, have to ensure it is directly loaded:
		ReloadableType itype = typeRegistry.addType(t + "$Foo", loadBytesForClass(t + "$Foo"));

		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("a", r.returnValue);

		itype.loadNewVersion("002", retrieveRename(t + "$Foo", t + "2$Foo"));
		rtype.loadNewVersion("002", retrieveRename(t, t + "2", t + "2$Foo:" + t + "$Foo"));

		r = runUnguarded(simpleClass, "run");
		assertEquals("ab", r.returnValue);
	}


	@Test
	public void lambdaInvokeVirtual() throws Exception {
		String t = "basic.LambdaJ";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		// Since Foo needs promoting to public, have to ensure it is directly loaded:
		ReloadableType itype = typeRegistry.addType(t + "$Foo", loadBytesForClass(t + "$Foo"));

		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("fooa", r.returnValue);

		itype.loadNewVersion("002", retrieveRename(t + "$Foo", t + "2$Foo"));
		rtype.loadNewVersion("002", retrieveRename(t, t + "2", t + "2$Foo:" + t + "$Foo"));

		r = runUnguarded(simpleClass, "run");
		assertEquals("fooab", r.returnValue);
	}

	// https://github.com/spring-projects/spring-loaded/issues/87
	@Test
	public void lambdaMethodReference() throws Exception {
		String t = "basic.LambdaM";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("{5=test3}", r.returnValue);

		rtype.loadNewVersion("2", retrieveRename(t, t + "2"));//, t + "2$Foo:" + t + "$Foo"));

		r = runUnguarded(simpleClass, "run");
		assertEquals("{10=test3}", r.returnValue);

	}

	// https://github.com/spring-projects/spring-loaded/issues/87
	// This variant reloads both pieces
	@Test
	public void lambdaMethodReferenceInAnotherClass() throws Exception {
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		byte[] sc = loadBytesForClass("basic.LambdaN");
		ReloadableType rtype = typeRegistry.addType("basic.LambdaN", sc);
		byte[] helperBytes = loadBytesForClass("basic.HelperN");
		ReloadableType htype = typeRegistry.addType("basic.HelperN", helperBytes);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("{15=test3}", r.returnValue);

		rtype.loadNewVersion(sc);
		htype.loadNewVersion(helperBytes);

		r = runUnguarded(simpleClass, "run");
		assertEquals("{15=test3}", r.returnValue);
	}

	// This variant reloads only the caller
	@Test
	public void lambdaMethodReferenceInAnotherClass2() throws Exception {
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		byte[] sc = loadBytesForClass("basic.LambdaN");
		ReloadableType rtype = typeRegistry.addType("basic.LambdaN", sc);
		byte[] helperBytes = loadBytesForClass("basic.HelperN");
		typeRegistry.addType("basic.HelperN", helperBytes);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("{15=test3}", r.returnValue);

		rtype.loadNewVersion(sc);
		//		htype.loadNewVersion(helperBytes); // don't reload the helper

		r = runUnguarded(simpleClass, "run");
		assertEquals("{15=test3}", r.returnValue);
	}

	// This variant reloads only the helper (target)
	@Test
	public void lambdaMethodReferenceInAnotherClass3() throws Exception {
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");

		byte[] sc = loadBytesForClass("basic.LambdaN");
		ReloadableType rtype = typeRegistry.addType("basic.LambdaN", sc);
		byte[] helperBytes = loadBytesForClass("basic.HelperN");
		ReloadableType htype = typeRegistry.addType("basic.HelperN", helperBytes);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals("{15=test3}", r.returnValue);

		//		rtype.loadNewVersion(sc);
		htype.loadNewVersion(helperBytes);

		//		try {
		r = runUnguarded(simpleClass, "run");
		assertEquals("{15=test3}", r.returnValue);
		//			fail("did not expect that to work");
		//		}
		//		catch (Exception e) {
		//			e.printStackTrace();
		//			//			Caused by: java.lang.NoClassDefFoundError: basic/HelperN$$E2
		//			//			at basic.LambdaN$$Lambda$8/2085857771.apply(Unknown Source)
		//			//			at java.util.stream.Collectors.lambda$toMap$172(Collectors.java:1320)
		//			//			at java.util.stream.Collectors$$Lambda$5/1521118594.accept(Unknown Source)
		//			// That happens because LambdaN, which has not been reloaded, is loaded by classloader X, the computed
		//			// method to satisfy the lambda is in the executor for the helper, which is in a child classloader - that
		//			// is not visible from the one that loaded LambdaN.
		//			// However, part of the resolution process in the Java8 handling forces LambdaN to reload, so next
		//			// time we go in, the class can be seen because LambdaN$$E2 is in the same classloader. That is
		//			// why when we repeat what we just did, it'll work
		//		}
		//		r = runUnguarded(simpleClass, "run");
		//		assertEquals("{15=test3}", r.returnValue);
	}

	@Test
	public void lambdaWithDoubleDotConstructor() throws Exception {
		String t = "basic.LambdaO";
		TypeRegistry typeRegistry = getTypeRegistry(t);
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");

		r = runUnguarded(simpleClass, "run");
		assertEquals(3, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(4, r.returnValue);

	}

	@Test
	public void streamWithLambda() throws Exception {
		String t = "basic.StreamA";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");
		assertEquals(3, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Foo:" + t + "$Foo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(4, r.returnValue);
	}

	// inner interface (for the invokeinterface BSM)
	@Test
	public void streamWithLambdaInvokedVirtually() throws Exception {
		String t = "basic.StreamB";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");
		assertEquals(3, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2", t + "2$Foo:" + t + "$Foo");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(4, r.returnValue);
	}

	// not an inner interface this time (for the invokeinterface BSM)
	@Test
	public void streamWithLambdaInvokedVirtually2() throws Exception {
		String t = "basic.StreamBB";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");
		assertEquals(3, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(4, r.returnValue);
	}

	@Test
	public void streamWithoutLambda() throws Exception {
		String t = "basic.StreamC";
		TypeRegistry typeRegistry = getTypeRegistry("basic..*");
		byte[] sc = loadBytesForClass(t);
		ReloadableType rtype = typeRegistry.addType(t, sc);

		Class<?> simpleClass = rtype.getClazz();
		Result r = runUnguarded(simpleClass, "run");
		assertEquals(3, r.returnValue);

		byte[] renamed = retrieveRename(t, t + "2");
		rtype.loadNewVersion("002", renamed);
		r = runUnguarded(simpleClass, "run");
		assertEquals(4, r.returnValue);
	}

	@Ignore
	@Test
	public void lambdaWithVirtualMethodUse() throws Exception {
		// not yet written
	}

	@Ignore
	@Test
	public void altMetaFactoryUsage() throws Exception {
		// not yet written
	}

	// TODO catchers and lambda methods (non static ones)

}
