package ioc.specs

import java.awt.Image
import java.beans.*
import java.lang.reflect.Method

import org.apache.tapestry5.beaneditor.DataType
import org.apache.tapestry5.beaneditor.Validate
import org.apache.tapestry5.ioc.annotations.Scope
import org.apache.tapestry5.ioc.internal.services.*
import org.apache.tapestry5.ioc.internal.util.Pair
import org.apache.tapestry5.ioc.internal.util.StringLongPair
import org.apache.tapestry5.ioc.services.ClassPropertyAdapter
import org.apache.tapestry5.ioc.services.PropertyAccess

import spock.lang.*

class ExceptionBean {

  boolean getFailure() {
    throw new RuntimeException("getFailure");
  }

  void setFailure(boolean b) {
    throw new RuntimeException("setFailure");
  }

  @Override
  String toString() {
    return "PropertyAccessImplSpecBean";
  }
}

class UglyBean {
}

class UglyBeanBeanInfo implements BeanInfo {

  BeanInfo[] getAdditionalBeanInfo() {
    return new BeanInfo[0];
  }

  BeanDescriptor getBeanDescriptor() {
    return null;
  }

  int getDefaultEventIndex() {
    return 0;
  }

  int getDefaultPropertyIndex() {
    return 0;
  }

  EventSetDescriptor[] getEventSetDescriptors() {
    return new EventSetDescriptor[0];
  }

  Image getIcon(int iconKind) {
    return null;
  }

  MethodDescriptor[] getMethodDescriptors() {
    return new MethodDescriptor[0];
  }

  PropertyDescriptor[] getPropertyDescriptors() {
    throw new RuntimeException("This is the UglyBean.");
  }

}

class ScalaBean {

  private String value;

  String getValue() {
    return value;
  }

  void setValue(String value) {
    this.value = value;
  }

  String value() {
    return value;
  }

  void value_$eq(String value) {
    this.value = value;
  }
}

class ScalaClass {

  private String value;

  String value() {
    return value;
  }

  void value_$eq(String value) {
    this.value = value;
  }
}

interface BeanInterface {

  String getValue();

  void setValue(String v);

  String getOtherValue();

  void setOtherValue(String v);

  int getIntValue(); // read-only
}

abstract class AbstractBean implements BeanInterface {
  // abstract class implements method from interface
  private String other;

  String getOtherValue() {
    return other;
  }

  void setOtherValue(String v) {
    other = v;
  }
}

class ConcreteBean extends AbstractBean {

  private String value;
  private int intValue;

  ConcreteBean(int intValue) {
    this.intValue = intValue;
  }

  String getValue() {
    return value;
  }

  void setValue(String v) {
    value = v;
  }

  int getIntValue() {
    return intValue;
  }
}

abstract class GenericBean<T> {

  public T value;
}

class GenericStringBean extends GenericBean<String> {
}

class PropertyAccessImplSpec extends Specification {

  @Shared
  PropertyAccess access = new PropertyAccessImpl()

  @Shared
  Random random = new Random()

  def "simple read access to a standard bean"() {
    Bean b = new Bean()
    int value = random.nextInt()

    when:

    b.value = value

    then:

    access.get(b, "value") == value
  }

  def "property name access is case insensitive"() {
    Bean b = new Bean()
    int value = random.nextInt()

    when:

    b.value = value

    then:

    access.get(b, "VaLUe") == value
  }

  def "simple write access to a standard bean"() {
    Bean b = new Bean()
    int value = random.nextInt()

    when:

    access.set(b, "value", value)

    then:

    b.value == value
  }

  def "missing properties are an exception"() {
    Bean b = new Bean()

    when:

    access.get(b, "zaphod")

    then:

    IllegalArgumentException e = thrown()

    e.message == "Class ${b.class.name} does not contain a property named 'zaphod'."
  }

