/*
 * Copyright (c) 2013-2017 Chris Newland.
 * Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
 * Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
 */
package org.adoptopenjdk.jitwatch.test;

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

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_VOID;

import org.adoptopenjdk.jitwatch.loader.BytecodeLoader;
import org.adoptopenjdk.jitwatch.logger.NullLogListener;
import org.adoptopenjdk.jitwatch.model.MemberSignatureParts;
import org.adoptopenjdk.jitwatch.model.bytecode.ClassBC;
import org.adoptopenjdk.jitwatch.model.bytecode.MemberBytecode;
import org.adoptopenjdk.jitwatch.model.bytecode.SourceMapper;
import org.adoptopenjdk.jitwatch.process.compiler.CompilerJava;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

/*
 javap -v * | egrep "Classfile|line|public"

 Classfile /home/chris/jitwatch/sandbox/classes/TestInner.class
 public TestInner();
 line 4: 0
 line 5: 4
 line 7: 12
 line 9: 16
 line 10: 25
 public void a();
 line 14: 0
 line 15: 8
 public static void main(java.lang.String[]);
 line 52: 0
 line 53: 8

 Classfile /home/chris/jitwatch/sandbox/classes/TestInner$Inner1.class
 public TestInner$Inner1(TestInner);
 line 20: 0
 line 21: 9
 line 23: 17
 line 25: 21
 line 26: 30
 public void b();
 line 30: 0
 line 31: 8

 Classfile /home/chris/jitwatch/sandbox/classes/TestInner$Inner1$Inner2.class
 public TestInner$Inner1$Inner2(TestInner$Inner1);
 line 36: 0
 line 37: 9
 line 39: 17
 line 40: 21
 public void c();
 line 44: 0
 line 45: 8
 */
public class TestBytecodeLoaderWithInnerClasses
{
	private static String classNameOuter = "TestInner";
	private static String classNameInner1 = "TestInner$Inner1";
	private static String classNameInner2 = "TestInner$Inner1$Inner2";

	private static Path pathToSourceDir;
	private static Path pathToTempClassDir;
	private static List<String> classpathLocations;

	private static ClassBC classBytecodeForOuter;
	private static ClassBC classBytecodeForInner1;
	private static ClassBC classBytecodeForInner2;

