/*
 * 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
 *
 *     http://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 com.datastax.driver.mapping;

import static org.testng.Assert.assertEquals;

import com.datastax.driver.core.CCMTestsSupport;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.google.common.io.Closeables;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
import org.testng.annotations.Test;

@SuppressWarnings("unused")
public class SyntheticFieldsMapperTest extends CCMTestsSupport {

  @Override
  public void onTestContextInitialized() {
    execute("CREATE TABLE synthetic_fields (id int PRIMARY KEY)");
  }

  /**
   * Test that synthetic fields are ignored by the {@code AnnotationParser} (JAVA-465).
   *
   * <p>This test creates a modified version of this class where {@code futureSynthetic} is marked
   * as synthetic, and therefore ignored by the {@code AnnotationParser}. If the test throws an
   * error "Cannot find matching getter and setter for field 'futureSynthetic'", it means that the
   * field is not ignored by the mapped. However, if it succeed, we know that the field was ignored
   * and that the fix does the right job.
   *
   * <p>If the class {@code ClassWithSyntheticField} was used as-is, the {@code Mapper} would
   * complain that there are no getter and setter for the field {@code futureSynthetic}.
   *
   * <p>Note that we could also have used not-static inner classes, but the {@code Mapper} is never
   * able to instantiate those as they require a reference to their enclosing class that is not
   * provided at instantiation time.
   */
  @Test(groups = "short")
  public void should_ignore_synthetic_fields() {
    Class<?> classWithSyntheticFields = makeTestClassWithSyntheticFields();
    Object instance = instantiateNewClass(classWithSyntheticFields, 42);

    // Here we cannot use ClassWithSyntheticField since it is not the one the source file was
    // compiled against
    // Instead, it is a different Class (identity + ClassLoader), a modified version of
    // ClassWithSyntheticField
    // Nice ClassCastException will be thrown if we try to cast it to ClassWithSyntheticField
    @SuppressWarnings("unchecked")
    Mapper<Object> m =
        (Mapper<Object>) new MappingManager(session()).mapper(classWithSyntheticFields);
    m.save(instance);

    assertEquals(m.get(42), instance);
  }

  private Class<?> makeTestClassWithSyntheticFields() {
    InputStream stream = null;
    try {
      // Get class bytes
      Class<ClassWithSyntheticField> c = ClassWithSyntheticField.class;
      String classAsPath = c.getName().replace('.', '/') + ".class";
      stream = c.getClassLoader().getResourceAsStream(classAsPath);

      // Make "futureSynthetic" field actually synthetic
      ClassWriter cw = new ClassWriter(0);
      ClassVisitor cv = new SyntheticFieldCreator(Opcodes.ASM5, cw);
      ClassReader cr = new ClassReader(stream);
      cr.accept(cv, 0);
      byte[] updatedClassBytes = cw.toByteArray();

      // Build the new class
      return new InterceptingClassLoader()
          .defineClass(ClassWithSyntheticField.class.getName(), updatedClassBytes);
    } catch (IOException e) {
      throw new RuntimeException("Could not read Class bytes", e);
    } finally {
      try {
        Closeables.close(stream, true);
      } catch (IOException ignored) {
      }
    }
  }

  private Object instantiateNewClass(Class<?> classWithSyntheticFields, int id) {
    try {
      Constructor<?> declaredConstructor =
          classWithSyntheticFields.getDeclaredConstructor(int.class);
      return declaredConstructor.newInstance(id);
    } catch (Exception e) {
      throw new RuntimeException("Could not instantiate Class", e);
    }
  }

  @Table(name = "synthetic_fields")
  public static class ClassWithSyntheticField {
    @PartitionKey private int id;

    // Intentionally broken class: there is no getter/setter for this field
    private int futureSynthetic;

    // This constructor will be used by the Mapper
    @SuppressWarnings("unused")
    public ClassWithSyntheticField() {}

    // This constructor is invoked using reflection
    @SuppressWarnings("unused")
    public ClassWithSyntheticField(int id) {
      this.id = id;
    }

    // Getters & Setters used by the Mapper
    @SuppressWarnings("unused")
    public int getId() {
      return id;
    }

    // Getters & Setters used by the Mapper
    @SuppressWarnings("unused")
    public void setId(int id) {
      this.id = id;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      ClassWithSyntheticField that = (ClassWithSyntheticField) o;

      return id == that.id;
    }

    @Override
    public int hashCode() {
      return id;
    }
  }

  private static class SyntheticFieldCreator extends ClassVisitor {
    public SyntheticFieldCreator(int api, ClassVisitor cv) {
      super(api, cv);
    }

    @Override
    public FieldVisitor visitField(
        int access, String name, String desc, String signature, Object value) {
      int newAccesses = access;
      if ("futureSynthetic".equals(name)) {
        newAccesses = access + Opcodes.ACC_SYNTHETIC;
      }
      return super.visitField(newAccesses, name, desc, signature, value);
    }
  }

  private static class InterceptingClassLoader extends ClassLoader {
    public Class defineClass(String name, byte[] b) {
      return defineClass(name, b, 0, b.length);
    }
  }
}
