/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *    https://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.apache.cayenne.di.spi;

import org.apache.cayenne.di.Key;
import org.apache.cayenne.di.Module;
import org.apache.cayenne.di.mock.*;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import static org.junit.Assert.*;

public class DefaultInjectorInjectionTest {

    @Test
    public void testFieldInjection() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(MockInterface2.class).to(MockImplementation2.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface2 service = injector.getInstance(MockInterface2.class);
        assertNotNull(service);
        assertEquals("altered_MyName", service.getAlteredName());
    }

    @Test
    public void testFieldInjection_Named() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(Key.get(MockInterface1.class, "one")).to(MockImplementation1Alt.class);
            binder.bind(Key.get(MockInterface1.class, "two")).to(MockImplementation1Alt2.class);
            binder.bind(MockInterface2.class).to(MockImplementation2_Named.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface2 service = injector.getInstance(MockInterface2.class);
        assertNotNull(service);
        assertEquals("altered_alt", service.getAlteredName());
    }

    @Test
    public void testFieldInjectionSuperclass() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(MockInterface2.class).to(MockImplementation2Sub1.class);
            binder.bind(MockInterface3.class).to(MockImplementation3.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface2 service = injector.getInstance(MockInterface2.class);
        assertNotNull(service);
        assertEquals("altered_MyName:XName", service.getAlteredName());
    }

    @Test
    public void testConstructorInjection() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(MockInterface4.class).to(MockImplementation4.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface4 service = injector.getInstance(MockInterface4.class);
        assertNotNull(service);
        assertEquals("constructor_MyName", service.getName());
    }

    @Test
    public void testConstructorInjection_Named() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(Key.get(MockInterface1.class, "one")).to(MockImplementation1Alt.class);
            binder.bind(Key.get(MockInterface1.class, "two")).to(MockImplementation1Alt2.class);
            binder.bind(MockInterface4.class).to(MockImplementation4Alt.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface4 service = injector.getInstance(MockInterface4.class);
        assertNotNull(service);
        assertEquals("constructor_alt2", service.getName());
    }

    @Test
    public void testConstructorInjection_Named_Mixed() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(Key.get(MockInterface1.class, "one")).to(MockImplementation1Alt.class);
            binder.bind(Key.get(MockInterface1.class, "two")).to(MockImplementation1Alt2.class);
            binder.bind(MockInterface3.class).to(MockImplementation3.class);
            binder.bind(MockInterface4.class).to(MockImplementation4Alt2.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface4 service = injector.getInstance(MockInterface4.class);
        assertNotNull(service);
        assertEquals("constructor_alt2_XName", service.getName());
    }

    @Test
    public void testProviderInjection_Constructor() {

        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1.class);
            binder.bind(MockInterface2.class).to(MockImplementation2_ConstructorProvider.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface2 service = injector.getInstance(MockInterface2.class);
        assertEquals("altered_MyName", service.getAlteredName());
    }