  def "it is not possible to update a read-only property"() {
    Bean b = new Bean()

    when:

    access.set(b, "class", null)

    then:

    UnsupportedOperationException e = thrown()

    e.message == "Class ${b.class.name} does not provide a mutator ('setter') method for property 'class'."
  }

  def "it is not possible to read a write-only property"() {
    Bean b = new Bean()

    when:

    access.get(b, "writeOnly")

    then:

    UnsupportedOperationException e = thrown()

    e.message == "Class ${b.class.name} does not provide an accessor ('getter') method for property 'writeOnly'."
  }

  def "when a getter method throws an exception, the exception is wrapped and rethrown"() {

    ExceptionBean b = new ExceptionBean()

    when:

    access.get(b, "failure")

    then:

    RuntimeException e = thrown()

    e.message == "Error reading property 'failure' of ${b}: getFailure"
  }

  def "when a setter method throws an exception, the exception is wrapped and rethrown"() {
    ExceptionBean b = new ExceptionBean()

    when:

    access.set(b, "failure", false)

    then:

    RuntimeException e = thrown()

    e.message == "Error updating property 'failure' of ${b.class.name}: setFailure"
  }

  @Ignore
  def "exception throw when introspecting the class is wrapped and rethrown"() {

    // Due to Groovy, the exception gets thrown here, not inside
    // the access.get() method, thus @Ingore (for now)

    UglyBean b = new UglyBean()

    when:

    access.get(b, "google")

    then:

    RuntimeException e = thrown()

    e.message == "java.lang.RuntimeException: This is the UglyBean."
  }

  def "clearCache() wipes internal cache"() {
    when:

    ClassPropertyAdapter cpa1 = access.getAdapter Bean

    then:

    cpa1.is(access.getAdapter(Bean))


    when:

    access.clearCache()

    then:

    !cpa1.is(access.getAdapter(Bean))
  }

  def "ClassPropertyAdapter has a useful toString()"() {

    when:

    def cpa = access.getAdapter Bean

    then:

    cpa.toString() == "<ClassPropertyAdaptor ${Bean.class.name}: PI, class, readOnly, value, writeOnly>"
  }

  @Unroll
  def "expected properties for #beanClass.name property '#propertyName' are read=#read, update=#update, castRequired=#castRequired"() {

    when:

    def pa = getPropertyAdapter beanClass, propertyName

    then:

    pa.read == read
    pa.update == update
    pa.castRequired == castRequired
    pa.writeMethod == writeMethod
    pa.readMethod == readMethod

    where:

    beanClass | propertyName | read  | update | castRequired | writeMethodName | readMethodName
    Bean      | "readOnly"   | true  | false  | false        | null            | "getReadOnly"
    Bean      | "writeOnly"  | false | true   | false        | "setWriteOnly"  | null
    Bean      | "pi"         | true  | false  | false        | null            | null

    writeMethod = findMethod beanClass, writeMethodName
    readMethod = findMethod beanClass, readMethodName
  }

  def "PropertyAdapter for unknown property name is null"() {
    when:

    ClassPropertyAdapter cpa = access.getAdapter(Bean)

    then:

    cpa.getPropertyAdapter("google") == null
  }

  @Unroll
  def "PropertyAdapter.type for #beanClass.name property '#propertyName' is #type.name"() {

    ClassPropertyAdapter cpa = access.getAdapter(beanClass)

    when:

    def adapter = cpa.getPropertyAdapter(propertyName)

    then:

    adapter.type.is(type)

    where:

    beanClass | propertyName | type
    Bean      | "value"      | int
    Bean      | "readOnly"   | String
    Bean      | "writeOnly"  | boolean
  }

  def "ClassPropertyAdapter gives access to property names (in sorted order)"() {
    ClassPropertyAdapter cpa = access.getAdapter(Bean)

    expect:

    cpa.propertyNames == ["PI", "class", "readOnly", "value", "writeOnly"]
  }

  def "public static fields are treated as properties"() {
    when:

    def adapter = getPropertyAdapter Bean, "pi"

    then:

    adapter.get(null).is(Bean.PI)
  }

