package org.hamcrest.beans;

import org.hamcrest.*;
import org.hamcrest.core.IsEqual;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;
import static org.hamcrest.beans.HasPropertyWithValue.hasPropertyAtPath;
import static org.hamcrest.core.IsAnything.anything;
import static org.hamcrest.core.IsEqual.equalTo;

/**
 * @author Iain McGinniss
 * @author Nat Pryce
 * @author Steve Freeman
 * @since 1.1.0
 */
@SuppressWarnings("UnusedDeclaration")
public class HasPropertyWithValueTest extends AbstractMatcherTest {

  private final BeanWithoutInfo shouldMatch = new BeanWithoutInfo("is expected", true);
  private final BeanWithoutInfo shouldNotMatch = new BeanWithoutInfo("not expected", false);

  private final BeanWithInfo beanWithInfo = new BeanWithInfo("with info");

  @Override
  protected Matcher<?> createMatcher() {
    return hasProperty("irrelevant", anything());
  }

  public void testMatchesBeanWithoutInfoWithMatchedNamedProperty() {
    final Matcher<BeanWithoutInfo> propertyMatcher = hasProperty("property", equalTo("is expected"));

    assertMatches("with property", propertyMatcher, shouldMatch);
    assertMismatchDescription("property 'property' was \"not expected\"", propertyMatcher, shouldNotMatch);
  }

  public void testMatchesBeanWithoutInfoWithMatchedNamedBooleanProperty() {
    final Matcher<BeanWithoutInfo> booleanPropertyMatcher = hasProperty("booleanProperty", is(true));

    assertMatches("with property", booleanPropertyMatcher, shouldMatch);
    assertMismatchDescription("property 'booleanProperty' was <false>", booleanPropertyMatcher, shouldNotMatch);
  }

  public void testMatchesBeanWithInfoWithMatchedNamedProperty() {
    assertMatches("with bean info", hasProperty("property", equalTo("with info")), beanWithInfo);
    assertMismatchDescription("property 'property' was \"with info\"",
        hasProperty("property", equalTo("without info")), beanWithInfo);
  }

  public void testDoesNotMatchBeanWithoutInfoOrMatchedNamedProperty() {
    assertMismatchDescription("No property \"nonExistentProperty\"",
                              hasProperty("nonExistentProperty", anything()), shouldNotMatch);
   }

  public void testDoesNotMatchWriteOnlyProperty() {
    assertMismatchDescription("property \"writeOnlyProperty\" is not readable",
                              hasProperty("writeOnlyProperty", anything()), shouldNotMatch);
  }

  public void testMatchesPath() {
    assertMatches("1-step path", hasPropertyAtPath("property", equalTo("is expected")), shouldMatch);
    assertMatches("2-step path", hasPropertyAtPath("inner.property", equalTo("is expected")), new BeanWithInner(shouldMatch));
    assertMatches("3-step path", hasPropertyAtPath("inner.inner.property", equalTo("is expected")), new BeanWithInner(new BeanWithInner(shouldMatch)));

    assertMismatchDescription("inner.No property \"wrong\"", hasPropertyAtPath("inner.wrong.property", anything()), new BeanWithInner(new BeanWithInner(shouldMatch)));
    assertMismatchDescription("inner.inner.property.was \"not expected\"", hasPropertyAtPath("inner.inner.property", equalTo("something")), new BeanWithInner(new BeanWithInner(shouldNotMatch)));
  }

  public void testDescribeTo() {
    assertDescription("hasProperty(\"property\", <true>)", hasProperty("property", equalTo(true)));
  }

  public void testMatchesPropertyAndValue() {
    assertMatches("property with value", hasProperty("property", anything()), beanWithInfo);
  }

  public void testDoesNotWriteMismatchIfPropertyMatches() {
    Description description = new StringDescription();
    hasProperty( "property", anything()).describeMismatch(beanWithInfo, description);
    assertEquals("Expected mismatch description", "", description.toString());
  }

  public void testDescribesMissingPropertyMismatch() {
    assertMismatchDescription("No property \"honk\"", hasProperty("honk", anything()), shouldNotMatch);
  }

  public void testExceptionsInBeanMethodsShouldBeReportedCorrectly() {
    assertMismatchDescription(
      "Calling 'public java.lang.String org.hamcrest.beans.HasPropertyWithValueTest$BeanWithBug.getBroken()': \"bean failed\"",
      hasProperty("broken", anything()),
      new BeanWithBug());
  }

  public void testCanAccessAnAnonymousInnerClass() {
    class X implements IX {
      @Override
      public int getTest() {
        return 1;
      }
    }

    assertThat(new X(), HasPropertyWithValue.hasProperty("test", IsEqual.equalTo(1)));
  }

  interface IX {
    int getTest();
  }

  @SuppressWarnings("WeakerAccess")
  public static class BeanWithoutInfo {
    private String property;
    private final boolean booleanProperty;

    public BeanWithoutInfo(String property, boolean booleanProperty) {
      this.property = property;
      this.booleanProperty = booleanProperty;
    }

    public String getProperty() {
      return property;
    }

    public void setProperty(String property) {
      this.property = property;
    }

    public boolean isBooleanProperty() { return booleanProperty; }

    public void setWriteOnlyProperty(@SuppressWarnings("unused") float property) {
    }

    @Override
    public String toString() {
      return "[Person: " + property + "]";
    }
  }

  @SuppressWarnings("WeakerAccess")
  public static class BeanWithInner {
    private final Object inner;

    public BeanWithInner(Object inner) { this.inner = inner; }
    public Object getInner() { return inner; }
  }

  @SuppressWarnings("WeakerAccess")
  public static class BeanWithInfo {
    private final String propertyValue;

    public BeanWithInfo(String propertyValue) { this.propertyValue = propertyValue; }
    public String property() { return propertyValue; }
  }

  public static class BeanWithInfoBeanInfo extends SimpleBeanInfo {
    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
      try {
        return new PropertyDescriptor[] {
            new PropertyDescriptor("property", BeanWithInfo.class, "property", null)
          };
      } catch (IntrospectionException e) {
        throw new AssertionError("Introspection exception", e);
      }
    }
  }

  @SuppressWarnings("WeakerAccess")
  public static class BeanWithBug {
    public String getBroken() {
      throw new BeanFailed();
    }
  }

  @SuppressWarnings("WeakerAccess")
  public static class BeanFailed extends RuntimeException {
    public BeanFailed() { super("bean failed"); }
  }

}
