/*
 * @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 net.ranides.assira.collection.lists.ListUtils;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.reflection.*;
import net.ranides.test.mockup.reflection.ForRMethod;
import net.ranides.test.mockup.reflection.ForRMethod.TAction;
import net.ranides.assira.text.ResolveFormatUtils;

import org.hamcrest.CoreMatchers;
import org.junit.Assume;
import org.junit.AssumptionViolatedException;
import org.junit.Test;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static net.ranides.assira.junit.NewAssert.assertEquals;
import static net.ranides.assira.junit.NewAssert.assertThrows;
import static org.junit.Assert.assertTrue;

/**
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@SuppressWarnings({"PMD.AvoidDuplicateLiterals", "OptionalGetWithoutIsPresent"})
public class RMethodTest {
    
    private static final String RECORD_TYPE = "net.ranides.test.mockup.reflection.ForRMethod<java.lang.Float>$Record<java.lang.Double>";
    
    @Test
    public void testJVM1() {
        try {
            itestJVM1();
        } catch (java.lang.reflect.GenericSignatureFormatError e) {
            throw new AssumptionViolatedException("Detected JVM bug.", e);
        }
    }

    private void itestJVM1() {
        // this declarations are correct
        ForRMethod<Float> parent1 = new ForRMethod<Float>(){};
        Object record1 = parent1.new Record<Double>(){};
        Type type1 = record1.getClass().getGenericSuperclass();

        Assume.assumeThat(type1.toString(), CoreMatchers.equalTo(RECORD_TYPE));

        // Exception thrown from here is very suprising:
        // java.lang.reflect.GenericSignatureFormatError: Signature Parse error:
        //      ; expected in signature of type variable named
        //      Remaining input: <captured wildcard>;>.Record<Ljava/lang/Double;>;

        // It is not our fault. Compiler generates something wrong and JVM itself can parse it.

//        ForRMethod<? super Float> parent2 = new ForRMethod<Float>(){};
//        Object record2 = parent2.new Record<Double>(){};
//        Type type2 = record2.getClass().getGenericSuperclass();
//
//        Assume.assumeThat(type2.toString(), CoreMatchers.equalTo(RECORD_TYPE));

    }
    
    @Test
    public void testJVM2() {
        ForRMethod<Float> parent = new ForRMethod<Float>(){};
        Object record = parent.new Record<Double>(){};
        String type = record.getClass().getGenericSuperclass().getTypeName();

        Assume.assumeThat(type, CoreMatchers.equalTo(RECORD_TYPE));
    }
    
    @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
    @Test
    public void testMethodReturs() {
        // Ja pierdolę: uzupełniamy TypeParams poprawnie. Szok.
        // Jakkolwiek JVM trochę abstrakcyjnie te sygnatury generuje ;)

        ForRMethod p0 = new ForRMethod(){};
        ForRMethod<Float> p1 = p0;
        ForRMethod<String> p2 = p0;
        
        Object o1 = p1.new Record<Double>(){};
        Object o2 = p2.new Record<Character>(){};
        
        IClass<?> ic1 = IClass.typefor(o1);
        IClass<?> ic2 = IClass.typefor(o2);
        
        String o1map1 = ic1.methods().require("map1").first().get().toString();
        String o1map2 = ic1.methods().require("map2").first().get().toString();
        String o2map1 = ic2.methods().require("map1").first().get().toString();
        String o2map2 = ic2.methods().require("map2").first().get().toString();
        
        assertEquals(Arrays.asList("Q=class java.lang.Double", "T=class java.lang.Float"), asText(ic1.context()));
        assertEquals(Arrays.asList("Q=class java.lang.Character", "T=class java.lang.String"), asText(ic2.context()));
        
//        System.out.printf("1.context = %s%n", LexicalCast.toJSON(ic1.context().map()));
//        System.out.printf("2.context = %s%n", LexicalCast.toJSON(ic2.context().map()));

        String e1map1 = ResolveFormatUtils.nformat(
            "{map}<{float}, {map}<{double}, {map}<R, {int}>>> {record}<{double}>.map1(R a, {float} b, {double} c, {int} d)",
            "record = net.ranides.test.mockup.reflection.ForRMethod$Record",
            "map    = java.util.Map",
            "float  = java.lang.Float",
            "double = java.lang.Double",
            "int    = java.lang.Integer"
        );

        String e1map2 = ResolveFormatUtils.nformat(
            "{map}<{float}, {map}<{double}, {map}<R, {int}>>> {record}<{double}>.map2({list}<R> a, {list}<{float}> b, {list}<{double}> c, {list}<{int}> d)",
            "record = net.ranides.test.mockup.reflection.ForRMethod$Record",
            "map    = java.util.Map",
            "float  = java.lang.Float",
            "double = java.lang.Double",
            "int    = java.lang.Integer",
            "list   = java.util.List"
        );
        String e2map1 = ResolveFormatUtils.nformat(
            "{map}<{string}, {map}<{char}, {map}<R, {int}>>> {record}<{char}>.map1(R a, {string} b, {char} c, {int} d)",
            "record = net.ranides.test.mockup.reflection.ForRMethod$Record",
            "map    = java.util.Map",
            "string = java.lang.String",
            "char   = java.lang.Character",
            "int    = java.lang.Integer"
        );
        String e2map2 = ResolveFormatUtils.nformat(
            "{map}<{string}, {map}<{char}, {map}<R, {int}>>> {record}<{char}>.map2({list}<R> a, {list}<{string}> b, {list}<{char}> c, {list}<{int}> d)",
            "record = net.ranides.test.mockup.reflection.ForRMethod$Record",
            "map    = java.util.Map",
            "string = java.lang.String",
            "char   = java.lang.Character",
            "int    = java.lang.Integer",
            "list   = java.util.List"
        );

        Assume.assumeThat(o1map1, CoreMatchers.equalTo(e1map1));
        Assume.assumeThat(o1map2, CoreMatchers.equalTo(e1map2));
        Assume.assumeThat(o2map1, CoreMatchers.equalTo(e2map1));
        Assume.assumeThat(o2map2, CoreMatchers.equalTo(e2map2));
    }
    
    @Test
    public void testTypeInfo() {
        IClass<?> type = IClass.typeinfo(TAction.class);
        
        IMethod m1 = type.methods().require("hello").first().get();
        IMethod m2 = type.methods().require("stat").first().get();
        
        IMethod r1 = IMethod.typeinfo(m1.reflective());
        IMethod r2 = IMethod.typeinfo(m2.reflective());
        
        assertEquals(m1, r1);
        assertEquals(m2, r2);
    }
    
    @Test
    public void testApply() {
        TAction a1 = new TAction(3);
        TAction a2 = new TAction(10);
        IMethod m1 = IClass.typefor(a1).methods().require("hello").first().get();
        IMethod m2 = IClass.typefor(a1).methods().require("stat").first().get();
        
        assertEquals("[6:9]", m1.invoke(a1, 2, 3));
        assertEquals("[20:30]", m1.invoke(a2, 2, 3));
        
        assertEquals("[2/3]", m2.invoke(a1, 2, 3));
        assertEquals("[2/3]", m2.invoke(null, 2, 3));
        
        assertThrows(IReflectiveException.class, ()->{
            m1.invoke(a1, "q", 3);
        });
        assertThrows(IReflectiveException.class, ()->{
            m1.invoke(a1, 1, 2, 3);
        });
        assertThrows(IReflectiveException.class, ()->{
            m1.invoke(null, 2, 3);
        });
    }
    
    @Test
    public void testCall() {
        TAction a1 = new TAction(3);
        TAction a2 = new TAction(10);
        IMethod m1 = IClass.typefor(a1).methods().require("hello").first().get();
        IMethod m2 = IClass.typefor(a1).methods().require("stat").first().get();
        
        assertEquals("[6:9]", m1.call(a1, 2, 3));
        assertEquals("[20:30]", m1.call(a2, 2, 3));
        
        assertEquals("[2/3]", m2.call(2, 3));
        assertThrows(IReflectiveException.class, ()->{
            m2.call(null, 2, 3);
        });
        assertThrows(IReflectiveException.class, ()->{
            m2.call("z", 3);
        });
        
        assertThrows(IReflectiveException.class, ()->{
            m1.call(a1, "q", 3);
        });
        assertThrows(IReflectiveException.class, ()->{
            m1.invoke(a1, 1, 2, 3);
        });
        assertThrows(IReflectiveException.class, ()->{
            m1.invoke(null, 2, 3);
        });
    }
    
    @Test
    public void testCallVoid() {
        TAction a1 = new TAction(3);
        IMethod m0 = IClass.typefor(a1).methods().require("nop").first().get();
        
        assertEquals("nop!", m0.call());
        assertEquals("nop!", m0.invoke());
    }

    @Test
    public void testParamsNames() {
        IClass<ForRMethod<Float>> type = new TypeToken<ForRMethod<Float>>(){};

        Assume.assumeTrue(type.method("params1").get() instanceof RMethod);

        List<IClass<?>> params1 = type.method("params1").get().params();
        List<IClass<?>> params2 = type.method("params2").get().params();
        List<IClass<?>> params3 = type.method("params3").get().params();
        List<IClass<?>> params4 = type.method("params4").get().params();
        List<IClass<?>> params5 = type.method("params5").get().params();

        assertEquals(0, params1.size());
        assertEquals(0, params2.size());
        assertEquals(1, params3.size());
        assertEquals(1, params4.size());
        assertEquals(2, params5.size());

        assertEquals(Arrays.asList(), ListUtils.map(params1, IClass::name));
        assertEquals(Arrays.asList(), ListUtils.map(params2, IClass::name));
        assertEquals(Arrays.asList("Q"), ListUtils.map(params3, IClass::name));
        assertEquals(Arrays.asList("Q"), ListUtils.map(params4, IClass::name));
        assertEquals(Arrays.asList("P", "R"), ListUtils.map(params5, IClass::name));
    }

    @Test
    public void testParamsResolve() {
        IClass<ForRMethod<Float>> type = new TypeToken<ForRMethod<Float>>(){};

        Assume.assumeTrue(type.method("params1").get() instanceof RMethod);

        List<IClass<?>> params5 = type.method("params5").get().params();

        assertTrue(params5.get(0).isSubclass(CharSequence.class));
        assertTrue(params5.get(1).isSubclass(Number.class));
    }

    @Test
    public void testGenericMethodArguments() {
        IClass<ForRMethod<Float>> type = new TypeToken<ForRMethod<Float>>(){};

        assertTrue(type.method("params3").get().matches(CharSequence.class, CharSequence.class));
        assertTrue(type.method("params3").get().accepts("", ""));

        assertTrue(type.methods().matches(IClasses.typeinfo(CharSequence.class, CharSequence.class)).stream().matchAny(v -> v.name().equals("params3")) );
        assertTrue(type.methods().accepts("", "").stream().matchAny(v -> v.name().equals("params3")) );
    }

    private static List<String> asText(IContext context) {
        return context.map().entrySet().stream()
            .map(Object::toString)
            .sorted()
            .collect(Collectors.toList());
    }
    
}
