/*
 * 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
 *
 *   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.apache.bcel.generic;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.apache.bcel.AbstractTest;
import org.apache.bcel.Const;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ArrayElementValue;
import org.apache.bcel.classfile.ElementValue;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.ParameterAnnotationEntry;
import org.apache.bcel.classfile.SimpleElementValue;
import org.apache.bcel.util.SyntheticRepository;
import org.junit.jupiter.api.Test;

/**
 * The program that some of the tests generate looks like this:
 *
 * <pre>
 * public class HelloWorld {
 *     public static void main(String[] argv) {
 *         BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
 *         String name = null;
 *
 *         try {
 *             name = &quot;Andy&quot;;
 *         } catch (IOException e) {
 *             return;
 *         }
 *         System.out.println(&quot;Hello, &quot; + name);
 *     }
 * }
 * </pre>
 */
class GeneratingAnnotatedClassesTest extends AbstractTest {
    private void assertArrayElementValue(final int nExpectedArrayValues, final AnnotationEntry anno) {
        final ElementValuePair elementValuePair = anno.getElementValuePairs()[0];
        assertEquals("value", elementValuePair.getNameString());
        final ArrayElementValue ev = (ArrayElementValue) elementValuePair.getValue();
        final ElementValue[] eva = ev.getElementValuesArray();
        assertEquals(nExpectedArrayValues, eva.length);
    }

    private void assertMethodAnnotations(final Method method, final int expectedNumberAnnotations, final int nExpectedArrayValues) {
        final String methodName = method.getName();
        final AnnotationEntry[] annos = method.getAnnotationEntries();
        assertEquals(expectedNumberAnnotations, annos.length, () -> "For " + methodName);
        if (expectedNumberAnnotations != 0) {
            assertArrayElementValue(nExpectedArrayValues, annos[0]);
        }
    }

    private void assertParameterAnnotations(final Method method, final int... expectedNumberOfParmeterAnnotations) {
        final String methodName = "For " + method.getName();
        final ParameterAnnotationEntry[] parameterAnnotations = method.getParameterAnnotationEntries();
        assertEquals(expectedNumberOfParmeterAnnotations.length, parameterAnnotations.length, methodName);

        int i = 0;
        for (final ParameterAnnotationEntry parameterAnnotation : parameterAnnotations) {
            final AnnotationEntry[] annos = parameterAnnotation.getAnnotationEntries();
            final int expectedLength = expectedNumberOfParmeterAnnotations[i++];
            final int j = i;
            assertEquals(expectedLength, annos.length, () -> methodName + " parameter " + j);
            if (expectedLength != 0) {
                assertSimpleElementValue(annos[0]);
            }
        }
        assertNotNull(method.getAttribute(Const.ATTR_RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS));
        assertNull(method.getAttribute(Const.ATTR_RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS));
    }

    private void assertSimpleElementValue(final AnnotationEntry anno) {
        final ElementValuePair elementValuePair = anno.getElementValuePairs()[0];
        assertEquals("id", elementValuePair.getNameString());
        final SimpleElementValue ev = (SimpleElementValue) elementValuePair.getValue();
        assertEquals(42, ev.getValueInt());
    }

