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

import org.junit.Test;

import net.ranides.assira.collection.lists.IntList;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.collection.sets.SetUtils;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.reflection.*;
import net.ranides.test.mockup.reflection.ForBeanModel;
import net.ranides.assira.text.ResolveFormatUtils;

import java.io.Serializable;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.function.Supplier;

import static net.ranides.assira.junit.NewAssert.*;
import static net.ranides.assira.reflection.IAttribute.*;
import static net.ranides.assira.reflection.IAttributes.of;
import static org.junit.Assert.assertEquals;

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

    @Test
    public void testConstructors() {
        List<String> exp1 = Arrays.asList(
            "net.ranides.assira.reflection.impl.AClassTest$A[]"
        );
        List<String> exp2 = Arrays.asList(
            "net.ranides.assira.reflection.impl.AClassTest$B[int]",
            "net.ranides.assira.reflection.impl.AClassTest$B[int, int]",
            "net.ranides.assira.reflection.impl.AClassTest$B[]"
        );
        
        List<String> list1 = IClass.typeinfo(A.class)
            .constructors()
            .require(DECLARED)
            .accepts()
            .list(c -> c.name()+c.arguments().types());
        assertEquivalent(exp1, list1);
        
        List<String> list2 = IClass.typeinfo(B.class)
            .constructors()
            .require(DECLARED)
            .list(c -> c.name()+c.arguments().types());
        assertEquivalent(exp2, list2);

    }
    
    @Test
    public void testConstruct() {
        IClass<?> ic = IClass.typeinfo(B.class);
        
        assertEquals("B{777:777}", ic.construct().toString());
        assertEquals("B{3:3}", ic.construct(3).toString());
        assertEquals("B{5:4}", ic.construct(5,4).toString());
    }
    
    @Test
    public void testNULL() {
        IClass<?> ic = IClass.NULL;
        
        assertSame(IContext.getDefault(), ic.context());
        assertEquals(Collections.emptyList(), ic.params());
        assertEquals(Collections.emptyList(), ic.fields().list());
        assertEquals(Collections.emptyList(), ic.constructors().list());
        assertEquals(Collections.emptyList(), ic.methods().list());
        assertEquals(Collections.emptyList(), ic.parents().list());
        assertEquals(Collections.emptyList(), ic.interfaces().list());
        assertEquals(Collections.emptyList(), ic.inner().list());
        assertEquals(Collections.emptyList(), ic.annotations().list());
        assertEquals(of(FINAL, NATIVE, PUBLIC, SYMBOLIC), ic.attributes());
        
        assertEquals(false, ic.attributes().contains(PRIMITIVE));
        assertEquals(true, ic.isInstance(null));
        assertEquals(true, ic.isInstance(55));
        assertEquals(true, ic.isInstance("hello"));
        
        assertThrows(NoSuchElementException.class, ()->{
            ic.method("hello").get();
        });
        assertThrows(IReflectiveException.class, ()->{
            ic.parent();
        });
        assertThrows(IReflectiveException.class, ()->{
            ic.outer();
        });
        assertThrows(IReflectiveException.class, ()->{
            ic.construct();
        });
        assertThrows(IReflectiveException.class, ()->{
            ic.construct(1,2,3);
        });
        assertThrows(IReflectiveException.class, ()->{
            ic.reflective();
        });
        assertThrows(IReflectiveException.class, ()->{
            ic.raw();
        });
    }
    
    @Test
    public void testCollect() {
        assertEquals(IClasses.typeinfo(int.class).list(), IClass.typeinfo(int.class).collect().list());
        assertNotEquals(IClasses.typeinfo(float.class).list(), IClass.typeinfo(int.class).collect().list());
    }
    
    @Test
    public void testVoid() {
        IAttributes exp1 = of(PUBLIC, FINAL, ABSTRACT, DECLARED, PRIMITIVE, VOID, RESOLVED);
        IAttributes exp2 = of(PUBLIC, FINAL, DECLARED, BOXED, VOID, RESOLVED);
        IAttributes exp3 = of(PUBLIC, FINAL, ABSTRACT, DECLARED, PRIMITIVE, NUMBER, INTEGER, RESOLVED);
        IAttributes exp4 = of(PUBLIC, FINAL, ABSTRACT, DECLARED, PRIMITIVE, VOID, RESOLVED);
        
        assertEquals(exp1, IClass.typeinfo(void.class).attributes());
        assertEquals(exp2, IClass.typeinfo(Void.class).attributes());
        assertEquals(exp3, IClass.typeinfo(ForBeanModel.class).method("getArea").get().returns().attributes());
        assertEquals(exp4, IClass.typeinfo(ForBeanModel.class).method("setList").get().returns().attributes());
    }

    enum Foo {
        A, B, C
    }

    enum Bar {
        A, B, C
    }

    @Test
    public void testRecursiveEnum() {
        IClass<?> foo = IClass.typeinfo(Foo.class);
        IClass<?> bar = IClass.typeinfo(Bar.class);


        String foo1raw = "net.ranides.assira.reflection.impl.AClassTest$Foo";

        String bar1raw = "net.ranides.assira.reflection.impl.AClassTest$Bar";

        assertEquals(foo1raw, foo.toString());
        assertEquals(bar1raw, bar.toString());

        assertEquals("[]", bar.params().toString());

        assertTrue(foo.isInstance(Foo.A));
        assertTrue(bar.isInstance(Bar.A));

        assertFalse(foo.isInstance(Bar.A));
        assertFalse(bar.isInstance(Foo.A));
    }


    @Test
    public void testRecursiveOthers() {
        IClass<?> spec = new TypeToken<MySpec1<MySpec2>>() {};

        String exp1 = ResolveFormatUtils.nformat(
            "{MySpec1}<{MySpec2}>",
            "MySpec1 = net.ranides.assira.reflection.impl.AClassTest$MySpec1",
            "MySpec2 = net.ranides.assira.reflection.impl.AClassTest$MySpec2"
        );

        assertEquals(exp1, spec.toString());
    }

    @Test
    public void testRecursiveBuilder() {
        String exp1 = "net.ranides.assira.reflection.impl.AClassTest$MyBuilder2";

        String exp2 = "net.ranides.assira.reflection.impl.AClassTest$MyBuilder3";


        IClass<?> builder2 = new TypeToken<MyBuilder2>() {};
        IClass<?> builder3 = new TypeToken<MyBuilder3>() {};
        IClass<?> mylist = new TypeToken<MyArrayList>() {};

        IMethod append1 = builder2.method("append1").get();
        IMethod append2 = builder2.method("append2").get();

        IMethod append0 = builder3.method("append0").get();
        IMethod append3 = builder3.method("append3").get();

        IMethod listAt = mylist.method("at").get();
        IMethod listAvg = mylist.method("average").get();

        assertEquals(exp1, append1.returns().toString());
        assertEquals(exp1, append2.returns().toString());

//        assertEquals(exp2, append0.returns().toString()); // sorry, we can't deduce interface arguments
        assertEquals(exp2, append3.returns().toString());

//        System.out.println(listAt.returns().toString()); // sorry, we can't deduce interface arguments
        assertEquals("double", listAvg.returns().toString());

//        System.out.println();
    }

    @Test
    public void testInterfaces() {
        IClass<MyIntList> type1 = IClass.typeinfo(MyIntList.class);

        Set<String> declare1 = type1.interfaces().require(DECLARED).stream(Object::toString).set();
        Set<String> inherit1 = type1.interfaces().stream(Object::toString).set();

        String[] expect1 = {
            "net.ranides.assira.collection.lists.IntList",
            "java.util.List<java.lang.Integer>",
            "java.io.Serializable"
        };
        String[] expect2 = {
            "net.ranides.assira.collection.lists.IntList",
            "java.util.List<java.lang.Integer>",
            "java.io.Serializable",
            "java.util.Collection<java.lang.Integer>",
            "java.lang.Iterable<java.lang.Integer>",
            "net.ranides.assira.collection.IntCollection"
        };

        assertEquals(new OpenSet<>(expect1), declare1);
        assertEquals(new OpenSet<>(expect2), inherit1);

        IClass<MySubList> type2 = IClass.typeinfo(MySubList.class);
        Set<String> declare2 = type2.interfaces().require(DECLARED).stream(Object::toString).set();
        Set<String> inherit2 = type2.interfaces().stream(Object::toString).set();

        String[] expect3 = {
            "java.util.List<java.lang.Integer>",
            "java.util.Queue<java.lang.Integer>"
        };

        String[] expect4 = {
            "net.ranides.assira.collection.lists.IntList",
            "java.util.List<java.lang.Integer>",
            "java.util.Queue<java.lang.Integer>",
            "java.io.Serializable",
            "java.lang.Iterable<java.lang.Integer>",
            "net.ranides.assira.collection.IntCollection",
            "java.util.Collection<java.lang.Integer>"
        };

        assertEquals(new OpenSet<>(expect3), declare2);
        assertEquals(new OpenSet<>(expect4), inherit2);
    }


    public abstract class MySubList extends MyIntList implements Queue<Integer>, List<Integer> {

    }

    public abstract class MyIntList implements List<Integer>, Serializable, IntList {

    }

    private interface MySpec1<T extends AbstractList<Supplier<T>>>  {

    }

    private static abstract class MySpec2 extends AbstractList<Supplier<MySpec2>> {

    }

    public static class MyBuilder1<P extends MyBuilder1<P>> {

        public P append1() {
            return null;
        }

    }

    private static class MyBuilder2 extends MyBuilder1<MyBuilder2> {

        public MyBuilder2 append2() {
            return null;
        }

    }

    public interface MyBuilder0<P extends MyBuilder0<P>> {

        default P append0() {
            return null;
        }

    }

    private static class MyBuilder3 implements MyBuilder0<MyBuilder3> {

        public MyBuilder3 append3() {
            return null;
        }

    }

    public interface MyList<T> {

        default T at(int index) {
            return null;
        }

    }

    private static class MyArrayList implements MyList<Integer> {

        public double average() {
            return 0.0;
        }

    }

    @Test
    public void testRecursiveString() {
        IClass<?> type = IClass.typeinfo(ThisSomething.class);

        String ex0 = "net.ranides.assira.reflection.impl.AClassTest$ThisSomething";

        String ex1 = ResolveFormatUtils.nformat(
            "void {some}<{float}>.run1({some}<{float}> x)",
            "some  = net.ranides.assira.reflection.impl.AClassTest$Something",
            "float = java.lang.Float"
        );

        String ex2 = ResolveFormatUtils.nformat(
            "void {some}<{float}>.run2({some}<{some}<{float}>> x)",
            "some  = net.ranides.assira.reflection.impl.AClassTest$Something",
            "float = java.lang.Float"
        );

        String ex3 = ResolveFormatUtils.nformat(
            "void {some}<{float}>.run3({some}<{some}<{some}<{float}>>> x)",
            "some  = net.ranides.assira.reflection.impl.AClassTest$Something",
            "float = java.lang.Float"
        );

        assertEquals(ex0, type.toString());
        assertEquals(ex1, type.method("run1").get().toString());
        assertEquals(ex2, type.method("run2").get().toString());
        assertEquals(ex3, type.method("run3").get().toString());
    }

    public class ThisSomething extends Something<Float> { };

    public class Something<T> {

        public void run1(Something<T> x) {
            //
        }

        public void run2(Something<Something<T>> x) {
            //
        }

        public void run3(Something<Something<Something<T>>> x) {
            //
        }

    }

    private static class A { }
    
    private static class B { 
        
        public final int x;
        public final int y;
        
        public B(int x) {
            this.x = x;
            this.y = x;
        }
        
        protected B(int x, int y) { 
            this.x = x;
            this.y = y;
        }
        
        private B() {
            this.x = 777;
            this.y = 777;
        }

        @Override
        public String toString() {
            return "B{" + x + ":" + y + '}';
        }

    }
    
}
