/*******************************************************************************
 * Copyright (c) 2009, 2018 Mountainminds GmbH & Co. KG and Contributors
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Marc R. Hoffmann - initial API and implementation
 *    
 *******************************************************************************/
package org.jacoco.core.runtime;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;

import org.jacoco.core.internal.BytecodeVersion;
import org.jacoco.core.test.TargetLoader;
import org.junit.Test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;

/**
 * Unit tests for {@link ModifiedSystemClassRuntime}.
 */
public class ModifiedSystemClassRuntimeTest extends RuntimeTestBase {

	@Override
	IRuntime createRuntime() {
		return new ModifiedSystemClassRuntime(
				ModifiedSystemClassRuntimeTest.class, "accessField");
	}

	@Test(expected = RuntimeException.class)
	public void testCreateForNegative() throws Exception {
		Instrumentation inst = newInstrumentationMock();
		ModifiedSystemClassRuntime.createFor(inst, TARGET_CLASS_NAME);
	}

	@Test
	public void should_instrument_java10_class() {
		final byte[] bytes = createClass(BytecodeVersion.V10);

		byte[] instrumented = ModifiedSystemClassRuntime.instrument(bytes,
				"accessField");

		assertEquals(BytecodeVersion.V10, BytecodeVersion.get(instrumented));
		instrumented = BytecodeVersion.downgradeIfNeeded(BytecodeVersion.V10,
				instrumented);
		final ClassNode classNode = new ClassNode();
		new ClassReader(instrumented).accept(classNode, 0);
		assertEquals("accessField", classNode.fields.get(0).name);
	}

	private static byte[] createClass(final int version) {
		final ClassWriter cw = new ClassWriter(0);
		cw.visit(version, 0, "Foo", null, "java/lang/Object", null);
		cw.visitEnd();
		return cw.toByteArray();
	}

	/** This static member emulate the instrumented system class. */
	public static Object accessField;

	private static final String TARGET_CLASS_NAME = "org/jacoco/core/runtime/ModifiedSystemClassRuntimeTest";

	/**
	 * Note that we use Proxy here to mock {@link Instrumentation}, because JDK
	 * 9 adds new method "addModule", whose parameter depends on class
	 * "java.lang.reflect.Module" introduced in JDK 9.
	 */
	private Instrumentation newInstrumentationMock() {
		return (Instrumentation) Proxy.newProxyInstance(getClass()
				.getClassLoader(), new Class[] { Instrumentation.class },
				new MyInvocationHandler());
	}

	private static class MyInvocationHandler implements InvocationHandler {
		boolean added = false;

		boolean removed = false;

		/**
		 * {@link Instrumentation#addTransformer(ClassFileTransformer)}
		 */
		void addTransformer(ClassFileTransformer transformer) {
			assertFalse(added);
			added = true;
			try {
				// Our class should get instrumented:
				final byte[] data = TargetLoader
						.getClassDataAsBytes(ModifiedSystemClassRuntimeTest.class);
				verifyInstrumentedClass(TARGET_CLASS_NAME,
						transformer.transform((ClassLoader) null,
								TARGET_CLASS_NAME, null, null, data));

				// Other classes will not be instrumented:
				assertNull(transformer.transform(getClass().getClassLoader(),
						"some/other/Class", null, null, new byte[0]));
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}

		/**
		 * {@link Instrumentation#removeTransformer(ClassFileTransformer)}
		 */
		Boolean removeTransformer() {
			assertTrue(added);
			assertFalse(removed);
			removed = true;
			return Boolean.TRUE;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			if (args.length == 1) {
				if ("removeTransformer".equals(method.getName())) {
					return removeTransformer();
				} else if ("addTransformer".equals(method.getName())) {
					addTransformer((ClassFileTransformer) args[0]);
					return null;
				}
			}
			fail();
			return null;
		}
	}

	private static void verifyInstrumentedClass(String name, byte[] source)
			throws Exception {
		name = name.replace('/', '.');
		final Class<?> targetClass = new TargetLoader().add(name, source);

		// Check added field:
		final Field f = targetClass.getField("$jacocoAccess");
		assertTrue(Modifier.isPublic(f.getModifiers()));
		assertTrue(Modifier.isStatic(f.getModifiers()));
		assertTrue(Modifier.isTransient(f.getModifiers()));
		assertEquals(Object.class, f.getType());
	}
}