    private void buildClassContents(final ClassGen cg, final ConstantPoolGen cp, final InstructionList il) {
        // Create method 'public static void main(String[]argv)'
        final MethodGen mg = createMethodGen("main", il, cp);
        final InstructionFactory factory = new InstructionFactory(cg);
        // We now define some often used types:
        final ObjectType inStream = new ObjectType("java.io.InputStream");
        final ObjectType printStream = new ObjectType("java.io.PrintStream");
        // Create variables in and name : We call the constructors, that is,
        // execute BufferedReader(InputStreamReader(System.in)) . The reference
        // to the BufferedReader object stays on top of the stack and is stored
        // in the newly allocated in variable.
        il.append(factory.createNew("java.io.BufferedReader"));
        il.append(InstructionConst.DUP); // Use predefined constant
        il.append(factory.createNew("java.io.InputStreamReader"));
        il.append(InstructionConst.DUP);
        il.append(factory.createFieldAccess("java.lang.System", "in", inStream, Const.GETSTATIC));
        il.append(factory.createInvoke("java.io.InputStreamReader", "<init>", Type.VOID, new Type[] {inStream}, Const.INVOKESPECIAL));
        il.append(factory.createInvoke("java.io.BufferedReader", "<init>", Type.VOID, new Type[] {new ObjectType("java.io.Reader")}, Const.INVOKESPECIAL));
        LocalVariableGen lg = mg.addLocalVariable("in", new ObjectType("java.io.BufferedReader"), null, null);
        final int in = lg.getIndex();
        lg.setStart(il.append(new ASTORE(in))); // "in" valid from here
        // Create local variable name and initialize it to null
        lg = mg.addLocalVariable("name", Type.STRING, null, null);
        final int name = lg.getIndex();
        il.append(InstructionConst.ACONST_NULL);
        lg.setStart(il.append(new ASTORE(name))); // "name" valid from here
        // Create try-catch block: We remember the start of the block, read a
        // line from the standard input and store it into the variable name.
        // InstructionHandle try_start = il.append(factory.createFieldAccess(
        // "java.lang.System", "out", p_stream, Constants.GETSTATIC));
        // il.append(new PUSH(cp, "Please enter your name> "));
        // il.append(factory.createInvoke("java.io.PrintStream", "print",
        // Type.VOID, new Type[] { Type.STRING },
        // Constants.INVOKEVIRTUAL));
        // il.append(new ALOAD(in));
        // il.append(factory.createInvoke("java.io.BufferedReader", "readLine",
        // Type.STRING, Type.NO_ARGS, Constants.INVOKEVIRTUAL));
        final InstructionHandle tryStart = il.append(new PUSH(cp, "Andy"));
        il.append(new ASTORE(name));
        // Upon normal execution we jump behind exception handler, the target
        // address is not known yet.
        final GOTO g = new GOTO(null);
        final InstructionHandle tryEnd = il.append(g);
        // We add the exception handler which simply returns from the method.
        final LocalVariableGen varEx = mg.addLocalVariable("ex", Type.getType("Ljava.io.IOException;"), null, null);
        final int varExSlot = varEx.getIndex();
        final InstructionHandle handler = il.append(new ASTORE(varExSlot));
        varEx.setStart(handler);
        varEx.setEnd(il.append(InstructionConst.RETURN));
        mg.addExceptionHandler(tryStart, tryEnd, handler, new ObjectType("java.io.IOException"));
        // "Normal" code continues, now we can set the branch target of the GOTO
        // .
        final InstructionHandle ih = il.append(factory.createFieldAccess("java.lang.System", "out", printStream, Const.GETSTATIC));
        g.setTarget(ih);
        // Printing "Hello": String concatenation compiles to StringBuffer
        // operations.
        il.append(factory.createNew(Type.STRINGBUFFER));
        il.append(InstructionConst.DUP);
        il.append(new PUSH(cp, "Hello, "));
        il.append(factory.createInvoke("java.lang.StringBuffer", "<init>", Type.VOID, new Type[] {Type.STRING}, Const.INVOKESPECIAL));
        il.append(new ALOAD(name));
        il.append(factory.createInvoke("java.lang.StringBuffer", "append", Type.STRINGBUFFER, new Type[] {Type.STRING}, Const.INVOKEVIRTUAL));
        il.append(factory.createInvoke("java.lang.StringBuffer", "toString", Type.STRING, Type.NO_ARGS, Const.INVOKEVIRTUAL));
        il.append(factory.createInvoke("java.io.PrintStream", "println", Type.VOID, new Type[] {Type.STRING}, Const.INVOKEVIRTUAL));
        il.append(InstructionConst.RETURN);
        // Finalization: Finally, we have to set the stack size, which normally
        // would have to be computed on the fly and add a default constructor
        // method to the class, which is empty in this case.
        mg.setMaxStack();
        mg.setMaxLocals();
        cg.addMethod(mg.getMethod());
        il.dispose(); // Allow instruction handles to be reused
        cg.addEmptyConstructor(Const.ACC_PUBLIC);
    }

