/*******************************************************************************
 * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors
 * This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Marc R. Hoffmann - initial API and implementation
 *
 *******************************************************************************/
package org.jacoco.core.internal.instr;

import static org.junit.Assert.assertEquals;

import org.jacoco.core.instr.MethodRecorder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypeReference;

/**
 * Unit tests for {@link ProbeInserter}.
 */
public class ProbeInserterTest {

	private MethodRecorder actual, expected;

	private MethodVisitor actualVisitor, expectedVisitor;

	private IProbeArrayStrategy arrayStrategy;

	@Before
	public void setup() {
		actual = new MethodRecorder();
		actualVisitor = actual.getVisitor();
		expected = new MethodRecorder();
		expectedVisitor = expected.getVisitor();
		arrayStrategy = new IProbeArrayStrategy() {
			public int storeInstance(MethodVisitor mv, boolean clinit,
					int variable) {
				mv.visitLdcInsn(clinit ? "clinit" : "init");
				return 5;
			}

			public void addMembers(ClassVisitor delegate, int probeCount) {
			}
		};
	}

	@After
	public void verify() {
		assertEquals(expected, actual);
	}

	@Test
	public void testVariableStatic() {
		ProbeInserter pi = new ProbeInserter(Opcodes.ACC_STATIC, "m", "()V",
				actualVisitor, arrayStrategy);
		pi.insertProbe(0);

		expectedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
		expectedVisitor.visitInsn(Opcodes.ICONST_0);
		expectedVisitor.visitInsn(Opcodes.ICONST_1);
		expectedVisitor.visitInsn(Opcodes.BASTORE);
	}

	@Test
	public void testVariableNonStatic() {
		ProbeInserter pi = new ProbeInserter(0, "m", "()V", actualVisitor,
				arrayStrategy);
		pi.insertProbe(0);

		expectedVisitor.visitVarInsn(Opcodes.ALOAD, 1);
		expectedVisitor.visitInsn(Opcodes.ICONST_0);
		expectedVisitor.visitInsn(Opcodes.ICONST_1);
		expectedVisitor.visitInsn(Opcodes.BASTORE);
	}

