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

import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import net.ranides.assira.annotations.Meta;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.reflection.TypeFactory;
import net.ranides.assira.reflection.InspectException;
import net.ranides.assira.reflection.MethodFactory.ConstructorPointer;
import net.ranides.assira.reflection.GenericMethod;
import net.ranides.assira.reflection.mock.ForClassInspector.*;
import net.ranides.assira.text.LexicalCast;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class MethodInspectorTest {
    
    private static final String NS = "net.ranides.assira.reflection.mock.ForClassInspector";

    @Test
    public void testInClass() {
        MethodInspector m1 = MethodInspector.inclass(C1.class);
        MethodInspector m2 = MethodInspector.inclass(TypeFactory.construct(C1.class));
        MethodInspector m3 = MethodInspector.inclass(new TypeToken<C1>() {});
        MethodInspector m4 = MethodInspector.inclass(NS + "$C1");
        MethodInspector m5 = MethodInspector.inobject(new C1());
        
        assertEquals(m1, m2);
        assertEquals(m1, m3);
        assertEquals(m1, m4);
        assertEquals(m1, m5);
    }
    
    @Test
    public void testEquals() {
        MethodInspector m1 = MethodInspector.inclass(C1.class);
        MethodInspector m2 = MethodInspector.inclass(C1.class);
        MethodInspector m3 = MethodInspector.inclass(C1.class).follow();
        MethodInspector m4 = MethodInspector.inclass(C2.class);
        
        assertEquals(m1, m1);
        assertEquals(m1, m2);
        assertFalse(m1.equals(m3));
        assertFalse(m1.equals(m4));
        assertFalse(m1.equals(null));
        assertFalse(m1.equals(new Object()));
    }
    
    @Test
    public void testConstructorDefault() {
        Collection<ConstructorPointer<NCA>> list1 = MethodInspector.inclass(NCA.class).constructors().pointers();
        List<String> exp1 = Arrays.asList(
            NS + "$NCA()"
        );
        assertEquals(exp1, LexicalCast.asList(list1));
        
        Collection<ConstructorPointer<NCB>> list2 = MethodInspector.inclass(NCB.class).constructors().pointers();
        List<String> exp2 = Arrays.asList(
            NS + "$NCB()"
        );
        assertEquals(exp2, LexicalCast.asList(list2));
    }
    
    @Test
    public void testConstructor() {
        List<String> exp1 = Arrays.asList(
            NS + "$A(float, float)", 
            NS + "$A(int)", 
            NS + "$A()"
        );
        assertFilter(exp1, MethodInspector.inclass(A.class).constructors());
        
        List<String> exp2 = Arrays.asList(
            NS + "$B(java.lang.String)", 
            NS + "$B(double)", 
            NS + "$B()"
        );
        assertFilter(exp2, MethodInspector.inclass(B.class).constructors());
        
        List<String> exp3 = Arrays.asList(NS + "$C1()");
        assertFilter(exp3, MethodInspector.inclass(C1.class).constructors());

        List<String> exp4 = Arrays.asList(
            NS + "$C2(java.lang.String, int, int)", 
            NS + "$C2(java.lang.String)", 
            NS + "$C2(double)"
        );
        assertFilter(exp4, MethodInspector.inclass(C2.class).constructors());
        
        List<String> exp5 = Arrays.asList(NS + "$C2(double)");
        assertFilter(exp5, MethodInspector.inclass(C2.class).constructors()
            .require(Modifier.PUBLIC)
        );
        
        List<String> exp6 = Arrays.asList(NS + "$C2(java.lang.String)");
        assertFilter(exp6, MethodInspector.inclass(C2.class).constructors()
            .require(Modifier.PROTECTED)
        );
        
        List<String> exp7 = Arrays.asList(
            NS + "$C2(java.lang.String)", 
            NS + "$C2(double)"
        );
        assertFilter(exp7, MethodInspector.inclass(C2.class).constructors()
            .discard(Modifier.PRIVATE)
        );
        
        List<String> exp8 = Collections.emptyList();
        assertFilter(exp8, MethodInspector.inclass(C2.class).constructors()
            .discard(Modifier.PRIVATE)
            .require(Modifier.PRIVATE)
        );
        
        // smoke test - annotations are tested in other place
        List<String> exp9 = Arrays.asList(
            NS + "$Annotated(int, int, int)"
        );
        assertFilter(exp9, MethodInspector.inclass(Annotated.class).constructors()
            .annotated(Meta.Generated.class)
        );
        
        // smoke test - parameters are tested in other place
        List<String> exp10 = Arrays.asList(
            NS + "$Annotated(int, int)"
        );
        assertFilter(exp10, MethodInspector.inclass(Annotated.class).constructors()
            .params(int.class, int.class)
        );
    }
    
    private void printFilter(List<String> expected, MethodInspector.Filter filter) {
        Set<String> sorted = new TreeSet<>( LexicalCast.asList(filter.pointers()) );
        System.out.println( LexicalCast.asText(sorted));
        System.out.println();
    }
    
    private void assertFilter(List<String> expected, MethodInspector.Filter filter) {
        Collection<GenericMethod<?>> list = filter.pointers();
        Iterator<GenericMethod<?>> i = list.iterator();
        int delta = 0;
        while(i.hasNext()) {
            if("$jacocoInit".equals(i.next().name())) {
                i.remove();
                delta++;
            }
        }
        
        
        Set<String> exset = new TreeSet<>(expected);
        Set<String> crset = new TreeSet<>( LexicalCast.asList(list) );
        
        assertEquals(exset, crset);
        assertEquals(expected.size(), filter.count()-delta);
        if( expected.isEmpty() ) {
            assertNull(filter.first());
        } else {
            assertNotNull(filter.first());
        }
    }
    
    @Test
    public void testPublicMethods() {
        List<String> exp1 = Arrays.asList(
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubC2(int)", 
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubA(int)", 
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubB(int)", 
            "java.lang.Object.wait(long, int)", 
            "java.lang.Object.wait(long)", 
            "java.lang.Object.wait()", 
            "java.lang.Object.equals(java.lang.Object)", 
            "java.lang.Object.toString()", 
            "java.lang.Object.hashCode()", 
            "java.lang.Object.getClass()", 
            "java.lang.Object.notify()", 
            "java.lang.Object.notifyAll()"
        );
        assertFilter(exp1, MethodInspector.inclass(C2.class).published());
        
        List<String> exp2 = Arrays.asList(
            "java.lang.Object.wait(long, int)", 
            "java.lang.Object.wait(long)", 
            "java.lang.Object.wait()"
        );
        assertFilter(exp2, MethodInspector.inclass(C2.class)
            .published()
            .name("wait")
        );
        
        List<String> exp3 = Arrays.asList(
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubC2(int)", 
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubA(int)", 
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubB(int)" 
        );
        assertFilter(exp3, MethodInspector.inclass(C2.class)
            .published()
            .params(int.class)
        );
        
    }
    
    @Test
    public void testParams() {
        List<String> exp1 = Arrays.asList(
            NS + "$CovariantBoxing.sum1(int, int)"
        );
        assertFilter(exp1, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .params(int.class, int.class)
        );
        
        List<String> exp2 = Arrays.asList(
            NS + "$CovariantBoxing.sum2(java.lang.Integer, java.lang.Integer)"
        );
        assertFilter(exp2, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .params(Integer.class, Integer.class)
        );
        
        List<String> exp3 = Arrays.asList(
            NS + "$CovariantBoxing.sum1(int, int)",
            NS + "$CovariantBoxing.sum2(java.lang.Integer, java.lang.Integer)"
        );
        assertFilter(exp3, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .autoboxing()
            .params(int.class, int.class)
        );
        assertFilter(exp3, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .autoboxing()
            .params(Integer.class, Integer.class)
        );
        
        assertFilter(exp1, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .covariant()
            .params(int.class, int.class)
        );
        
        List<String> exp4 = Arrays.asList(
            NS + "$CovariantBoxing.sum2(java.lang.Integer, java.lang.Integer)",
            NS + "$CovariantBoxing.sum3(java.lang.Number, java.lang.Number)",
            NS + "$CovariantBoxing.sum4(java.lang.Object, java.lang.Object)"
        );
        assertFilter(exp4, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .covariant()
            .params(Integer.class, Integer.class)
        );
        List<String> exp5 = Arrays.asList(
            NS + "$CovariantBoxing.sum1(int, int)",
            NS + "$CovariantBoxing.sum2(java.lang.Integer, java.lang.Integer)",
            NS + "$CovariantBoxing.sum3(java.lang.Number, java.lang.Number)",
            NS + "$CovariantBoxing.sum4(java.lang.Object, java.lang.Object)"
        );
        assertFilter(exp5, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .covariant()
            .autoboxing()
            .params(int.class, int.class)
        );
        assertFilter(exp5, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .covariant()
            .autoboxing()
            .params(Integer.class, Integer.class)
        );
        
        // smoke test
        assertFilter(exp5, MethodInspector.inclass(CovariantBoxing.class)
            .published()
            .covariant()
            .autoboxing()
            .params(new TypeToken<Integer>(){}, new TypeToken<Integer>(){})
        );

    }

    
    @Test
    public void testReturns() {
        List<String> exp1 = Arrays.asList(
            NS + "$CovariantBoxing.div(" + NS + "$I0)",
            NS + "$CovariantBoxing.div(" + NS + "$I1)",
            NS + "$CovariantBoxing.div(" + NS + "$I2)",
            NS + "$CovariantBoxing.div(" + NS + "$I3)"
        );
        assertFilter(exp1, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
        );
        
        List<String> exp2 = Arrays.asList(
            NS + "$CovariantBoxing.div(" + NS + "$I1)"
        );
        assertFilter(exp2, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .returns(int.class)
        );
        
        List<String> exp3 = Arrays.asList(
            NS + "$CovariantBoxing.div(" + NS + "$I2)"
        );
        assertFilter(exp3, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .returns(Integer.class)
        );
        
        List<String> exp4 = Arrays.asList(
            NS + "$CovariantBoxing.div(" + NS + "$I1)",
            NS + "$CovariantBoxing.div(" + NS + "$I2)"
        );
        assertFilter(exp4, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .autoboxing()
            .returns(int.class)
        );
        assertFilter(exp4, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .autoboxing()
            .returns(Integer.class)
        );

        
        assertFilter(exp2, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .returns(int.class)
        );
        
        assertFilter(exp3, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .returns(Integer.class)
        );
        
        List<String> exp5 = Arrays.asList(
            NS + "$CovariantBoxing.div(" + NS + "$I2)",
            NS + "$CovariantBoxing.div(" + NS + "$I3)"
        );
        assertFilter(exp5, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .returns(Number.class)
        );
        
        List<String> exp6 = Arrays.asList(
            NS + "$CovariantBoxing.div(" + NS + "$I0)",
            NS + "$CovariantBoxing.div(" + NS + "$I2)",
            NS + "$CovariantBoxing.div(" + NS + "$I3)"
        );
        assertFilter(exp6, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .returns(Object.class)
        );
        
        assertFilter(exp4, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .autoboxing()
            .returns(Integer.class)
        );
        
        assertFilter(exp1, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .autoboxing()
            .returns(Object.class)
        );
        
        // smoke test
        assertFilter(exp4, MethodInspector.inclass(CovariantBoxing.class)
            .published().name("div")
            .covariant()
            .autoboxing()
            .returns(new TypeToken<Integer>(){})
        );
    }
    
    @Test
    public void testFollowCheck() {
        
        assertNotNull( MethodInspector.inclass(C1.class).follow() );
        
        assertNotNull( MethodInspector.inclass(C1.class).follow(A.class) );
        
        assertNotNull( MethodInspector.inclass(C1.class).follow(NS+"$A") );
        
        assertNotNull( MethodInspector.inclass(C1.class).follow(new TypeToken<A>() {}) );
        
        try {
            MethodInspector.inclass(C1.class).follow(C2.class);
            fail("InspectException expected");
        } catch(InspectException cause) {
            assertTrue(true);
        }
        
        try {
            MethodInspector.inclass(C1.class).follow(NS+"$C2");
            fail("InspectException expected");
        } catch(InspectException cause) {
            assertTrue(true);
        }
        
        try {
            MethodInspector.inclass(C1.class).follow(new TypeToken<C2>() {});
            fail("InspectException expected");
        } catch(InspectException cause) {
            assertTrue(true);
        }
    }
    
    @Test
    public void testFollow() {
        List<String> exp1 = Arrays.asList(
            "net.ranides.assira.reflection.mock.ForClassInspector$C1.pubA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C1.pubC1(int)"
        );
        
        List<String> exp2 = Arrays.asList(
            "java.lang.Object.clone()",
            "java.lang.Object.equals(java.lang.Object)",
            "java.lang.Object.finalize()",
            "java.lang.Object.getClass()",
            "java.lang.Object.hashCode()",
            "java.lang.Object.notify()",
            "java.lang.Object.notifyAll()",
            "java.lang.Object.registerNatives()",
            "java.lang.Object.toString()",
            "java.lang.Object.wait()",
            "java.lang.Object.wait(long)",
            "java.lang.Object.wait(long, int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.prvA(float)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.prvA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.pubA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.tecA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.prvB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.pubB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.tecB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C1.pubC1(int)"
        );
        
        List<String> exp3 = Arrays.asList(
            "net.ranides.assira.reflection.mock.ForClassInspector$A.prvA(float)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.prvA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.pubA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$A.tecA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.prvB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.pubB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.tecB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C1.pubC1(int)"
        );
        
        List<String> exp4 = Arrays.asList(
            "net.ranides.assira.reflection.mock.ForClassInspector$B.prvB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.pubB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$B.tecB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C1.pubA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C1.pubC1(int)"
        );
        
        
        
        assertFilter(exp1, MethodInspector.inclass(C1.class).declared());
        
        assertFilter(exp2, MethodInspector.inclass(C1.class).follow().declared());
        
        assertFilter(exp3, MethodInspector.inclass(C1.class).follow(Object.class).declared());
        
        assertFilter(exp4, MethodInspector.inclass(C1.class).follow(A.class).declared());
        
        List<String> exp5 = Arrays.asList(
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.prvC2(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubA(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.pubC2(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.tecB(int)",
            "net.ranides.assira.reflection.mock.ForClassInspector$C2.tecC2(int)"
        );
        
        assertFilter(exp5, MethodInspector.inclass(C2.class).declared());
        
        assertTrue(true);
    }
    
}