	@BeforeClass public static void setUp()
	{
		try
		{
			SourceMapper.clear();

			pathToSourceDir = Paths.get("src", "main", "resources", "examples");

			pathToTempClassDir = Files.createTempDirectory("testInnerClasses");

			Path pathToSourceFile = Paths.get(pathToSourceDir.toString(), classNameOuter + ".java");

			List<File> sources = new ArrayList<>();
			sources.add(pathToSourceFile.toFile());

			classpathLocations = new ArrayList<>();
			classpathLocations.add(pathToTempClassDir.toString());

			CompilerJava compiler = new CompilerJava(System.getProperty("java.home"));

			List<String> compileClasspath = new ArrayList<>();

			List<String> vmOptions = new ArrayList<>();

			boolean success = compiler.compile(sources, compileClasspath, vmOptions, pathToTempClassDir.toFile(), Collections.emptyMap(),
					new NullLogListener());

			if (!success)
			{
				System.err.println(compiler.getErrorStream());
				fail();
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();

			fail();
		}

		classBytecodeForOuter = BytecodeLoader.fetchBytecodeForClass(classpathLocations, classNameOuter, true);

		BytecodeLoader.fetchBytecodeForClass(classpathLocations, classNameInner1, true);
		BytecodeLoader.fetchBytecodeForClass(classpathLocations, classNameInner2, true);

		List<ClassBC> classBytecodeListForOuter = SourceMapper.getClassBytecodeList(classBytecodeForOuter);

		classBytecodeForOuter = classBytecodeListForOuter.get(0);
		classBytecodeForInner1 = classBytecodeListForOuter.get(1);
		classBytecodeForInner2 = classBytecodeListForOuter.get(2);
	}

	@AfterClass public static void tearDown()
	{
	}

	private void checkMemberNames(ClassBC classBytecode, String... memberNames)
	{
		List<MemberBytecode> memberBytecodeList = classBytecode.getMemberBytecodeList();

		Set<String> memberNameSet = new HashSet<>(Arrays.asList(memberNames));

		for (MemberBytecode memberBytecode : memberBytecodeList)
		{
			assertTrue(memberNameSet.contains(memberBytecode.getMemberSignatureParts().getMemberName()));

		}

		assertEquals(memberNames.length, memberBytecodeList.size());
	}

	@Test public void testCompilationCreatedCorrectOutputs()
	{
		assertTrue(Paths.get(pathToTempClassDir.toString(), classNameOuter + ".class").toFile().exists());
		assertTrue(Paths.get(pathToTempClassDir.toString(), classNameInner1 + ".class").toFile().exists());
		assertTrue(Paths.get(pathToTempClassDir.toString(), classNameInner2 + ".class").toFile().exists());

		assertEquals(classNameOuter + ".java", classBytecodeForOuter.getSourceFile());

		checkMemberNames(classBytecodeForOuter, "main", "TestInner", "a");

		List<String> innerClasses = classBytecodeForOuter.getInnerClassNames();

		assertEquals(1, innerClasses.size());

		assertEquals(classNameInner1, innerClasses.get(0));

		assertEquals(classNameOuter + ".java", classBytecodeForInner1.getSourceFile());

		checkMemberNames(classBytecodeForInner1, "TestInner$Inner1", "b");

		List<String> innerClassesOfInner1 = classBytecodeForInner1.getInnerClassNames();

		assertEquals(1, innerClassesOfInner1.size());

		assertEquals(classNameInner2, innerClassesOfInner1.get(0));

		assertEquals(classNameOuter + ".java", classBytecodeForInner2.getSourceFile());

		checkMemberNames(classBytecodeForInner2, "TestInner$Inner1$Inner2", "c");

		List<ClassBC> classBytecodeListForOuter = SourceMapper.getClassBytecodeList(classBytecodeForOuter);

		assertNotNull(classBytecodeListForOuter);

		assertEquals(3, classBytecodeListForOuter.size());
	}

	@Test public void testSearchFromSourceOuterClassConstructor()
	{
		String fqClassNameOuter = classBytecodeForOuter.getFullyQualifiedClassName();

		assertEquals(classNameOuter, fqClassNameOuter);

		MemberBytecode memberBytecodeForConstructor = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForOuter, 4);

		assertNotNull(memberBytecodeForConstructor);

		MemberSignatureParts mspOuterConstructor = memberBytecodeForConstructor.getMemberSignatureParts();

		assertNotNull(mspOuterConstructor);

		assertEquals(S_TYPE_NAME_VOID, mspOuterConstructor.getReturnType());
		assertEquals(0, mspOuterConstructor.getParamTypes().size());
		assertEquals(classNameOuter, mspOuterConstructor.getMemberName());
	}

	@Test public void testSearchFromSourceOuterClassMethod()
	{
		MemberBytecode memberBytecodeForMethod = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForOuter, 14);

		assertNotNull(memberBytecodeForMethod);

		MemberSignatureParts mspOuterMethod = memberBytecodeForMethod.getMemberSignatureParts();

		assertNotNull(mspOuterMethod);