    private void buildClassContentsWithAnnotatedMethods(final ClassGen cg, final ConstantPoolGen cp, final InstructionList il) {
        // Create method 'public static void main(String[]argv)'
        final MethodGen mg = createMethodGen("main", il, cp);
        final InstructionFactory factory = new InstructionFactory(cg);
        mg.addAnnotationEntry(createSimpleVisibleAnnotation(mg.getConstantPool()));
        // We now define some often used types:
        final ObjectType inStream = new ObjectType("java.io.InputStream");
        final ObjectType printStream = new ObjectType("java.io.PrintStream");
        // Create variables in and name : We call the constructors, that is,
        // execute BufferedReader(InputStreamReader(System.in)) . The reference
        // to the BufferedReader object stays on top of the stack and is stored
        // in the newly allocated in variable.
        il.append(factory.createNew("java.io.BufferedReader"));
        il.append(InstructionConst.DUP); // Use predefined constant
        il.append(factory.createNew("java.io.InputStreamReader"));
        il.append(InstructionConst.DUP);
        il.append(factory.createFieldAccess("java.lang.System", "in", inStream, Const.GETSTATIC));
        il.append(factory.createInvoke("java.io.InputStreamReader", "<init>", Type.VOID, new Type[] {inStream}, Const.INVOKESPECIAL));
        il.append(factory.createInvoke("java.io.BufferedReader", "<init>", Type.VOID, new Type[] {new ObjectType("java.io.Reader")}, Const.INVOKESPECIAL));
        LocalVariableGen lg = mg.addLocalVariable("in", new ObjectType("java.io.BufferedReader"), null, null);
        final int in = lg.getIndex();
        lg.setStart(il.append(new ASTORE(in))); // "in" valid from here
        // Create local variable name and initialize it to null
        lg = mg.addLocalVariable("name", Type.STRING, null, null);
        final int name = lg.getIndex();
        il.append(InstructionConst.ACONST_NULL);
        lg.setStart(il.append(new ASTORE(name))); // "name" valid from here
        // Create try-catch block: We remember the start of the block, read a
        // line from the standard input and store it into the variable name.
        // InstructionHandle try_start = il.append(factory.createFieldAccess(
        // "java.lang.System", "out", p_stream, Constants.GETSTATIC));
        // il.append(new PUSH(cp, "Please enter your name> "));
        // il.append(factory.createInvoke("java.io.PrintStream", "print",
        // Type.VOID, new Type[] { Type.STRING },
        // Constants.INVOKEVIRTUAL));
        // il.append(new ALOAD(in));
        // il.append(factory.createInvoke("java.io.BufferedReader", "readLine",
        // Type.STRING, Type.NO_ARGS, Constants.INVOKEVIRTUAL));
        final InstructionHandle tryStart = il.append(new PUSH(cp, "Andy"));
        il.append(new ASTORE(name));
        // Upon normal execution we jump behind exception handler, the target
        // address is not known yet.
        final GOTO g = new GOTO(null);
        final InstructionHandle tryEnd = il.append(g);
        // We add the exception handler which simply returns from the method.
        final LocalVariableGen varEx = mg.addLocalVariable("ex", Type.getType("Ljava.io.IOException;"), null, null);
        final int varExSlot = varEx.getIndex();
        final InstructionHandle handler = il.append(new ASTORE(varExSlot));
        varEx.setStart(handler);
        varEx.setEnd(il.append(InstructionConst.RETURN));
        mg.addExceptionHandler(tryStart, tryEnd, handler, new ObjectType("java.io.IOException"));
        // "Normal" code continues, now we can set the branch target of the GOTO
        // .
        final InstructionHandle ih = il.append(factory.createFieldAccess("java.lang.System", "out", printStream, Const.GETSTATIC));
        g.setTarget(ih);
        // Printing "Hello": String concatenation compiles to StringBuffer
        // operations.
        il.append(factory.createNew(Type.STRINGBUFFER));
        il.append(InstructionConst.DUP);
        il.append(new PUSH(cp, "Hello, "));
        il.append(factory.createInvoke("java.lang.StringBuffer", "<init>", Type.VOID, new Type[] {Type.STRING}, Const.INVOKESPECIAL));
        il.append(new ALOAD(name));
        il.append(factory.createInvoke("java.lang.StringBuffer", "append", Type.STRINGBUFFER, new Type[] {Type.STRING}, Const.INVOKEVIRTUAL));
        il.append(factory.createInvoke("java.lang.StringBuffer", "toString", Type.STRING, Type.NO_ARGS, Const.INVOKEVIRTUAL));
        il.append(factory.createInvoke("java.io.PrintStream", "println", Type.VOID, new Type[] {Type.STRING}, Const.INVOKEVIRTUAL));
        il.append(InstructionConst.RETURN);
        // Finalization: Finally, we have to set the stack size, which normally
        // would have to be computed on the fly and add a default constructor
        // method to the class, which is empty in this case.
        mg.setMaxStack();
        mg.setMaxLocals();
        cg.addMethod(mg.getMethod());
        il.dispose(); // Allow instruction handles to be reused
        cg.addEmptyConstructor(Const.ACC_PUBLIC);
    }

