package com.vaadin.data;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;

import java.time.LocalDate;
import java.util.Arrays;

import org.junit.Assert;
import org.junit.Test;

import com.vaadin.annotations.PropertyId;
import com.vaadin.data.converter.StringToIntegerConverter;
import com.vaadin.data.validator.StringLengthValidator;
import com.vaadin.tests.data.bean.Address;
import com.vaadin.tests.data.bean.Person;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.AbstractTextField;
import com.vaadin.ui.DateField;
import com.vaadin.ui.FormLayout;
import com.vaadin.ui.TextField;

public class BinderInstanceFieldTest {

    public static class BindAllFields extends FormLayout {
        private TextField firstName;
        private DateField birthDate;
    }

    public static class BindFieldsUsingAnnotation extends FormLayout {
        @PropertyId("firstName")
        private TextField nameField;

        @PropertyId("birthDate")
        private DateField birthDateField;
    }

    public static class BindNestedFieldsUsingAnnotation extends FormLayout {
        @PropertyId("address.streetAddress")
        private TextField streetAddressField;
    }

    public static class BindDeepNestedFieldsUsingAnnotation extends FormLayout {
        @PropertyId("first.address.streetAddress")
        private TextField firstStreetField;

        @PropertyId("second.address.streetAddress")
        private TextField secondStreetField;
    }

    public static class BindDeepNestingFieldsWithCircularStructure
            extends FormLayout {
        @PropertyId("child.name")
        private TextField childName;

        @PropertyId("child.child.name")
        private TextField grandchildName;

        @PropertyId("child.child.child.child.child.child.child.child.name")
        private TextField eighthLevelGrandchildName;

        @PropertyId("child.child.child.child.child.child.child.child.child.child.child.child.child.name")
        private TextField distantGreatGrandchildName;
    }

    public static class BindOnlyOneField extends FormLayout {
        private TextField firstName;
        private TextField noFieldInPerson;
    }

    public static class BindWithNoFieldInPerson extends FormLayout {
        private TextField firstName;
        private DateField birthDate;
        private TextField noFieldInPerson;
    }

    public static class BindFieldHasWrongType extends FormLayout {
        private String firstName;
        private DateField birthDate;
    }

    public static class BindGenericField extends FormLayout {
        private CustomField<String> firstName;
    }

    public static class BindGenericWrongTypeParameterField extends FormLayout {
        private CustomField<Boolean> firstName;
    }

    public static class BindWrongTypeParameterField extends FormLayout {
        private IntegerTextField firstName;
    }

    public static class BindOneFieldRequiresConverter extends FormLayout {
        private TextField firstName;
        private TextField age;
    }

    public static class BindGeneric<T> extends FormLayout {
        private CustomField<T> firstName;
    }

    public static class BindRaw extends FormLayout {
        private CustomField firstName;
    }

    public static class BindAbstract extends FormLayout {
        private AbstractTextField firstName;
    }

    public static class BindNonInstantiatableType extends FormLayout {
        private NoDefaultCtor firstName;
    }

    public static class BindComplextHierarchyGenericType extends FormLayout {
        private ComplexHierarchy firstName;
    }

    public static class NoDefaultCtor extends TextField {
        public NoDefaultCtor(int arg) {
        }
    }

    public static class IntegerTextField extends CustomField<Integer> {

    }

    public static class ComplexHierarchy extends Generic<Long> {

    }

    public static class Generic<T> extends ComplexGeneric<Boolean, String, T> {

    }

    public static class ComplexGeneric<U, V, S> extends CustomField<V> {

    }

    public static class CustomField<T> extends AbstractField<T> {

        private T value;

        @Override
        public T getValue() {
            return value;
        }

        @Override
        protected void doSetValue(T value) {
            this.value = value;
        }

    }

    final static class Couple {
        Person first;
        Person second;

        public Person getFirst() {
            return first;
        }

        public Person getSecond() {
            return second;
        }

        public void setFirst(Person first) {
            this.first = first;
        }

        public void setSecond(Person second) {
            this.second = second;
        }
    }

    final class NestingStructure {
        NestingStructure child;
        String name;

        public NestingStructure getChild() {
            return child;
        }

        public void setChild(NestingStructure child) {
            this.child = child;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    @Test
    public void bindInstanceFields_bindAllFields() {
        BindAllFields form = new BindAllFields();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);

        Person person = new Person();
        person.setFirstName("foo");
        person.setBirthDate(LocalDate.now());

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.firstName.getValue());
        assertEquals(person.getBirthDate(), form.birthDate.getValue());