  def "public final static fields may not be updated"() {
    def adapter = getPropertyAdapter Bean, "pi"

    when:

    adapter.set(null, 3.0d)

    then:

    RuntimeException e = thrown()

    e.message.contains "final"
    e.message.contains "PI"
  }

  def "super interface methods are inherited by sub-interface"() {
    when:

    ClassPropertyAdapter cpa = access.getAdapter SubInterface

    then:

    cpa.propertyNames == ["grandParentProperty", "parentProperty", "subProperty"]
  }

  def "indexed properties are ignored"() {
    when:

    ClassPropertyAdapter cpa = access.getAdapter BeanWithIndexedProperty

    then:

    cpa.propertyNames == ["class", "primitiveProperty"]
  }

  def "getAnnotation from a bean property"() {
    AnnotatedBean b = new AnnotatedBean()

    when:

    def annotation = access.getAnnotation(b, "annotationOnRead", Scope)

    then:

    annotation.value() == "onread"
  }


  def "getAnnotation() when annotation is not present is null"() {

    when:

    def pa = getPropertyAdapter AnnotatedBean, "readWrite"

    then:

    pa.getAnnotation(Scope) == null
  }

  def "getAnnotation() with annotation on setter method"() {

    when:

    def pa = getPropertyAdapter AnnotatedBean, "annotationOnWrite"

    then:

    pa.getAnnotation(Scope).value() == "onwrite"
  }

  def "annotation on getter method overrides annotation on setter method"() {
    def pa = getPropertyAdapter AnnotatedBean, "annotationOnRead"

    when:

    Scope annotation = pa.getAnnotation(Scope)

    then:

    annotation.value() == "onread"
  }

  def "getAnnotation() works on read-only properties, skipping the missing setter method"() {

    when:

    def pa = getPropertyAdapter AnnotatedBean, "readOnly"

    then:

    pa.getAnnotation(Scope) == null
  }

  def "annotations directly on fields are located"() {
    when:

    def pa = access.getAdapter(Bean).getPropertyAdapter("value")

    then:

    pa.getAnnotation(DataType).value() == "fred"
  }

  @Issue("TAPESTY-2448")
  def "getAnnotation() will find annotations from an inherited field in a super-class"() {
    when:

    def pa = getPropertyAdapter AnnotatedBeanSubclass, "value"

    then:

    pa.getAnnotation(DataType).value() == "fred"
  }

  def "annotations on a getter or setter method override annotations on the field"() {
    when:

    def pa = getPropertyAdapter Bean, "value"

    then:

    pa.getAnnotation(Validate).value() == "getter-value-overrides"
  }

  def "PropertyAdapter.type understands (simple) generic signatures"() {
    def cpa1 = access.getAdapter(StringLongPair)

    when:

    def key = cpa1.getPropertyAdapter("key")

    then:

    key.type == String
    key.castRequired
    key.declaringClass == Pair

    when:

    def value = cpa1.getPropertyAdapter("value")

    then:

    value.type == Long
    value.castRequired

    when:

    def cpa2 = access.getAdapter(Pair)
    def pkey = cpa2.getPropertyAdapter("key")

    then:

    pkey.type == Object
    !pkey.castRequired

    when:

    def pvalue = cpa2.getPropertyAdapter("value")

    then:

    pvalue.type == Object
    !pvalue.castRequired
  }

  def "PropertyAdapter prefers JavaBeans property method names to Scala method names"() {
    when:

    def pa = getPropertyAdapter ScalaBean, "value"

    then:

    pa.readMethod.name == "getValue"
    pa.writeMethod.name == "setValue"
  }

  def "PropertyAdapter understands Scala accessor method naming"() {
    when:

    def pa = getPropertyAdapter ScalaClass, "value"

    then:

    pa.readMethod.name == "value"
    pa.writeMethod.name == 'value_$eq'
  }