    // helper methods
    private ClassGen createClassGen(final String className) {
        return new ClassGen(className, "java.lang.Object", "<generated>", Const.ACC_PUBLIC | Const.ACC_SUPER, null);
    }

    public AnnotationEntryGen createCombinedAnnotation(final ConstantPoolGen cp) {
        // Create an annotation instance
        final AnnotationEntryGen a = createSimpleVisibleAnnotation(cp);
        final ArrayElementValueGen array = new ArrayElementValueGen(cp);
        array.addElement(new AnnotationElementValueGen(a, cp));
        final ElementValuePairGen nvp = new ElementValuePairGen("value", array, cp);
        final List<ElementValuePairGen> elements = new ArrayList<>();
        elements.add(nvp);
        return new AnnotationEntryGen(new ObjectType("CombinedAnnotation"), elements, true, cp);
    }

    public AnnotationEntryGen createFruitAnnotation(final ConstantPoolGen cp, final String aFruit) {
        final SimpleElementValueGen evg = new SimpleElementValueGen(ElementValueGen.STRING, cp, aFruit);
        final ElementValuePairGen nvGen = new ElementValuePairGen("fruit", evg, cp);
        final ObjectType t = new ObjectType("SimpleStringAnnotation");
        final List<ElementValuePairGen> elements = new ArrayList<>();
        elements.add(nvGen);
        return new AnnotationEntryGen(t, elements, true, cp);
    }

    private MethodGen createMethodGen(final String methodname, final InstructionList il, final ConstantPoolGen cp) {
        return new MethodGen(Const.ACC_STATIC | Const.ACC_PUBLIC, // access
            // flags
            Type.VOID, // return type
            new Type[] {new ArrayType(Type.STRING, 1)}, // argument
            // types
            new String[] {"argv"}, // arg names
            methodname, "HelloWorld", // method, class
            il, cp);
    }

    public AnnotationEntryGen createSimpleInvisibleAnnotation(final ConstantPoolGen cp) {
        final SimpleElementValueGen evg = new SimpleElementValueGen(ElementValueGen.PRIMITIVE_INT, cp, 4);
        final ElementValuePairGen nvGen = new ElementValuePairGen("id", evg, cp);
        final ObjectType t = new ObjectType("SimpleAnnotation");
        final List<ElementValuePairGen> elements = new ArrayList<>();
        elements.add(nvGen);
        return new AnnotationEntryGen(t, elements, false, cp);
    }

    public AnnotationEntryGen createSimpleVisibleAnnotation(final ConstantPoolGen cp) {
        final SimpleElementValueGen evg = new SimpleElementValueGen(ElementValueGen.PRIMITIVE_INT, cp, 4);
        final ElementValuePairGen nvGen = new ElementValuePairGen("id", evg, cp);
        final ObjectType t = new ObjectType("SimpleAnnotation");
        final List<ElementValuePairGen> elements = new ArrayList<>();
        elements.add(nvGen);
        return new AnnotationEntryGen(t, elements, true, cp);
    }

    private void dumpClass(final ClassGen cg, final String fname) {
        try {
            final File f = createTestdataFile(fname);
            cg.getJavaClass().dump(f);
        } catch (final java.io.IOException e) {
            System.err.println(e);
        }
    }

