/*
 * Copyright 2009 the original author or authors.
 *
 * Licensed 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 org.gradle.api.internal;

import groovy.lang.Closure;
import groovy.lang.MissingPropertyException;
import org.gradle.api.Action;
import org.gradle.api.DomainObjectCollection;
import org.gradle.api.Rule;
import org.gradle.api.UnknownDomainObjectException;
import org.gradle.api.specs.Spec;
import org.gradle.api.specs.Specs;
import org.gradle.util.*;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Iterator;

import static org.gradle.util.HelperUtil.*;
import static org.gradle.util.WrapUtil.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

@RunWith(JMock.class)
public class DefaultNamedDomainObjectContainerTest {
    private final ClassGenerator classGenerator = new AsmBackedClassGenerator();
    private final DefaultNamedDomainObjectContainer<Bean> container = classGenerator.newInstance(DefaultNamedDomainObjectContainer.class, Bean.class, classGenerator);
    private final JUnit4Mockery context = new JUnit4Mockery();

    @Test
    public void usesTypeNameToGenerateDisplayName() {
        assertThat(container.getTypeDisplayName(), equalTo("Bean"));
        assertThat(container.getDisplayName(), equalTo("Bean container"));
    }

    @Test
    public void canGetAllDomainObjectsForEmptyContainer() {
        assertTrue(container.getAll().isEmpty());
    }

    @Test
    public void canGetAllDomainObjectsOrderedByName() {
        Bean bean1 = new Bean();
        Bean bean2 = new Bean();
        Bean bean3 = new Bean();

        container.addObject("b", bean2);
        container.addObject("a", bean1);
        container.addObject("c", bean3);

        assertThat(container.getAll(), equalTo(toLinkedSet(bean1, bean2, bean3)));
    }

    @Test
    public void canIterateOverEmptyContainer() {
        Iterator<Bean> iterator = container.iterator();
        assertFalse(iterator.hasNext());
    }

    @Test
    public void canIterateOverDomainObjectsOrderedByName() {
        Bean bean1 = new Bean();
        Bean bean2 = new Bean();
        Bean bean3 = new Bean();

        container.addObject("b", bean2);
        container.addObject("a", bean1);
        container.addObject("c", bean3);

        Iterator<Bean> iterator = container.iterator();
        assertThat(iterator.next(), sameInstance(bean1));
        assertThat(iterator.next(), sameInstance(bean2));
        assertThat(iterator.next(), sameInstance(bean3));
        assertFalse(iterator.hasNext());
    }

    @Test
    public void canGetAllDomainObjectsAsMapForEmptyContainer() {
        assertTrue(container.getAsMap().isEmpty());
    }

    @Test
    public void canGetAllDomainObjectsAsMap() {
        Bean bean1 = new Bean();
        Bean bean2 = new Bean();
        Bean bean3 = new Bean();

        container.addObject("b", bean2);
        container.addObject("a", bean1);
        container.addObject("c", bean3);

        assertThat(container.getAsMap(), equalTo(GUtil.map("a", bean1, "b", bean2, "c", bean3)));
    }

    @Test
    public void canGetAllMatchingDomainObjectsOrderedByName() {
        Bean bean1 = new Bean();
        final Bean bean2 = new Bean();
        Bean bean3 = new Bean();

        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element == bean2;
            }
        };

        container.addObject("a", bean1);
        container.addObject("b", bean2);
        container.addObject("c", bean3);

        assertThat(container.findAll(spec), equalTo(toLinkedSet(bean2)));
    }

    @Test
    public void getAllMatchingDomainObjectsReturnsEmptySetWhenNoMatches() {
        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return false;
            }
        };

        container.addObject("a", new Bean());

        assertTrue(container.findAll(spec).isEmpty());
    }

    @Test
    public void canGetFilteredCollectionContainingAllObjectsWhichMeetSpec() {
        final Bean bean1 = new Bean();
        Bean bean2 = new Bean();
        Bean bean3 = new Bean();

        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element != bean1;
            }
        };

        TestClosure testClosure = new TestClosure() {
            public Object call(Object param) {
                return param != bean1;
            }
        };

        container.addObject("a", bean1);
        container.addObject("b", bean2);
        container.addObject("c", bean3);

        assertThat(container.matching(spec).getAll(), equalTo(toLinkedSet(bean2, bean3)));
        assertThat(container.matching(HelperUtil.toClosure(testClosure)).getAll(), equalTo(toLinkedSet(bean2, bean3)));
        assertThat(container.matching(spec).findByName("a"), nullValue());
        assertThat(container.matching(spec).findByName("b"), sameInstance(bean2));
    }

    @Test
    public void canGetFilteredCollectionContainingAllObjectsWhichHaveType() {
        class OtherBean extends Bean {
        }
        Bean bean1 = new Bean();
        OtherBean bean2 = new OtherBean();
        Bean bean3 = new Bean();

        container.addObject("c", bean3);
        container.addObject("a", bean1);
        container.addObject("b", bean2);

        assertThat(container.withType(Bean.class).getAll(), equalTo(toLinkedSet(bean1, bean2, bean3)));
        assertThat(container.withType(OtherBean.class).getAll(), equalTo(toLinkedSet(bean2)));
        assertThat(container.withType(OtherBean.class).findByName("a"), nullValue());
        assertThat(container.withType(OtherBean.class).findByName("b"), sameInstance(bean2));
    }

    @Test
    public void filteredCollectionIsLive() {
        final Bean bean1 = new Bean();
        Bean bean2 = new Bean();
        Bean bean3 = new Bean();
        Bean bean4 = new Bean();

        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element != bean1;
            }
        };

        container.addObject("a", bean1);

        DomainObjectCollection<Bean> filteredCollection = container.matching(spec);
        assertTrue(filteredCollection.getAll().isEmpty());

        container.addObject("b", bean2);
        container.addObject("c", bean3);

        assertThat(filteredCollection.getAll(), equalTo(toLinkedSet(bean2, bean3)));

        container.addObject("c", bean4);

        assertThat(filteredCollection.getAll(), equalTo(toLinkedSet(bean2, bean4)));
    }

    @Test
    public void filteredCollectionExecutesActionWhenMatchingObjectAdded() {
        final Action<Bean> action = context.mock(Action.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(action).execute(bean);
        }});

        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element == bean;
            }
        };

        container.matching(spec).whenObjectAdded(action);

        container.addObject("bean", bean);
        container.addObject("bean2", new Bean());
    }

    @Test
    public void filteredCollectionExecutesClosureWhenMatchingObjectAdded() {
        final TestClosure closure = context.mock(TestClosure.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(closure).call(bean);
        }});

        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element == bean;
            }
        };

        container.matching(spec).whenObjectAdded(HelperUtil.toClosure(closure));

        container.addObject("bean", bean);
        container.addObject("bean2", new Bean());
    }

    @Test
    public void canChainFilteredCollections() {
        final Bean bean = new Bean();
        final Bean bean2 = new Bean();
        final Bean bean3 = new Bean();

        Spec<Bean> spec = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element != bean;
            }
        };
        Spec<Bean> spec2 = new Spec<Bean>() {
            public boolean isSatisfiedBy(Bean element) {
                return element != bean2;
            }
        };

        container.addObject("a", bean);
        container.addObject("b", bean2);
        container.addObject("c", bean3);

        DomainObjectCollection<Bean> collection = container.matching(spec).matching(spec2);
        assertThat(collection.getAll(), equalTo(toSet(bean3)));
    }

    @Test
    public void canGetDomainObjectByName() {
        Bean bean = new Bean();
        container.addObject("a", bean);

        assertThat(container.getByName("a"), sameInstance(bean));
        assertThat(container.getAt("a"), sameInstance(bean));
    }

    @Test
    public void getDomainObjectByNameFailsForUnknownDomainObject() {
        try {
            container.getByName("unknown");
            fail();
        } catch (UnknownDomainObjectException e) {
            assertThat(e.getMessage(), equalTo("Bean with name 'unknown' not found."));
        }
    }

    @Test
    public void getDomainObjectInvokesRuleForUnknownDomainObject() {
        Bean bean = new Bean();
        addRuleFor(bean);

        assertThat(container.getByName("bean"), sameInstance(bean));
    }

    @Test
    public void canConfigureDomainObjectByName() {
        Bean bean = new Bean();
        container.addObject("a", bean);

        assertThat(container.getByName("a", toClosure("{ beanProperty = 'hi' }")), sameInstance(bean));
        assertThat(bean.getBeanProperty(), equalTo("hi"));
    }

    @Test
    public void configureDomainObjectInvokesRuleForUnknownDomainObject() {
        Bean bean = new Bean();
        addRuleFor(bean);

        assertThat(container.getByName("bean", toClosure("{ beanProperty = 'hi' }")), sameInstance(bean));
        assertThat(bean.getBeanProperty(), equalTo("hi"));
    }

    @Test
    public void canFindDomainObjectByName() {
        Bean bean = new Bean();
        container.addObject("a", bean);

        assertThat(container.findByName("a"), sameInstance(bean));
    }

    @Test
    public void findDomainObjectByNameReturnsNullForUnknownDomainObject() {
        assertThat(container.findByName("a"), nullValue());
    }

    @Test
    public void findDomainObjectByNameInvokesRulesForUnknownDomainObject() {
        Bean bean = new Bean();
        addRuleFor(bean);

        assertThat(container.findByName("bean"), sameInstance(bean));
    }

    @Test
    public void findDomainObjectByNameInvokesNestedRulesOnlyOnceForUnknownDomainObject() {
        final Bean bean1 = new Bean();
        final Bean bean2 = new Bean();
        container.addRule(new Rule() {
            public String getDescription() {
                return "rule1";
            }

            public void apply(String domainObjectName) {
                if (domainObjectName.equals("bean1")) {
                    container.addObject("bean1", bean1);
                }
            }
        });
        container.addRule(new Rule() {
            private boolean applyHasBeenCalled;

            public String getDescription() {
                return "rule2";
            }

            public void apply(String domainObjectName) {
                if (domainObjectName.equals("bean2")) {
                    assertThat(applyHasBeenCalled, equalTo(false));
                    container.findByName("bean1");
                    container.findByName("bean2");
                    container.addObject("bean2", bean2);
                    applyHasBeenCalled = true;
                }
            }
        });
        container.findByName("bean2");
        assertThat(container.getAll(), equalTo(WrapUtil.toSet(bean1, bean2)));
    }

    @Test
    public void callsActionWhenObjectAdded() {
        final Action<Bean> action = context.mock(Action.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(action).execute(bean);
        }});

        container.whenObjectAdded(action);
        container.addObject("bean", bean);
    }

    @Test
    public void callsClosureWhenObjectAdded() {
        final TestClosure closure = context.mock(TestClosure.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(closure).call(bean);
        }});

        container.whenObjectAdded(HelperUtil.toClosure(closure));
        container.addObject("bean", bean);
    }

    @Test
    public void callsActionWhenObjectRemoved() {
        final Action<Bean> action = context.mock(Action.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(action).execute(bean);
        }});

        container.whenObjectRemoved(action);
        container.addObject("bean", bean);
        container.addObject("bean", new Bean());
    }

    @Test
    public void allObjectsCallsActionForEachExistingObject() {
        final Action<Bean> action = context.mock(Action.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(action).execute(bean);
        }});

        container.addObject("bean", bean);
        container.allObjects(action);
    }

    @Test
    public void allObjectsCallsClosureForEachExistingObject() {
        final TestClosure closure = context.mock(TestClosure.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(closure).call(bean);
        }});

        container.addObject("bean", bean);
        container.allObjects(HelperUtil.toClosure(closure));
    }

    @Test
    public void allObjectsCallsActionForEachNewObject() {
        final Action<Bean> action = context.mock(Action.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(action).execute(bean);
        }});

        container.allObjects(action);
        container.addObject("bean", bean);
    }

    @Test
    public void allObjectsCallsClosureForEachNewObject() {
        final TestClosure closure = context.mock(TestClosure.class);
        final Bean bean = new Bean();

        context.checking(new Expectations() {{
            one(closure).call(bean);
        }});

        container.allObjects(HelperUtil.toClosure(closure));
        container.addObject("bean", bean);
    }

    @Test
    public void eachObjectIsAvailableAsADynamicProperty() {
        Bean bean = new Bean();
        container.addObject("child", bean);
        assertTrue(container.getAsDynamicObject().hasProperty("child"));
        assertThat(container.getAsDynamicObject().getProperty("child"), sameInstance((Object) bean));
        assertThat(container.getAsDynamicObject().getProperties().get("child"), sameInstance((Object) bean));
        assertThat(call("{ it.child }", container), sameInstance((Object) bean));
        assertThat(call("{ it.child }", container.withType(Bean.class)), sameInstance((Object) bean));
        assertThat(call("{ it.child }", container.matching(Specs.satisfyAll())), sameInstance((Object) bean));
    }

    @Test
    public void eachObjectIsAvailableUsingAnIndex() {
        Bean bean = new Bean();
        container.addObject("child", bean);
        assertThat(call("{ it['child'] }", container), sameInstance((Object) bean));
    }

    @Test
    public void cannotGetUnknownProperty() {
        assertFalse(container.getAsDynamicObject().hasProperty("unknown"));

        try {
            container.getAsDynamicObject().getProperty("unknown");
            fail();
        } catch (MissingPropertyException e) {
            // expected
        }
    }

    @Test
    public void dynamicPropertyAccessInvokesRulesForUnknownDomainObject() {
        Bean bean = new Bean();
        addRuleFor(bean);

        assertTrue(container.getAsDynamicObject().hasProperty("bean"));
        assertThat(container.getAsDynamicObject().getProperty("bean"), sameInstance((Object) bean));
    }

    @Test
    public void eachObjectIsAvailableAsConfigureMethod() {
        Bean bean = new Bean();
        container.addObject("child", bean);

        Closure closure = toClosure("{ beanProperty = 'value' }");
        assertTrue(container.getAsDynamicObject().hasMethod("child", closure));
        container.getAsDynamicObject().invokeMethod("child", closure);
        assertThat(bean.getBeanProperty(), equalTo("value"));

        call("{ it.child { beanProperty = 'value 2' } }", container);
        assertThat(bean.getBeanProperty(), equalTo("value 2"));

        call("{ it.invokeMethod('child') { beanProperty = 'value 3' } }", container);
        assertThat(bean.getBeanProperty(), equalTo("value 3"));
    }

    @Test
    public void canUseDynamicPropertiesAndMethodsInsideConfigureClosures() {
        Bean bean = new Bean();
        container.addObject("child", bean);
        container.addObject("aProp", bean);
        container.addObject("a", bean);
        container.addObject("withType", bean);
        container.addObject("allObjects", bean);

        ConfigureUtil.configure(toClosure("{ child.beanProperty = 'value 1' }"), container);
        assertThat(bean.getBeanProperty(), equalTo("value 1"));

        ConfigureUtil.configure(toClosure("{ child { beanProperty = 'value 2' } }"), container);
        assertThat(bean.getBeanProperty(), equalTo("value 2"));

        ConfigureUtil.configure(toClosure("{ aProp.beanProperty = 'value 3' }"), container);
        assertThat(bean.getBeanProperty(), equalTo("value 3"));

        ConfigureUtil.configure(toClosure("{ a.beanProperty = 'value 4' }"), container);
        assertThat(bean.getBeanProperty(), equalTo("value 4"));

        // Try with an element with the same name as a method
        ConfigureUtil.configure(toClosure("{ withType.beanProperty = 'value 6' }"), container);
        assertThat(bean.getBeanProperty(), equalTo("value 6"));

        ConfigureUtil.configure(toClosure("{ withType { beanProperty = 'value 6' } }"), container);
        assertThat(bean.getBeanProperty(), equalTo("value 6"));
    }

    @Test
    public void cannotInvokeUnknownMethod() {
        container.addObject("child", new Bean());

        assertMethodUnknown("unknown");
        assertMethodUnknown("unknown", toClosure("{ }"));
        assertMethodUnknown("child");
        assertMethodUnknown("child", "not a closure");
        assertMethodUnknown("child", toClosure("{ }"), "something else");
    }

    private void assertMethodUnknown(String name, Object... arguments) {
        assertFalse(container.getAsDynamicObject().hasMethod(name, arguments));
        try {
            container.getAsDynamicObject().invokeMethod(name, arguments);
            fail();
        } catch (groovy.lang.MissingMethodException e) {
            // Expected
        }
    }

    @Test
    public void configureMethodInvokesRuleForUnknownDomainObject() {
        Bean bean = new Bean();
        addRuleFor(bean);

        assertTrue(container.getAsDynamicObject().hasMethod("bean", toClosure("{ }")));
    }

    @Test
    public void addRuleByClosure() {
        String testPropertyKey = "org.gradle.test.addRuleByClosure";
        String expectedTaskName = "someTaskName";
        Closure ruleClosure = HelperUtil.toClosure(String.format("{ taskName -> System.setProperty('%s', '%s') }",
                testPropertyKey, expectedTaskName));
        container.addRule("description", ruleClosure);
        container.getRules().get(0).apply(expectedTaskName);
        assertThat(System.getProperty(testPropertyKey), equalTo(expectedTaskName));
        System.getProperties().remove(testPropertyKey);
    }

    private void addRuleFor(final Bean bean) {
        container.addRule(new Rule() {
            public String getDescription() {
                throw new UnsupportedOperationException();
            }

            public void apply(String taskName) {
                container.addObject(taskName, bean);
            }
        });
    }

    private class Bean {
        private String beanProperty;

        public String getBeanProperty() {
            return beanProperty;
        }

        public void setBeanProperty(String beanProperty) {
            this.beanProperty = beanProperty;
        }
    }
}