	@Test
	public void testVariableNonStatic_IZObject() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(IZLjava/lang/Object;)V",
				actualVisitor, arrayStrategy);
		pi.insertProbe(0);

		expectedVisitor.visitVarInsn(Opcodes.ALOAD, 4);
		expectedVisitor.visitInsn(Opcodes.ICONST_0);
		expectedVisitor.visitInsn(Opcodes.ICONST_1);
		expectedVisitor.visitInsn(Opcodes.BASTORE);
	}

	@Test
	public void testVariableNonStatic_JD() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(JD)V", actualVisitor,
				arrayStrategy);
		pi.insertProbe(0);

		expectedVisitor.visitVarInsn(Opcodes.ALOAD, 5);
		expectedVisitor.visitInsn(Opcodes.ICONST_0);
		expectedVisitor.visitInsn(Opcodes.ICONST_1);
		expectedVisitor.visitInsn(Opcodes.BASTORE);
	}

	@Test
	public void testVisitCode() {
		ProbeInserter pi = new ProbeInserter(0, "m", "()V", actualVisitor,
				arrayStrategy);
		pi.visitCode();

		expectedVisitor.visitLdcInsn("init");
	}

	@Test
	public void testVisitClinit() {
		ProbeInserter pi = new ProbeInserter(0, "<clinit>", "()V",
				actualVisitor, arrayStrategy);
		pi.visitCode();

		expectedVisitor.visitLdcInsn("clinit");
	}

	@Test
	public void testVisitVarIns() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(II)V", actualVisitor,
				arrayStrategy);

		pi.visitVarInsn(Opcodes.ALOAD, 0);
		pi.visitVarInsn(Opcodes.ILOAD, 1);
		pi.visitVarInsn(Opcodes.ILOAD, 2);
		pi.visitVarInsn(Opcodes.ISTORE, 3);
		pi.visitVarInsn(Opcodes.FSTORE, 4);

		// Argument variables stay at the same position:
		expectedVisitor.visitVarInsn(Opcodes.ALOAD, 0);
		expectedVisitor.visitVarInsn(Opcodes.ILOAD, 1);
		expectedVisitor.visitVarInsn(Opcodes.ILOAD, 2);

		// Local variables are shifted by one:
		expectedVisitor.visitVarInsn(Opcodes.ISTORE, 4);
		expectedVisitor.visitVarInsn(Opcodes.FSTORE, 5);
	}

	@Test
	public void testVisitIincInsn() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(II)V", actualVisitor,
				arrayStrategy);
		pi.visitIincInsn(0, 100);
		pi.visitIincInsn(1, 101);
		pi.visitIincInsn(2, 102);
		pi.visitIincInsn(3, 103);
		pi.visitIincInsn(4, 104);

		// Argument variables stay at the same position:
		expectedVisitor.visitIincInsn(0, 100);
		expectedVisitor.visitIincInsn(1, 101);
		expectedVisitor.visitIincInsn(2, 102);

		// Local variables are shifted by one:
		expectedVisitor.visitIincInsn(4, 103);
		expectedVisitor.visitIincInsn(5, 104);
	}

	@Test
	public void testVisitLocalVariable() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(II)V", actualVisitor,
				arrayStrategy);

		pi.visitLocalVariable(null, null, null, null, null, 0);
		pi.visitLocalVariable(null, null, null, null, null, 1);
		pi.visitLocalVariable(null, null, null, null, null, 2);
		pi.visitLocalVariable(null, null, null, null, null, 3);
		pi.visitLocalVariable(null, null, null, null, null, 4);

		// Argument variables stay at the same position:
		expectedVisitor.visitLocalVariable(null, null, null, null, null, 0);
		expectedVisitor.visitLocalVariable(null, null, null, null, null, 1);
		expectedVisitor.visitLocalVariable(null, null, null, null, null, 2);

		// Local variables are shifted by one:
		expectedVisitor.visitLocalVariable(null, null, null, null, null, 4);
		expectedVisitor.visitLocalVariable(null, null, null, null, null, 5);
	}

	@Test
	public void should_remap_LocalVariableAnnotation() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(I)V", actualVisitor,
				arrayStrategy);

		final Label start = new Label();
		pi.visitLabel(start);
		final Label end = new Label();
		pi.visitLabel(end);

		pi.visitLocalVariableAnnotation(TypeReference.LOCAL_VARIABLE, null,
				new Label[] { start }, new Label[] { end }, new int[] { 2 },
				"LNonNull;", false);

		expectedVisitor.visitLabel(start);
		expectedVisitor.visitLabel(end);
		// Local variables are shifted by one:
		expectedVisitor.visitLocalVariableAnnotation(
				TypeReference.LOCAL_VARIABLE, null, new Label[] { start },
				new Label[] { end }, new int[] { 3 }, "LNonNull;", false);
	}

	@Test
	public void testVisitMaxs1() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(II)V", actualVisitor,
				arrayStrategy);
		pi.visitCode();
		pi.visitMaxs(0, 8);

		expectedVisitor.visitLdcInsn("init");
		expectedVisitor.visitMaxs(5, 9);
	}

	@Test
	public void testVisitMaxs2() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(II)V", actualVisitor,
				arrayStrategy);
		pi.visitCode();
		pi.visitMaxs(10, 8);

		expectedVisitor.visitLdcInsn("init");
		expectedVisitor.visitMaxs(13, 9);
	}

	@Test
	public void testVisitFrame() {
		ProbeInserter pi = new ProbeInserter(0, "m", "(J)V", actualVisitor,
				arrayStrategy);

		pi.visitFrame(Opcodes.F_NEW, 3,
				new Object[] { "Foo", Opcodes.LONG, "java/lang/String" }, 0,
				new Object[0]);

		expectedVisitor.visitFrame(Opcodes.F_NEW, 4,
				new Object[] { "Foo", Opcodes.LONG, "[Z", "java/lang/String" },
				0, new Object[0]);
	}

	@Test
	public void testVisitFrameNoLocals() {
		ProbeInserter pi = new ProbeInserter(Opcodes.ACC_STATIC, "m", "()V",
				actualVisitor, arrayStrategy);

		pi.visitFrame(Opcodes.F_NEW, 0, new Object[] {}, 0, new Object[0]);

		expectedVisitor.visitFrame(Opcodes.F_NEW, 1, new Object[] { "[Z" }, 0,
				new Object[0]);
	}

	@Test
	public void testVisitFrameProbeAt0() {
		ProbeInserter pi = new ProbeInserter(Opcodes.ACC_STATIC, "m", "()V",
				actualVisitor, arrayStrategy);

		pi.visitFrame(Opcodes.F_NEW, 2, new Object[] { Opcodes.DOUBLE, "Foo" },
				0, new Object[0]);

		expectedVisitor.visitFrame(Opcodes.F_NEW, 3,
				new Object[] { "[Z", Opcodes.DOUBLE, "Foo" }, 0, new Object[0]);
	}

	@Test
	public void testFillOneWord() {
		ProbeInserter pi = new ProbeInserter(Opcodes.ACC_STATIC, "m", "(I)V",
				actualVisitor, arrayStrategy);

		pi.visitFrame(Opcodes.F_NEW, 0, new Object[] {}, 0, new Object[] {});

		// The locals in this frame are filled with TOP up to the probe variable
		expectedVisitor.visitFrame(Opcodes.F_NEW, 2,
				new Object[] { Opcodes.TOP, "[Z", }, 0, new Object[] {});
	}

	@Test
	public void testFillTwoWord() {
		ProbeInserter pi = new ProbeInserter(Opcodes.ACC_STATIC, "m", "(J)V",
				actualVisitor, arrayStrategy);

		pi.visitFrame(Opcodes.F_NEW, 0, new Object[] {}, 0, new Object[] {});

		// The locals in this frame are filled with TOP up to the probe variable
		expectedVisitor.visitFrame(Opcodes.F_NEW, 3,
				new Object[] { Opcodes.TOP, Opcodes.TOP, "[Z", }, 0,
				new Object[] {});
	}

	@Test
	public void testFillPartly() {
		ProbeInserter pi = new ProbeInserter(Opcodes.ACC_STATIC, "m", "(DIJ)V",
				actualVisitor, arrayStrategy);

		pi.visitFrame(Opcodes.F_NEW, 1, new Object[] { Opcodes.DOUBLE }, 0,
				new Object[] {});

		// The locals in this frame are filled with TOP up to the probe variable
		expectedVisitor
				.visitFrame(
						Opcodes.F_NEW, 5, new Object[] { Opcodes.DOUBLE,
								Opcodes.TOP, Opcodes.TOP, Opcodes.TOP, "[Z", },
						0, new Object[] {});
	}

	@Test(expected = IllegalArgumentException.class)
	public void testVisitFrame_invalidType() {
		ProbeInserter pi = new ProbeInserter(0, "m", "()V", actualVisitor,
				arrayStrategy);
		pi.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
	}

}