    private void dumpClass(final ClassGen cg, final String dir, final String fname) {
        dumpClass(cg, dir + File.separator + fname);
    }

    private JavaClass getClassFrom(final String where, final String clazzname) throws ClassNotFoundException {
        // System.out.println(where);
        final SyntheticRepository repos = createRepos(where);
        return repos.loadClass(clazzname);
    }

    /**
     * Steps in the test:
     * <ol>
     * <li>Programmatically construct the HelloWorld program</li>
     * <li>Add two simple annotations at the class level</li>
     * <li>Save the class to disk</li>
     * <li>Reload the class using the 'static' variant of the BCEL classes</li>
     * <li>Check the attributes are OK</li>
     * </ol>
     */
    @Test
    void testGenerateClassLevelAnnotations() throws ClassNotFoundException {
        // Create HelloWorld
        final ClassGen cg = createClassGen("HelloWorld");
        cg.setMajor(49);
        cg.setMinor(0);
        final ConstantPoolGen cp = cg.getConstantPool();
        final InstructionList il = new InstructionList();
        cg.addAnnotationEntry(createSimpleVisibleAnnotation(cp));
        cg.addAnnotationEntry(createSimpleInvisibleAnnotation(cp));
        buildClassContents(cg, cp, il);
        // System.out.println(cg.getJavaClass().toString());
        dumpClass(cg, "HelloWorld.class");
        final JavaClass jc = getClassFrom(".", "HelloWorld");
        final AnnotationEntry[] as = jc.getAnnotationEntries();
        assertEquals(2, as.length, "Wrong number of AnnotationEntries");
        // TODO L??;
        assertEquals("LSimpleAnnotation;", as[0].getAnnotationType(), "Wrong name of annotation 1");
        assertEquals("LSimpleAnnotation;", as[1].getAnnotationType(), "Wrong name of annotation 2");
        final ElementValuePair[] vals = as[0].getElementValuePairs();
        final ElementValuePair nvp = vals[0];
        assertEquals("id", nvp.getNameString(), "Wrong name of element in SimpleAnnotation");
        final ElementValue ev = nvp.getValue();
        assertEquals(ElementValue.PRIMITIVE_INT, ev.getElementValueType(), "Wrong type of element value");
        assertEquals("4", ev.stringifyValue(), "Wrong value of element");
        assertTrue(createTestdataFile("HelloWorld.class").delete());
    }

    /**
     * Just check that we can dump a class that has a method annotation on it and it is still there when we read it back in
     */
    @Test
    void testGenerateMethodLevelAnnotations1() throws ClassNotFoundException {
        // Create HelloWorld
        final ClassGen cg = createClassGen("HelloWorld");
        final ConstantPoolGen cp = cg.getConstantPool();
        final InstructionList il = new InstructionList();
        buildClassContentsWithAnnotatedMethods(cg, cp, il);
        // Check annotation is OK
        int i = cg.getMethods()[0].getAnnotationEntries().length;
        assertEquals(1, i, "Wrong number of annotations of main method prior to dumping");
        dumpClass(cg, "temp1" + File.separator + "HelloWorld.class");
        final JavaClass jc2 = getClassFrom("temp1", "HelloWorld");
        // Check annotation is OK
        i = jc2.getMethods()[0].getAnnotationEntries().length;
        assertEquals(1, i, "Wrong number of annotation on JavaClass");
        final ClassGen cg2 = new ClassGen(jc2);
        // Check it now it is a ClassGen
        final Method[] m = cg2.getMethods();
        i = m[0].getAnnotationEntries().length;
        assertEquals(1, i, "Wrong number of annotations on the main 'Method'");
        final FieldGenOrMethodGen mg = new MethodGen(m[0], cg2.getClassName(), cg2.getConstantPool());
        // Check it finally when the Method is changed to a MethodGen
        i = mg.getAnnotationEntries().length;
        assertEquals(1, i, "Wrong number of annotations on the main 'MethodGen'");

        assertTrue(delete("temp1", "HelloWorld.class"));
    }