  def "PropertyAccess exposes public fields as if they were properties"() {
    when:

    def pa = getPropertyAdapter PublicFieldBean, "value"

    then:

    pa.field
    pa.read
    pa.update

    when:

    PublicFieldBean bean = new PublicFieldBean()

    pa.set(bean, "fred")

    then:

    bean.value == "fred"

    when:

    bean.value = "barney"

    then:

    pa.get(bean) == "barney"
  }

  def "access to property is favored over public field when the names are the same"() {
    def bean = new ShadowedPublicFieldBean()

    when:

    def pa = getPropertyAdapter ShadowedPublicFieldBean, "value"

    then:

    !pa.field

    when:

    pa.set(bean, "fred")

    then:

    bean.@value == null

    when:

    bean.@value = "barney"
    bean.value = "wilma"

    then:

    pa.get(bean) == "wilma"
  }

  def "a property defined by an unimplemented inteface method of an abstract class is accessible"() {
    AbstractBean bean = new ConcreteBean(33)
    def ca = access.getAdapter(AbstractBean)

    when:

    def va = ca.getPropertyAdapter("value")

    then:

    !va.field

    when:

    va.set(bean, "hello")

    then:

    va.get(bean) == "hello"
    bean.value == "hello"

    when:

    def ova = ca.getPropertyAdapter("otherValue")

    then:

    !ova.field

    when:

    ova.set(bean, "other value")

    then:

    ova.get(bean) == "other value"
    bean.otherValue == "other value"

    when:

    def iva = ca.getPropertyAdapter("intvalue")

    then:

    iva.get(bean) == 33
    iva.read
    !iva.update
    !iva.field
  }

  def "generic field is recognized"() {
    when:
    def pa = getPropertyAdapter GenericStringBean, "value"

    then:

    pa.castRequired
    pa.type == String
    pa.declaringClass == GenericBean
  }
  
  interface GetterInterface {
    int getValue();
  }
        
  interface SetterGetterInterface extends GetterInterface {
    void setValue(int value);
  }
  
  interface SetterInterface {
    void setValue(int value);
  }
          
  interface GetterSetterInterface extends SetterInterface {
    int getValue();
  }
  
  final class GetterSetterClass implements GetterSetterInterface {
    public void setValue(int value) {}
    public int getValue() {}
  }
    
  // TAP5-1885
  def "split properties (getter in one supertype, setter in another)"() {
        
    when:
    def pa1 = getPropertyAdapter SetterGetterInterface, "value";
    def pa2 = getPropertyAdapter GetterSetterInterface, "value";
    def pa3 = getPropertyAdapter GetterSetterClass, "value";
      
    then:
    pa1.isRead();
    pa1.isUpdate();
    pa2.isRead();
    pa2.isUpdate();
    pa3.isRead();
    pa3.isUpdate();
  }
  
  public interface Baz { String getBar(); }
 
  public class AbstractFoo implements Baz {
    private String bar;
    public String getBar() { return bar; }
    public void setBar(String bar){ this.bar =bar; }
  }
  
  public class Foo extends AbstractFoo {}
  
  // TAP5-1548
  def "property expressions fails when using a supertype that implements an interface with a matching method"() {
        
    when:
    def pa = getPropertyAdapter AbstractFoo, "bar";
      
    then:
    pa.isRead();
    pa.isUpdate();
  }
  
  public static interface Entity<T extends Serializable>
  { 
    T getId(); 
  }
  
  public static interface NamedEntity extends Entity<Long> { 
    String getName(); 
  }

  // TAP5-1480
  def "exception when creating property conduits for generic interfaces"() {
    when:
    def paId = getPropertyAdapter Entity, "id";
    def paName = getPropertyAdapter NamedEntity, "name";
    
    then:
    paId != null
    paName != null
  }
  
  def getPropertyAdapter(clazz, name) {
    access.getAdapter(clazz).getPropertyAdapter(name)
  }

  private Method findMethod(Class beanClass, String methodName) {
    return beanClass.methods.find { it.name == methodName }
  }
}