        form.firstName.setValue("bar");
        form.birthDate.setValue(person.getBirthDate().plusDays(345));

        assertEquals(form.firstName.getValue(), person.getFirstName());
        assertEquals(form.birthDate.getValue(), person.getBirthDate());
    }

    @Test(expected = IllegalStateException.class)
    public void bind_instanceFields_noArgsConstructor() {
        BindAllFields form = new BindAllFields();
        Binder<Person> binder = new Binder<>();
        binder.bindInstanceFields(form);
    }

    @Test
    public void bindInstanceFields_bindOnlyOneFields() {
        BindOnlyOneField form = new BindOnlyOneField();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);

        Person person = new Person();
        person.setFirstName("foo");

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.firstName.getValue());

        assertNull(form.noFieldInPerson);

        form.firstName.setValue("bar");

        assertEquals(form.firstName.getValue(), person.getFirstName());
    }

    @Test
    public void bindInstanceFields_bindNotHasValueField_fieldIsNull() {
        BindFieldHasWrongType form = new BindFieldHasWrongType();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);

        Person person = new Person();
        person.setFirstName("foo");

        binder.setBean(person);

        assertNull(form.firstName);
    }

    @Test
    public void bindInstanceFields_genericField() {
        BindGenericField form = new BindGenericField();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);

        Person person = new Person();
        person.setFirstName("foo");

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.firstName.getValue());

        form.firstName.setValue("bar");

        assertEquals(form.firstName.getValue(), person.getFirstName());
    }

    @Test(expected = IllegalStateException.class)
    public void bindInstanceFields_genericFieldWithWrongTypeParameter() {
        BindGenericWrongTypeParameterField form = new BindGenericWrongTypeParameterField();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);
    }

    @Test(expected = IllegalStateException.class)
    public void bindInstanceFields_generic() {
        BindGeneric<String> form = new BindGeneric<>();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);
    }

    @Test(expected = IllegalStateException.class)
    public void bindInstanceFields_rawFieldType() {
        BindRaw form = new BindRaw();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);
    }

    @Test(expected = IllegalStateException.class)
    public void bindInstanceFields_abstractFieldType() {
        BindAbstract form = new BindAbstract();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);
    }

    @Test(expected = IllegalStateException.class)
    public void bindInstanceFields_noInstantiatableFieldType() {
        BindNonInstantiatableType form = new BindNonInstantiatableType();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);
    }

    @Test(expected = IllegalStateException.class)
    public void bindInstanceFields_wrongFieldType() {
        BindWrongTypeParameterField form = new BindWrongTypeParameterField();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);
    }

    @Test
    public void bindInstanceFields_complexGenericHierarchy() {
        BindComplextHierarchyGenericType form = new BindComplextHierarchyGenericType();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);

        Person person = new Person();
        person.setFirstName("foo");

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.firstName.getValue());

        form.firstName.setValue("bar");

        assertEquals(form.firstName.getValue(), person.getFirstName());
    }

    @Test
    public void bindInstanceFields_bindNotHasValueField_fieldIsNotReplaced() {
        BindFieldHasWrongType form = new BindFieldHasWrongType();
        Binder<Person> binder = new Binder<>(Person.class);

        String name = "foo";
        form.firstName = name;

        Person person = new Person();
        person.setFirstName("foo");

        binder.setBean(person);

        assertEquals(name, form.firstName);
    }

    @Test
    public void bindInstanceFields_bindAllFieldsUsingAnnotations() {
        BindFieldsUsingAnnotation form = new BindFieldsUsingAnnotation();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.bindInstanceFields(form);

        Person person = new Person();
        person.setFirstName("foo");
        person.setBirthDate(LocalDate.now());

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.nameField.getValue());
        assertEquals(person.getBirthDate(), form.birthDateField.getValue());

        form.nameField.setValue("bar");
        form.birthDateField.setValue(person.getBirthDate().plusDays(345));

        assertEquals(form.nameField.getValue(), person.getFirstName());
        assertEquals(form.birthDateField.getValue(), person.getBirthDate());
    }

    @Test
    public void bindInstanceFields_bindNestedFieldUsingAnnotation() {
        BindNestedFieldsUsingAnnotation form = new BindNestedFieldsUsingAnnotation();
        Binder<Person> binder = new Binder<>(Person.class, true);
        binder.bindInstanceFields(form);

        Person person = new Person();
        Address address = new Address();
        address.setStreetAddress("Foo st.");
        person.setAddress(address);

        binder.setBean(person);

        Assert.assertEquals("Reading nested properties bound using annotation",
                person.getAddress().getStreetAddress(),
                form.streetAddressField.getValue());

        form.streetAddressField.setValue("Bar ave.");
        Assert.assertEquals("Changing nested properties bound using annotation",
                form.streetAddressField.getValue(),
                person.getAddress().getStreetAddress());
    }

    @Test
    public void bindInstanceFields_bindDeepNestedFieldsUsingAnnotation() {
        BindDeepNestedFieldsUsingAnnotation form = new BindDeepNestedFieldsUsingAnnotation();
        Binder<Couple> binder = new Binder<>(Couple.class, true);
        binder.bindInstanceFields(form);
        Person first = new Person();
        Person second = new Person();
        Address firstAddress = new Address();
        firstAddress.setStreetAddress("Foo st.");
        first.setAddress(firstAddress);
        Address secondAddress = new Address();
        second.setAddress(secondAddress);
        secondAddress.setStreetAddress("Bar ave.");
        Couple couple = new Couple();
        couple.setFirst(first);
        couple.setSecond(second);

        binder.setBean(couple);

        Assert.assertEquals("Binding deep nested properties using annotation",
                couple.first.getAddress().getStreetAddress(),
                form.firstStreetField.getValue());
        Assert.assertEquals(
                "Binding parallel deep nested properties using annotation",
                couple.second.getAddress().getStreetAddress(),
                form.secondStreetField.getValue());

        form.firstStreetField.setValue(second.getAddress().getStreetAddress());
        Assert.assertEquals("Updating value in deep nested properties",
                form.firstStreetField.getValue(),
                first.getAddress().getStreetAddress());
    }

    @Test
    public void bindInstanceFields_circular() {
        BindDeepNestingFieldsWithCircularStructure form = new BindDeepNestingFieldsWithCircularStructure();
        Binder<NestingStructure> binder = new Binder<>(NestingStructure.class,
                true);
        binder.bindInstanceFields(form);
        NestingStructure parent = new NestingStructure();
        parent.setName("parent");
        NestingStructure child = new NestingStructure();
        child.setName("child");
        parent.setChild(child);
        NestingStructure grandchild = new NestingStructure();
        grandchild.setName("grandchild");
        child.setChild(grandchild);
        NestingStructure root = grandchild;
        for (int i = 1; i < 15; i++) {
            NestingStructure ns = new NestingStructure();
            ns.setName("great " + root.getName());
            root.setChild(ns);
            root = ns;
        }
        binder.setBean(parent);
        Assert.assertEquals(child.getName(), form.childName.getValue());
        Assert.assertEquals(grandchild.getName(),
                form.grandchildName.getValue());
        Assert.assertNotNull(
                "Reading nested properties within default supported nested depth (max 10 levels)",
                form.eighthLevelGrandchildName);
        // only 10 levels of nesting properties are scanned by default
        Assert.assertNull(
                "By default, only 10 levels of nesting properties are scanned.",
                form.distantGreatGrandchildName);
    }

    @Test
    public void bindInstanceFields_customNestingLevel() {
        BindDeepNestingFieldsWithCircularStructure form = new BindDeepNestingFieldsWithCircularStructure();
        int customScanningDepth = 5;
        PropertyFilterDefinition shallowFilter = new PropertyFilterDefinition(
                customScanningDepth, Arrays.asList("java.lang"));
        Binder<NestingStructure> binder = new Binder<>(BeanPropertySet
                .get(NestingStructure.class, true, shallowFilter));
        binder.bindInstanceFields(form);
        NestingStructure parent = new NestingStructure();
        parent.setName("parent");
        NestingStructure child = new NestingStructure();
        child.setName("child");
        parent.setChild(child);
        NestingStructure grandchild = new NestingStructure();
        grandchild.setName("grandchild");
        child.setChild(grandchild);
        NestingStructure root = grandchild;
        for (int i = 1; i < 15; i++) {
            NestingStructure ns = new NestingStructure();
            ns.setName("great " + root.getName());
            root.setChild(ns);
            root = ns;
        }
        binder.setBean(parent);
        Assert.assertEquals(child.getName(), form.childName.getValue());
        Assert.assertEquals(
                "Reading 3rd level nesting works when custom scanning depth is 5",
                grandchild.getName(), form.grandchildName.getValue());
        Assert.assertNull(
                "Reading eighth level nesting doesn't work when custom scanning depth is 5",
                form.eighthLevelGrandchildName);
    }

    @Test
    public void bindInstanceFields_bindNotBoundFieldsOnly_customBindingIsNotReplaced() {
        BindAllFields form = new BindAllFields();
        Binder<Person> binder = new Binder<>(Person.class);

        TextField name = new TextField();
        form.firstName = name;
        binder.forField(form.firstName)
                .withValidator(
                        new StringLengthValidator("Name is invalid", 3, 10))
                .bind("firstName");

        binder.bindInstanceFields(form);

        Person person = new Person();
        String personName = "foo";
        person.setFirstName(personName);
        person.setBirthDate(LocalDate.now());

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.firstName.getValue());
        assertEquals(person.getBirthDate(), form.birthDate.getValue());
        // the instance is not overridden
        assertEquals(name, form.firstName);

        // Test automatic binding
        form.birthDate.setValue(person.getBirthDate().plusDays(345));
        assertEquals(form.birthDate.getValue(), person.getBirthDate());

        // Test custom binding
        form.firstName.setValue("aa");
        assertEquals(personName, person.getFirstName());

        assertFalse(binder.validate().isOk());
    }

    @Test
    public void bindInstanceFields_fieldsAreConfigured_customBindingIsNotReplaced() {
        BindWithNoFieldInPerson form = new BindWithNoFieldInPerson();
        Binder<Person> binder = new Binder<>(Person.class);

        TextField name = new TextField();
        form.firstName = name;
        binder.forField(form.firstName)
                .withValidator(
                        new StringLengthValidator("Name is invalid", 3, 10))
                .bind("firstName");
        TextField ageField = new TextField();
        form.noFieldInPerson = ageField;
        binder.forField(form.noFieldInPerson)
                .withConverter(new StringToIntegerConverter(""))
                .bind(Person::getAge, Person::setAge);

        binder.bindInstanceFields(form);

        Person person = new Person();
        String personName = "foo";
        int age = 11;
        person.setFirstName(personName);
        person.setAge(age);

        binder.setBean(person);

        assertEquals(person.getFirstName(), form.firstName.getValue());
        assertEquals(String.valueOf(person.getAge()),
                form.noFieldInPerson.getValue());
        // the instances are not overridden
        assertEquals(name, form.firstName);
        assertEquals(ageField, form.noFieldInPerson);

        // Test correct age
        age += 56;
        form.noFieldInPerson.setValue(String.valueOf(age));
        assertEquals(form.noFieldInPerson.getValue(),
                String.valueOf(person.getAge()));

        // Test incorrect name
        form.firstName.setValue("aa");
        assertEquals(personName, person.getFirstName());

        assertFalse(binder.validate().isOk());
    }

    @Test
    public void bindInstanceFields_preconfiguredFieldNotBoundToPropertyPreserved() {
        BindOneFieldRequiresConverter form = new BindOneFieldRequiresConverter();
        form.age = new TextField();
        form.firstName = new TextField();
        Binder<Person> binder = new Binder<>(Person.class);
        binder.forField(form.age)
                .withConverter(str -> Integer.parseInt(str) / 2,
                        integer -> Integer.toString(integer * 2))
                .bind(Person::getAge, Person::setAge);
        binder.bindInstanceFields(form);
        Person person = new Person();
        person.setFirstName("first");
        person.setAge(45);
        binder.setBean(person);
        assertEquals("90", form.age.getValue());
    }

    @Test
    public void bindInstanceFields_explicitelyBoundFieldAndNotBoundField() {
        BindOnlyOneField form = new BindOnlyOneField();
        Binder<Person> binder = new Binder<>(Person.class);

        binder.forField(new TextField()).bind("firstName");

        binder.bindInstanceFields(form);
    }

    @Test
    public void bindInstanceFields_tentativelyBoundFieldAndNotBoundField() {
        BindOnlyOneField form = new BindOnlyOneField();
        Binder<Person> binder = new Binder<>(Person.class);

        TextField field = new TextField();
        form.firstName = field;

        // This is an incomplete binding which is supposed to be configured
        // manually
        binder.forMemberField(field);

        // bindInstanceFields will not complain even though it can't bind
        // anything as there is a binding in progress (an exception will be
        // thrown later if the binding is not completed)
        binder.bindInstanceFields(form);
    }
}