    /**
     * Going further than the last test - when we reload the method back in, let's change it (adding a new annotation) and
     * then store that, read it back in and verify both annotations are there ! Also check that we can remove method
     * annotations.
     */
    @Test
    void testGenerateMethodLevelAnnotations2() throws ClassNotFoundException {
        // Create HelloWorld
        final ClassGen cg = createClassGen("HelloWorld");
        final ConstantPoolGen cp = cg.getConstantPool();
        final InstructionList il = new InstructionList();
        buildClassContentsWithAnnotatedMethods(cg, cp, il);
        dumpClass(cg, "temp2", "HelloWorld.class");
        final JavaClass jc2 = getClassFrom("temp2", "HelloWorld");
        final ClassGen cg2 = new ClassGen(jc2);
        // Main method after reading the class back in
        final Method mainMethod1 = jc2.getMethods()[0];
        assertEquals(1, mainMethod1.getAnnotationEntries().length, "Wrong number of annotations of the 'Method'");
        final MethodGen mainMethod2 = new MethodGen(mainMethod1, cg2.getClassName(), cg2.getConstantPool());
        assertEquals(1, mainMethod2.getAnnotationEntries().length, "Wrong number of annotations of the 'MethodGen'");
        final AnnotationEntryGen fruit = createFruitAnnotation(cg2.getConstantPool(), "Pear");
        mainMethod2.addAnnotationEntry(fruit);
        cg2.removeMethod(mainMethod1);
        cg2.addMethod(mainMethod2.getMethod());
        dumpClass(cg2, "temp3", "HelloWorld.class");
        final JavaClass jc3 = getClassFrom("temp3", "HelloWorld");
        final ClassGen cg3 = new ClassGen(jc3);
        final Method mainMethod3 = cg3.getMethods()[1];
        final int i = mainMethod3.getAnnotationEntries().length;
        assertEquals(2, i, "Wrong number of annotations on the 'Method'");
        mainMethod2.removeAnnotationEntry(fruit);
        assertEquals(1, mainMethod2.getAnnotationEntries().length, "Wrong number of annotations on the 'MethodGen'");
        mainMethod2.removeAnnotationEntries();
        assertEquals(0, mainMethod2.getAnnotationEntries().length, 0, "Wrong number of annotations on the 'MethodGen'");
        assertTrue(delete("temp2", "HelloWorld.class"));
        assertTrue(delete("temp3", "HelloWorld.class"));
    }