		assertEquals(S_TYPE_NAME_VOID, mspOuterMethod.getReturnType());
		assertEquals(0, mspOuterMethod.getParamTypes().size());
		assertEquals("a", mspOuterMethod.getMemberName());
	}

	@Test public void testSearchFromSourceInner1ClassConstructor()
	{
		String fqClassNameInner1 = classBytecodeForInner1.getFullyQualifiedClassName();

		assertEquals(classNameInner1, fqClassNameInner1);

		MemberBytecode memberBytecodeForInner1Constructor = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForInner1, 20);

		assertNotNull(memberBytecodeForInner1Constructor);

		MemberSignatureParts mspInner1Constructor = memberBytecodeForInner1Constructor.getMemberSignatureParts();

		assertNotNull(mspInner1Constructor);

		assertEquals(S_TYPE_NAME_VOID, mspInner1Constructor.getReturnType());
		assertEquals(1, mspInner1Constructor.getParamTypes().size());
		assertEquals(classNameOuter, mspInner1Constructor.getParamTypes().get(0));

		assertEquals(classNameInner1, mspInner1Constructor.getMemberName());
	}

	@Test public void testSearchFromSourceInner1ClassMethod()
	{
		MemberBytecode memberBytecodeForInner1Method = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForInner1, 30);

		assertNotNull(memberBytecodeForInner1Method);

		MemberSignatureParts mspInner1Method = memberBytecodeForInner1Method.getMemberSignatureParts();

		assertNotNull(mspInner1Method);

		assertEquals(S_TYPE_NAME_VOID, mspInner1Method.getReturnType());
		assertEquals(0, mspInner1Method.getParamTypes().size());
		assertEquals("b", mspInner1Method.getMemberName());
	}

	@Test public void testSearchFromSourceInner2ClassConstructor()
	{
		String fqClassNameInner2 = classBytecodeForInner2.getFullyQualifiedClassName();

		assertEquals(classNameInner2, fqClassNameInner2);

		MemberBytecode memberBytecodeForInner2Constructor = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForInner2, 36);

		assertNotNull(memberBytecodeForInner2Constructor);

		MemberSignatureParts mspInner2Constructor = memberBytecodeForInner2Constructor.getMemberSignatureParts();

		assertNotNull(mspInner2Constructor);

		assertEquals(S_TYPE_NAME_VOID, mspInner2Constructor.getReturnType());
		assertEquals(1, mspInner2Constructor.getParamTypes().size());
		assertEquals(classNameInner1, mspInner2Constructor.getParamTypes().get(0));

		assertEquals(classNameInner2, mspInner2Constructor.getMemberName());
	}

	@Test public void testSearchFromSourceInner2ClassMethod()
	{
		MemberBytecode memberBytecodeForInner2Method = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForInner2, 44);

		assertNotNull(memberBytecodeForInner2Method);

		MemberSignatureParts mspInner2Method = memberBytecodeForInner2Method.getMemberSignatureParts();

		assertNotNull(mspInner2Method);

		assertEquals(S_TYPE_NAME_VOID, mspInner2Method.getReturnType());
		assertEquals(0, mspInner2Method.getParamTypes().size());
		assertEquals("c", mspInner2Method.getMemberName());
	}

	@Test public void testSearchFromBytecodeOuterClassConstructor()
	{
		MemberBytecode memberBytecode = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForOuter, 4);

		// line 4: 0
		// line 5: 4
		// line 7: 12
		// line 9: 16
		// line 10: 25

		assertEquals(4, SourceMapper.getSourceLineFromBytecode(memberBytecode, 0));
		assertEquals(5, SourceMapper.getSourceLineFromBytecode(memberBytecode, 4));
		assertEquals(7, SourceMapper.getSourceLineFromBytecode(memberBytecode, 12));
		assertEquals(9, SourceMapper.getSourceLineFromBytecode(memberBytecode, 16));
		assertEquals(10, SourceMapper.getSourceLineFromBytecode(memberBytecode, 25));
	}

	@Test public void testSearchFromBytecodeOuterClassMethod()
	{
		MemberBytecode memberBytecode = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForOuter, 14);

		// line 14: 0
		// line 15: 8

		assertEquals(14, SourceMapper.getSourceLineFromBytecode(memberBytecode, 0));
		assertEquals(15, SourceMapper.getSourceLineFromBytecode(memberBytecode, 8));
	}

	@Test public void testSearchFromBytecodeInner1ClassMethod()
	{
		MemberBytecode memberBytecode = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForInner1, 30);

		// line 30: 0
		// line 31: 8

		assertEquals(30, SourceMapper.getSourceLineFromBytecode(memberBytecode, 0));
		assertEquals(31, SourceMapper.getSourceLineFromBytecode(memberBytecode, 8));
	}

	@Test public void testSearchFromBytecodeInner2ClassMethod()
	{
		MemberBytecode memberBytecode = SourceMapper.getMemberBytecodeForSourceLine(classBytecodeForInner2, 44);

		//		 line 44: 0
		//		 line 45: 8

		assertEquals(44, SourceMapper.getSourceLineFromBytecode(memberBytecode, 0));
		assertEquals(45, SourceMapper.getSourceLineFromBytecode(memberBytecode, 8));
	}
}