    @Test
    public void testMapInjection_Empty() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_MapConfiguration.class);

            // empty map must be still bound
            binder.bindMap(Object.class, "xyz");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals("", service.getName());
    }

    @Test
    public void testMapInjection() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_MapConfiguration.class);
            binder.bindMap(Object.class, "xyz")
                    .put("x", "xvalue").put("y", "yvalue").put("x", "xvalue1");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";x=xvalue1;y=yvalue", service.getName());
    }

    @Test
    public void mapWithWildcardInjection() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_MapWithWildcards.class);
            binder.bindMap(Class.class).put("x", String.class).put("y", Integer.class).put("z", Object.class);
        };
        DefaultInjector injector = new DefaultInjector(module);

        // This is example of how to deal with wildcards:
        // to handle it nicer we need to use some hacks with anonymous classes:
        // Key.get(new TypeLiteral<String, Class<?>>(){});
        Map mapUntyped = injector.getInstance(Key.getMapOf(String.class, Class.class));
        @SuppressWarnings("unchecked")
        Map<String, Class<?>> map = (Map<String, Class<?>>) mapUntyped;

        assertNotNull(map);
        assertEquals(3, map.size());
        assertEquals(String.class, map.get("x"));

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals("map:3", service.getName());
    }

    @Test
    public void testMapInjection_Resumed() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_MapConfiguration.class);
            // bind 1
            binder.bindMap(Object.class, "xyz").put("x", "xvalue").put("y", "yvalue");
            // second binding attempt to the same map...
            binder.bindMap(Object.class, "xyz").put("z", "zvalue").put("x", "xvalue1");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";x=xvalue1;y=yvalue;z=zvalue", service.getName());
    }

    @Test
    public void testMapInjection_OverrideExplicitlyBoundType() {
        Module m1 = binder -> {
            binder.bind(MockInterface5.class).to(MockImplementation5.class);
            binder.bind(MockInterface1.class).to(MockImplementation1_MapConfiguration.class);

            binder.bindMap(Object.class, "xyz").put("a", MockInterface5.class);
        };

        Module m2 = binder -> binder.bind(MockInterface5.class).toInstance(new MockInterface5() {

            @Override
            public String toString() {
                return "abc";
            }
        });

        MockInterface1 service = new DefaultInjector(m1, m2).getInstance(MockInterface1.class);
        assertEquals("Map element was not overridden in submodule", ";a=abc", service.getName());
    }

    @Test
    public void testMapInjection_OverrideImplicitlyBoundType() {
        Module m1 = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_MapConfiguration.class);
            binder.bindMap(Object.class, "xyz").put("a", MockImplementation5.class);
        };

        Module m2 = binder -> binder.bind(MockImplementation5.class).toInstance(new MockImplementation5() {

            @Override
            public String toString() {
                return "abc";
            }
        });

        MockInterface1 service = new DefaultInjector(m1, m2).getInstance(MockInterface1.class);
        assertEquals("Map element was not overridden in submodule", ";a=abc", service.getName());
    }

    @Test
    public void testListInjection_addValue() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bindList(Object.class, "xyz").add("xvalue").add("yvalue");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";xvalue;yvalue", service.getName());
    }

    @Test
    public void testListInjection_addOrderedValues() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bind(MockInterface5.class).to(MockImplementation5.class);

            binder.bindList(Object.class, "xyz")
                    .add("1value")
                    .add("2value")
                    .addAfter("5value", MockInterface5.class)
                    .insertBefore("3value", MockInterface5.class)
                    .add(MockInterface5.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";1value;2value;3value;xyz;5value", service.getName());
    }

    @Test
    public void testListInjection_Instance_addOverrideValueOrdering() {

        Integer i = Integer.valueOf(5);

        Module m1 = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bind(MockInterface5.class).to(MockImplementation5.class);
            binder.bind(Integer.class).toInstance(i);

            binder.bindList(Object.class, "xyz")
                    .addAfter(i, MockInterface5.class)
                    .add(MockInterface5.class);
        };

        Module m2 = binder ->
                binder.bindList(Object.class, "xyz").insertBefore(i, MockInterface5.class);

        // instance is inserted multiple types, as each instance is treated as a standalone DI Key
        DefaultInjector i1 = new DefaultInjector(m1, m2);
        assertEquals(";5;xyz;5", i1.getInstance(MockInterface1.class).getName());

        DefaultInjector i2 = new DefaultInjector(m2, m1);
        assertEquals(";5;xyz;5", i2.getInstance(MockInterface1.class).getName());
    }

    @Test
    public void testListInjection_Type_addOverrideValueOrdering() {

        Module m1 = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bind(MockInterface5.class).to(MockImplementation5.class);
            binder.bind(String.class).toInstance("a");

            binder.bindList(Object.class, "xyz")
                    .addAfter(String.class, MockInterface5.class)
                    .add(MockInterface5.class);
        };

        Module m2 = binder -> binder.bindList(Object.class, "xyz")
                .insertBefore(String.class, MockInterface5.class);

        DefaultInjector i1 = new DefaultInjector(m1, m2);
        assertEquals(";a;xyz", i1.getInstance(MockInterface1.class).getName());

        DefaultInjector i2 = new DefaultInjector(m2, m1);
        assertEquals(";xyz;a", i2.getInstance(MockInterface1.class).getName());
    }

    @Test
    public void testListInjection_addAllValues() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);

            Collection<Object> firstList = new ArrayList<>();
            firstList.add("1value");
            firstList.add("2value");
            firstList.add("3value");

            Collection<Object> secondList = new ArrayList<>();
            secondList.add("6value");
            secondList.add("7value");
            secondList.add("8value");

            binder.bind(MockInterface5.class).to(MockImplementation5.class);

            binder.bindList(Object.class, "xyz")
                    .insertAllBefore(firstList, MockInterface5.class)
                    .addAllAfter(secondList, MockInterface5.class)
                    .add("5value")
                    .add(MockInterface5.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";1value;2value;3value;xyz;6value;7value;8value;5value", service.getName());
    }

    @Test
    public void testListInjection_addType() {
        Module module = binder -> {
            binder.bind(MockInterface5.class).to(MockImplementation5.class);
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);

            binder.bindList(Object.class, "xyz").add(MockInterface5.class).add("yvalue");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";xyz;yvalue", service.getName());
    }

    @Test
    public void testListInjection_addOrderedTypes() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bind(MockInterface5.class).to(MockImplementation5.class);

            binder.bindList(Object.class, "xyz")
                    .add("1value")
                    .insertBefore("5value", MockInterface5.class)
                    .add("2value")
                    .addAfter("6value", MockInterface5.class)
                    .add("3value")
                    .add(MockInterface5.class);
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";1value;2value;5value;xyz;6value;3value", service.getName());
    }

    @Test
    public void testListInjection_addTypeWithBinding() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bindList(Object.class, "xyz").add(MockImplementation5.class).add("yvalue");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";xyz;yvalue", service.getName());
    }

    @Test
    public void testListInjection_empty() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bindList(Object.class, "xyz");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals("", service.getName());
    }

    @Test
    public void testListInjection_resumed() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);

            binder.bindList(Object.class, "xyz").add("xvalue").add("yvalue");
            binder.bindList(Object.class, "xyz").add("avalue");
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";xvalue;yvalue;avalue", service.getName());
    }

    @Test
    public void testTypedListInjection() {
        Module module = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfigurationMock5.class);
            binder.bind(MockInterface2.class).to(MockImplementation2_ListConfiguration.class);

            // Bind list for MockImplementation2_ListConfiguration
            binder.bindList(Object.class, "xyz")
                    .add("xvalue")
                    .add("yvalue")
                    .add(MockImplementation5.class);

            // Bind list for MockImplementation1_ListConfigurationMock5
            binder.bindList(MockInterface5.class)
                    .add(MockImplementation5.class)
                    .add(new MockInterface5() {
                        @Override
                        public String toString() {
                            return "abc";
                        }
                    });

            binder.bindList(Object.class)
                    .add("avalue")
                    .add("bvalue")
                    .add(MockImplementation5.class);

            // Add to list for MockImplementation1_ListConfigurationMock5
            binder.bindList(MockInterface5.class)
                    .add(new MockInterface5() {
                        @Override
                        public String toString() {
                            return "cde";
                        }
                    });

            // Create named list for MockInterface5
            binder.bindList(MockInterface5.class, "another_binding")
                    .add(new MockInterface5() {
                        @Override
                        public String toString() {
                            return "fgh";
                        }
                    });
        };

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals(";xyz;abc;cde", service.getName());

        MockInterface2 service2 = injector.getInstance(MockInterface2.class);
        assertNotNull(service2);
        assertTrue(service2 instanceof MockImplementation2_ListConfiguration);
        assertEquals(";xvalue;yvalue;xyz", service2.getName());
    }

    @Test
    public void testListInjection_OverrideExplicitlyBoundType() {
        Module m1 = binder -> {
            binder.bind(MockInterface5.class).to(MockImplementation5.class);
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);

            binder.bindList(Object.class, "xyz").add(MockInterface5.class);
        };

        Module m2 = binder -> binder.bind(MockInterface5.class).toInstance(new MockInterface5() {

            @Override
            public String toString() {
                return "abc";
            }
        });

        MockInterface1 service = new DefaultInjector(m1, m2).getInstance(MockInterface1.class);
        assertEquals("List element was not overridden in submodule", ";abc", service.getName());
    }

    @Test
    public void testListInjection_OverrideImplicitlyBoundType() {
        Module m1 = binder -> {
            binder.bind(MockInterface1.class).to(MockImplementation1_ListConfiguration.class);
            binder.bindList(Object.class, "xyz").add(MockImplementation5.class);
        };

        Module m2 = binder -> binder.bind(MockImplementation5.class).toInstance(new MockImplementation5() {

            @Override
            public String toString() {
                return "abc";
            }
        });

        MockInterface1 service = new DefaultInjector(m1, m2).getInstance(MockInterface1.class);
        assertEquals("List element was not overridden in submodule", ";abc", service.getName());
    }


    @Test
    public void testInjectorInjection() {
        Module module = binder -> binder.bind(MockInterface1.class).to(
                MockImplementation1_WithInjector.class);

        DefaultInjector injector = new DefaultInjector(module);

        MockInterface1 service = injector.getInstance(MockInterface1.class);
        assertNotNull(service);
        assertEquals("injector_not_null", service.getName());
    }

}