    /**
     * Load a class in and modify it with a new attribute - A SimpleAnnotation annotation
     */
    @Test
    void testModifyingClasses1() throws ClassNotFoundException {
        final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME + ".data.SimpleAnnotatedClass");
        final ClassGen cgen = new ClassGen(jc);
        final ConstantPoolGen cp = cgen.getConstantPool();
        cgen.addAnnotationEntry(createFruitAnnotation(cp, "Pineapple"));
        assertEquals(2, cgen.getAnnotationEntries().length, "Wrong number of annotations");
        dumpClass(cgen, "SimpleAnnotatedClass.class");
        assertTrue(delete("SimpleAnnotatedClass.class"));
    }

    /**
     * Load a class in and modify it with a new attribute - A ComplexAnnotation annotation
     */
    @Test
    void testModifyingClasses2() throws ClassNotFoundException {
        final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME + ".data.SimpleAnnotatedClass");
        final ClassGen cgen = new ClassGen(jc);
        final ConstantPoolGen cp = cgen.getConstantPool();
        cgen.addAnnotationEntry(createCombinedAnnotation(cp));
        assertEquals(2, cgen.getAnnotationEntries().length, "Wrong number of annotations");
        dumpClass(cgen, "SimpleAnnotatedClass.class");
        final JavaClass jc2 = getClassFrom(".", "SimpleAnnotatedClass");
        jc2.getAnnotationEntries();
        assertTrue(delete("SimpleAnnotatedClass.class"));
        // System.err.println(jc2.toString());
    }

    /**
     * Transform simple class from an immutable to a mutable object. The class is annotated with an annotation that uses an
     * array of SimpleAnnotations.
     */
    @Test
    void testTransformClassToClassGen_ArrayAndAnnotationTypes() throws ClassNotFoundException {
        final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME + ".data.AnnotatedWithCombinedAnnotation");
        final ClassGen cgen = new ClassGen(jc);
        // Check annotations are correctly preserved
        final AnnotationEntryGen[] annotations = cgen.getAnnotationEntries();
        assertEquals(1, annotations.length, "Wrong number of annotations");
        final AnnotationEntryGen a = annotations[0];
        assertEquals(1, a.getValues().size(), "Wrong number of values for the annotation");
        final ElementValuePairGen nvp = a.getValues().get(0);
        final ElementValueGen value = nvp.getValue();
        assertInstanceOf(ArrayElementValueGen.class, value, "Value should be ArrayElementValueGen but is " + value);
        final ArrayElementValueGen arrayValue = (ArrayElementValueGen) value;
        assertEquals(1, arrayValue.getElementValuesSize(), "Wrong size of the array");
        final ElementValueGen innerValue = arrayValue.getElementValues().get(0);
        assertInstanceOf(AnnotationElementValueGen.class, innerValue, "Value in the array should be AnnotationElementValueGen but is " + innerValue);
        final AnnotationElementValueGen innerAnnotationValue = (AnnotationElementValueGen) innerValue;
        assertEquals("L" + PACKAGE_BASE_SIG + "/data/SimpleAnnotation;", innerAnnotationValue.getAnnotation().getTypeSignature(), "Wrong type signature");

        // check the three methods
        final Method[] methods = cgen.getMethods();
        assertEquals(3, methods.length);
        for (final Method method : methods) {
            final String methodName = method.getName();
            switch (methodName) {
            case "<init>":
                assertMethodAnnotations(method, 0, 1);
                assertParameterAnnotations(method, 0, 1);
                break;
            case "methodWithArrayOfZeroAnnotations":
                assertMethodAnnotations(method, 1, 0);
                break;
            case "methodWithArrayOfTwoAnnotations":
                assertMethodAnnotations(method, 1, 2);
                break;
            default:
                fail(() -> "unexpected method " + method.getName());
                break;
            }
        }
    }

    /**
     * Transform simple class from an immutable to a mutable object. The class is annotated with an annotation that uses an
     * enum.
     */
    @Test
    void testTransformClassToClassGen_EnumType() throws ClassNotFoundException {
        final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME + ".data.AnnotatedWithEnumClass");
        final ClassGen cgen = new ClassGen(jc);
        // Check annotations are correctly preserved
        final AnnotationEntryGen[] annotations = cgen.getAnnotationEntries();
        assertEquals(1, annotations.length, "Wrong number of annotations");
    }

    // J5TODO: Need to add deleteFile calls to many of these tests

    /**
     * Transform simple class from an immutable to a mutable object.
     */
    @Test
    void testTransformClassToClassGen_SimpleTypes() throws ClassNotFoundException {
        final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME + ".data.SimpleAnnotatedClass");
        final ClassGen cgen = new ClassGen(jc);
        // Check annotations are correctly preserved
        final AnnotationEntryGen[] annotations = cgen.getAnnotationEntries();
        assertEquals(1, annotations.length, "Wrong number of annotations");
    }

    /**
     * Transform complex class from an immutable to a mutable object.
     */
    @Test
    void testTransformComplexClassToClassGen() throws ClassNotFoundException {
        final JavaClass jc = getTestJavaClass(PACKAGE_BASE_NAME + ".data.ComplexAnnotatedClass");
        final ClassGen cgen = new ClassGen(jc);
        // Check annotations are correctly preserved
        final AnnotationEntryGen[] annotations = cgen.getAnnotationEntries();
        assertEquals(1, annotations.length, "Wrong number of annotations");
        final List<?> l = annotations[0].getValues();
        boolean found = false;
        for (final Object name : l) {
            final ElementValuePairGen element = (ElementValuePairGen) name;
            if (element.getNameString().equals("dval") && element.getValue().stringifyValue().equals("33.4")) {
                found = true;
            }
        }
        assertTrue(found, "Did not find double annotation value with value 33.4");
    }
}
