/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.reflection;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.ranides.assira.collection.SetUtils;
import net.ranides.assira.generic.Serializer;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.reflection.MethodFactory.ConstructorPointer;
import net.ranides.assira.reflection.MethodFactory.FunctionPointer;
import net.ranides.assira.reflection.MethodFactory.MethodPointer;
import net.ranides.assira.reflection.inspect.MethodInspector;
import net.ranides.assira.reflection.mock.ForMethodFactory;
import net.ranides.assira.reflection.mock.ForMethodFactory.Function1;
import net.ranides.assira.reflection.mock.ForMethodFactory.Function2;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class MethodFactoryTest {
    
    public MethodFactoryTest() {
    }

    @Test
    public void testConstructArgs() {
        
        Set<String> expected = SetUtils.asHashSet(
            "[Map<String, Q> map] @ 1 -> ForMethodFactory<?>",
            "[List<String> list] @ 1 -> ForMethodFactory<?>",
            "[int a, int b] @ 2 -> ForMethodFactory<?>",
            "[] @ 0 -> ForMethodFactory<?>"
        );
        Set<String> found = new HashSet<>();
        
        for( ConstructorPointer<ForMethodFactory> ctor : MethodInspector.inclass(ForMethodFactory.class).constructors().pointers()) {
            found.add( ctor.params().toString() + " @ " + ctor.paramsize() + " -> " + ctor.type() );
        }

        assertEquals(expected, found);
    }
    
    @Test
    public void testConstructErrors() {
        ConstructorPointer<ForMethodFactory> ctor = 
            MethodInspector.inclass(ForMethodFactory.class).constructors().paramsMatch(0,0).first();
       
        try {
            ctor.bind(null);
            fail("UnsupportedOperationException expected");
        } catch(UnsupportedOperationException ex) {
            assertTrue(true);
        }
        try {
            ctor.call(-5,-5);
            fail("InvokeException expected");
        } catch(InvokeException ex) {
            assertTrue(ex.getCause() instanceof ArithmeticException);
        }
        try {
            ctor.call(0);
            fail("InspectException expected");
        } catch(InspectException ex) {
            assertTrue(ex.getCause() instanceof IllegalArgumentException);
        }
    }
    
    @Test
    public void testConstructCan() {
        ConstructorPointer<ForMethodFactory> ctor = 
            MethodInspector.inclass(ForMethodFactory.class).constructors().paramsMatch(0,0).first();
        
        assertTrue( ctor.canCall(1,1));
        assertFalse( ctor.canCall(1));
        assertFalse( ctor.canCall(1,1,1));
        assertFalse( ctor.canCall("x"));
        
        assertTrue( ctor.canInvoke(null, 1,1));
        assertFalse( ctor.canInvoke(new ForMethodFactory(), 1,1));
        assertFalse( ctor.canInvoke(null, 1));
        assertFalse( ctor.canInvoke(null, 1,1,1));
        assertFalse( ctor.canInvoke(null, "x"));
    }
    
    @Test
    public void testConstructInvoke() {
        ConstructorPointer<ForMethodFactory> c1 = MethodInspector
            .inclass(ForMethodFactory.class).constructors().params().first();
        
        assertEquals(77, c1.call().value);
        assertEquals(77, c1.invoke(null).value);
        
        ConstructorPointer<ForMethodFactory> c2 = MethodInspector
            .inclass(ForMethodFactory.class).constructors().paramsMatch(0,0).first();
        
        assertEquals(603, c2.call(6, 3).value);
        assertEquals(603, c2.invoke(null, 6, 3).value);
        
        ConstructorPointer<ForMethodFactory> c3 = MethodInspector
            .inclass(ForMethodFactory.class).constructors().params(Map.class).first();
        ConstructorPointer<?> c4 = MethodInspector
            .inclass(ForMethodFactory.class).constructors().params(new TypeToken<Map>(){}).first();
        
        assertEquals(c3.reflective(), c4.reflective());
        assertEquals(704, c3.call(ForMethodFactory.asmap(7, 4)).value);
        assertEquals(704, ((ForMethodFactory)c4.call(ForMethodFactory.asmap(7, 4))).value);
        
        // unfortunatelly, generic params matching is unsupported now :(
        // it doesn't matter really, because you can't overload generic params anyway
        
//        ConstructorPointer<?> c5 = MethodInspector
//            .inclass(ForMethodFactory.class).constructors().params(new TypeToken<Map<Float,Float>>(){}).first();
//        assertNull(c5);
    }
    
    @Test
    public void testMethodsArgs() {
        MethodPointer<?> m1 = MethodInspector
            .inclass(ForMethodFactory.class).published().name("aslist").first();
        assertEquals("[int a, int b]", m1.params().toString());
        assertEquals("List<String>", m1.type().toString());
        assertEquals(2, m1.paramsize());
        
        MethodPointer<?> m2 = MethodInspector
            .inclass(ForMethodFactory.class).published().name("values").first();
        assertEquals("[int a, int b, int c]", m2.params().toString());
        assertEquals("List<Number>", m2.type().toString());
        assertEquals(3, m2.paramsize());
    }
    
    @Test
    public void testMethodsInvoke() {
        ForMethodFactory that = new ForMethodFactory();
                
        MethodPointer<?> m1 = MethodInspector
            .inclass(ForMethodFactory.class).published().name("aslist").first();
        assertEquals("[4, 7]", m1.call(4,7).toString());
        assertEquals("[4, 7]", m1.invoke(null, 4,7).toString());
        
        MethodPointer<?> m2 = MethodInspector
            .inclass(ForMethodFactory.class).published().name("values").first();
        assertEquals("[4, 7, 9]", m2.call(that, 4, 7, 9).toString());
        assertEquals("[4, 7, 9]", m2.invoke(that, 4, 7, 9).toString());
    }
    
    @Test
    public void testMethodsErrors() {
        ForMethodFactory that = new ForMethodFactory();
        MethodPointer<?> mstat = MethodInspector
            .inclass(ForMethodFactory.class).published().name("aslist").first();
        MethodPointer<?> msome = MethodInspector
            .inclass(ForMethodFactory.class).published().name("values").first();
        
        try {
            mstat.call(-5,-5);
            fail("InspectException:IllegalArgumentException expected");
        } catch(InvokeException ex) {
            assertTrue(ex.getCause() instanceof ArithmeticException);
        }
        assertEquals("[1, 2]", mstat.invoke(that, 1, 2).toString());
        assertEquals("[1, 2]", mstat.invoke(null, 1, 2).toString());
        assertEquals("[1, 2]", mstat.invoke(99, 1, 2).toString());
        
        try {
            mstat.call(1);
            fail("InspectException:IllegalArgumentException expected");
        } catch(InspectException ex) {
            assertTrue(ex.getCause() instanceof IllegalArgumentException);
        }
        try {
            mstat.invoke(1, 1);
            fail("InspectException:IllegalArgumentException expected");
        } catch(InspectException ex) {
            assertTrue(ex.getCause() instanceof IllegalArgumentException);
        }
        
        try {
            msome.call(1);
            fail("InspectException:IllegalArgumentException expected");
        } catch(InspectException ex) {
            assertTrue(ex.getCause() instanceof IllegalArgumentException);
        }
        try {
            msome.invoke(1, 1);
            fail("InspectException:IllegalArgumentException expected");
        } catch(InspectException ex) {
            assertTrue(ex.getCause() instanceof IllegalArgumentException);
        }
        try {
            msome.invoke(new Object(), 1, 1);
            fail("InspectException:IllegalArgumentException expected");
        } catch(InspectException ex) {
            assertTrue(ex.getCause() instanceof IllegalArgumentException);
        }
        try {
            msome.invoke(null, 1, 1);
            fail("NullPointerException expected");
        } catch(NullPointerException ex) {
            assertTrue(true);
        }
        try {
            msome.call(null, 1, 1);
            fail("NullPointerException expected");
        } catch(NullPointerException ex) {
            assertTrue(true);
        }
        
        try {
            mstat.bind(that);
            fail("UnsupportedOperationException expected");
        } catch(UnsupportedOperationException ex) {
            assertTrue(true);
        }

    }
    
    @Test
    public void testMethodsCan() {
        ForMethodFactory that = new ForMethodFactory();
        MethodPointer<?> mstat = MethodInspector
            .inclass(ForMethodFactory.class).published().name("aslist").first();
        MethodPointer<?> msome = MethodInspector
            .inclass(ForMethodFactory.class).published().name("values").first();
        
        assertTrue( mstat.canCall(1,2));
        assertFalse( mstat.canCall(1,2,3));
        assertFalse( mstat.canCall(1));
        assertFalse( mstat.canCall("1", "2"));
        
        // @todo (prev # 8) issue: static MethodPointer accepts any value of "this"
        // in #invoke & #canInvoke
        
        assertTrue( mstat.canInvoke(null, 1,2));
        assertTrue( mstat.canInvoke(that, 1,2)); // feature
        assertFalse( mstat.canInvoke(null, 1,2,3));
        assertFalse( mstat.canInvoke(null, 1));
        assertFalse( mstat.canInvoke(null, "1", "2"));
        
        assertTrue( msome.canCall(that, 1,2,3));
        assertFalse( msome.canCall(null, 1,2,3));
        assertFalse( msome.canCall(new Object(), 1,2,3));
        assertFalse( msome.canCall(that, 1,2,3,4));
        assertFalse( msome.canCall(that, 1));
        assertFalse( msome.canCall(that, "1", "2"));
        assertFalse( msome.canCall(that, "1", "2", "3"));
        
        assertTrue( msome.canInvoke(that, 1,2,3));
        assertFalse( msome.canInvoke(null, 1,2,3));
        assertFalse( msome.canInvoke(new Object(), 1,2,3));
        assertFalse( msome.canInvoke(that, 1,2,3,4));
        assertFalse( msome.canInvoke(that, 1));
        assertFalse( msome.canInvoke(that, "1", "2"));
        assertFalse( msome.canInvoke(that, "1", "2", "3"));
    }
    
    @Test
    public void testMethodsBind() {
        ForMethodFactory o1 = new ForMethodFactory(1,2);
        ForMethodFactory o2 = new ForMethodFactory(3,4);
        MethodPointer<?> msome = MethodInspector
            .inclass(ForMethodFactory.class).published().name("current").first();
        
        GenericMethod<?> bind1 = msome.bind(o1);
        GenericMethod<?> bind2 = bind1.bind(o2);
        
        assertNotSame(bind1, msome);
        
        assertEquals(msome.name(), bind1.name());
        assertEquals(msome.params(), bind1.params());
        assertEquals(msome.paramsize(), bind1.paramsize());
        assertEquals(msome.type(), bind1.type());
        assertEquals(msome.toString(), bind1.toString());
        
        assertFalse(bind1.canInvoke(o1));
        assertTrue(bind1.canCall());
        
        assertEquals(102, bind1.call());
        assertEquals(304, bind2.call());
        
        try {
            bind1.invoke(o1);
            fail("UnsupportedOperationException expected");
        } catch(UnsupportedOperationException ex)  {
            assertTrue(true);
        }
    }
    
    @Test
    public void testFunction() {
        
        FunctionPointer<Integer> pointer1 = MethodFactory.wrap(new Function1());
        FunctionPointer<List<Integer>> pointer2 = MethodFactory.wrap(new Function2());
        
        assertEquals("net.ranides.assira.reflection.mock.ForMethodFactory$Function1", pointer1.name());
        assertEquals("Integer", pointer1.type().toString());
        assertEquals("[String source]", pointer1.params().toString());
        assertEquals(1, pointer1.paramsize());
        

        assertEquals("net.ranides.assira.reflection.mock.ForMethodFactory$Function2", pointer2.name());
        assertEquals("List<Integer>", pointer2.type().toString());
        assertEquals("[List<String> input]", pointer2.params().toString());
        assertEquals(1, pointer2.paramsize());
        
        assertSame(pointer1, pointer1.bind(null));
        
        assertEquals(Arrays.asList(1,2,3), pointer2.invoke(null, Arrays.asList("1","2","3")));
        assertEquals(Arrays.asList(1,2,3), pointer2.call(Arrays.asList("1","2","3")));
        
        assertTrue( pointer1.canCall("1"));
        assertFalse( pointer1.canCall(7));
        assertTrue( pointer1.canInvoke(null, "1"));
        assertFalse( pointer1.canInvoke(new Object(), "1"));
    }
	
	@Test
	public void testSerialization() throws IOException {
		MethodPointer<?> m1 = MethodInspector
            .inclass(ForMethodFactory.class).published().name("aslist").first();
		MethodPointer<?> m2 = Serializer.copy(m1);
		
		assertEquals("[int a, int b]", m2.params().toString());
        assertEquals("List<String>", m2.type().toString());
        assertEquals(2, m2.paramsize());
	}
}
