/*****************************************************************************
 * Copyright (C) PicoContainer Organization. All rights reserved.            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *                                                                           *
 *****************************************************************************/
package org.picocontainer.injectors;

import org.junit.Test;
import org.picocontainer.ComponentMonitor;
import org.picocontainer.DefaultPicoContainer;
import org.picocontainer.LifecycleStrategy;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.PicoCompositionException;
import org.picocontainer.annotations.Nullable;
import org.picocontainer.behaviors.Caching;
import org.picocontainer.behaviors.ThreadCaching;
import org.picocontainer.lifecycle.ReflectionLifecycleStrategy;
import org.picocontainer.monitors.LifecycleComponentMonitor;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class ProviderTestCase {
    
    @Test
    public void provideMethodCanParticipateInInjection() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        dpc.addAdapter(new Chocolatier(true));
        dpc.addComponent(NeedsChocolate.class);
        dpc.addComponent(CocaoBeans.class);
        dpc.addComponent(String.class, "Cadbury's"); // the only string in the set of components
        NeedsChocolate needsChocolate = dpc.getComponent(NeedsChocolate.class);
        assertNotNull(needsChocolate);
        assertNotNull(needsChocolate.choc);
        assertEquals(true, needsChocolate.choc.milky);
        assertNotNull(needsChocolate.choc.cocaoBeans);
        assertEquals("Cadbury's", needsChocolate.choc.name);
    }

    @Test
    public void provideMethodCanDisambiguateUsingParameterNames() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        dpc.addAdapter(new Chocolatier(true));
        dpc.addComponent(NeedsChocolate.class);
        dpc.addComponent(CocaoBeans.class);
        dpc.addComponent("color", "Red"); // not used by virtue of key
        dpc.addComponent("name", "Cadbury's");
        dpc.addComponent("band", "Abba"); // not used by virtue of key
        NeedsChocolate needsChocolate = dpc.getComponent(NeedsChocolate.class);
        assertNotNull(needsChocolate);
        assertNotNull(needsChocolate.choc);
        assertEquals(true, needsChocolate.choc.milky);
        assertNotNull(needsChocolate.choc.cocaoBeans);
        assertEquals("Cadbury's", needsChocolate.choc.name);
    }

    @Test
    public void providerBarfsIfProvideMethodsParamsCanNotBeSatisfied() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        dpc.addAdapter(new Chocolatier(true));
        dpc.addComponent(NeedsChocolate.class);
        try {
            dpc.getComponent(NeedsChocolate.class);
        } catch (PicoCompositionException e) {
            String message = e.getMessage();
            assertTrue(message.contains("Parameter 0 "));
            assertTrue(message.contains("cannot be null"));
        }
    }

    @Test
    public void providerDoesNotBarfIfProvideMethodsParamsCanNotBeSatisfiedButNullbleAnnotationUsed() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        dpc.addAdapter(new NullableChocolatier());
        dpc.addComponent(NeedsChocolate.class);
        NeedsChocolate nc = dpc.getComponent(NeedsChocolate.class);
        assertNotNull(nc);
        assertNotNull(nc.choc);
        assertTrue(nc.choc.cocaoBeans == null);
    }

    @Test
    public void testHasLifecycle() {
        DefaultPicoContainer dpc = new DefaultPicoContainer(new Caching());
        dpc.addAdapter(new NullableChocolatier());
        dpc.addComponent(NeedsChocolate.class);
        NeedsChocolate nc = dpc.getComponent(NeedsChocolate.class);
        dpc.start();
        dpc.stop();
        dpc.dispose();
    }

    public static class CocaoBeans {
    }

    public static class Chocolate {
        private boolean milky;
        private final CocaoBeans cocaoBeans;
        private final String name;

        public Chocolate(String name) {
            this(true, new CocaoBeans(), name);
        }

        public Chocolate(boolean milky, CocaoBeans cocaoBeans, String name) {
            this.milky = milky;
            this.cocaoBeans = cocaoBeans;
            this.name = name;
        }
    }

    public static class Chocolatier extends ProviderAdapter {
        private final boolean milky;
        public Chocolatier(boolean milky) {
            this.milky = milky;
        }
        public Chocolate provide(CocaoBeans cocaoBeans, String name) {
            return new Chocolate(milky, cocaoBeans, name);
        }
        @Override
        protected boolean useNames() {
            return true;
        }
    }

    public static class NullableChocolatier extends Chocolatier {
        public NullableChocolatier() {
            super(true);
        }

        public Chocolate provide(@Nullable CocaoBeans cocaoBeans, @Nullable String name) {
            return super.provide(cocaoBeans, name);
        }
    }

    public static class NeedsChocolate {
        private Chocolate choc;
        public NeedsChocolate(Chocolate choc) {
            this.choc = choc;
        }
    }

    @Test
    public void providerBarfsIfNoProvideMethod() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        try {
            dpc.addAdapter(new ProviderWithoutProvideMethod());
            fail("should have barfed");
        } catch (PicoCompositionException e) {
            assertEquals("There must be a method named 'provide' in the AbstractProvider implementation", e.getMessage());
        }
    }

    @Test
    public void providerBarfsIfBadProvideMethod() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        try {
            dpc.addAdapter(new ProviderWithBadProvideMethod());
            fail("should have barfed");
        } catch (PicoCompositionException e) {
            assertEquals("There must be a non void returning method named 'provide' in the AbstractProvider implementation", e.getMessage());
        }
    }

    @Test
    public void providerBarfsIfTooManyProvideMethod() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        try {
            dpc.addAdapter(new ProviderWithTooManyProvideMethods());
            fail("should have barfed");
        } catch (PicoCompositionException e) {
            assertEquals("There must be only one method named 'provide' in the AbstractProvider implementation", e.getMessage());
        }
    }

    public static class ProviderWithoutProvideMethod extends ProviderAdapter {
    }
    public static class ProviderWithBadProvideMethod extends ProviderAdapter {
        public void provide() {

        }
    }
    public static class ProviderWithTooManyProvideMethods extends ProviderAdapter {
        public String provide(String str) {
            return null;
        }
        public Integer provide() {
            return null;
        }
    }

    @Test
    public void provideMethodCanParticipateInInjectionWhenNotExtendingProviderAdapter() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();
        dpc.addAdapter(new ProviderAdapter(new Chocolatier2(true)));
        dpc.addComponent(NeedsChocolate.class);
        dpc.addComponent(CocaoBeans.class);
        dpc.addComponent(String.class, "Cadbury's"); // the only string in the set of components
        NeedsChocolate needsChocolate = dpc.getComponent(NeedsChocolate.class);
        assertNotNull(needsChocolate);
        assertNotNull(needsChocolate.choc);
        assertEquals(true, needsChocolate.choc.milky);
        assertNotNull(needsChocolate.choc.cocaoBeans);
        assertEquals("Cadbury's", needsChocolate.choc.name);
    }

    public static class Chocolatier2 implements Provider {
        private final boolean milky;
        public Chocolatier2(boolean milky) {
            this.milky = milky;
        }
        public Chocolate provide(CocaoBeans cocaoBeans, String name) {
            return new Chocolate(milky, cocaoBeans, name);
        }
    }

    @Test
    public void providedTypeCanBeDyanamicallyDeterminedFromInstanceRatherThanType() {
        DefaultPicoContainer dpc = new DefaultPicoContainer();

        // a simlation of what a web framework would essentially do in a thread-local way
        dpc.addComponent(new StubHttpRequest("chocolate", "Lindt"));

        // this is the style being recomended for automatic request-params -> beans
        dpc.addAdapter(new ExampleRequestReader(Chocolate.class, "chocolate"));

        dpc.addComponent(NeedsChocolate.class);
        NeedsChocolate needsChocolate = dpc.getComponent(NeedsChocolate.class);
        assertNotNull(needsChocolate);
        assertNotNull(needsChocolate.choc);
        assertEquals(true, needsChocolate.choc.milky);
        assertNotNull(needsChocolate.choc.cocaoBeans);
        assertEquals("Lindt", needsChocolate.choc.name);
    }


    public static class StubHttpRequest {
        private final String key;
        private final String value;

        public StubHttpRequest(String key, String value) {
            this.key = key;
            this.value = value;
        }

        public String getParameter(String name) {
            return name.equals(key) ? value : null;
        }
    }

    public static class ExampleRequestReader extends ProviderAdapter {
        private final Class clazz;
        private final String paramName;

        public ExampleRequestReader(Class clazz, String paramName) {
            this.clazz = clazz;
            this.paramName = paramName;
        }

        public Class getComponentImplementation() {
            return clazz;
        }

        public Object provide(StubHttpRequest req) {
            try {
            	return clazz.getConstructor(String.class).newInstance(req.getParameter(paramName));
            } catch (Exception e) {
                throw new RuntimeException(e);  
            }
        }
    }

    @Test
    public void providersCanHaveLifecyclesToo() {
        ComponentMonitor componentMonitor = new LifecycleComponentMonitor();
        LifecycleStrategy lifecycleStrategy = new
                ReflectionLifecycleStrategy(componentMonitor);

        MutablePicoContainer pico = new DefaultPicoContainer(new
                ThreadCaching(), lifecycleStrategy, null);

        StringBuilder sb = new StringBuilder();
        pico.addComponent(Configuration.class);
        pico.addAdapter(new ProviderAdapter(lifecycleStrategy, new ComponentProvider(sb)));
        Object foo = pico.getComponent(Component.class);
        pico.start();
        pico.stop();
        assertEquals("@<>", sb.toString());

    }

    public class ComponentProvider implements Provider {
        private StringBuilder sb;

        public ComponentProvider(StringBuilder sb) {
            this.sb = sb;
        }

        public Component provide(Configuration config) {
            return new ComponentImpl(sb, config.getHost(), config.getPort());
        }
    }

    public static class Configuration {

        public String getHost() {
            return "hello";
        }

        public int getPort() {
            return 99;
        }

        public void start() {
        }

        public void stop() {
        }

    }

    public static interface Component {

        public void start();

        public void stop();

    }

    public static class ComponentImpl implements Component {

        private StringBuilder sb;

        public ComponentImpl(StringBuilder sb, String host, int port) {
            this.sb = sb.append("@");
        }

        public void start() {
            sb.append("<");
        }
        public void stop() {
            sb.append(">");
        }

    }


    /**
     * Reference Johann Burkard's
     * http://jira.codehaus.org/browse/PICO-375
     */
    @Test
    public void providerTest() {
        DefaultPicoContainer container = new DefaultPicoContainer();
        ProviderAdapter adapter = new ProviderAdapter(new BlorbProvider());
        container.addAdapter(adapter);
        assertNotNull(container.getComponent(Blorb.class));
    }
    // Differs from Johann's by the "implements Provider" only.
    public static class BlorbProvider implements Provider {
        public Blorb provide() {
            return new Blorb();
        }
    }
    public static class Blorb {}

//    @Test
//    public void providerAdapterMustBeHandedAnImplementationOfProvider() {
//        DefaultPicoContainer container = new DefaultPicoContainer();
//        try {
//            ProviderAdapter adapter = new ProviderAdapter(new Blorb2Provider());
//        } catch (Exception e) {
//
//        }
//    }
//    public static class Blorb2Provider {
//        public Blorb2 provide() {
//            return new Blorb2();
//        }
//    }
//    public static class Blorb2 {}



}
