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

import java.util.*;

import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.maps.MapUtils;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.reflection.*;
import net.ranides.assira.system.RuntimeConfiguration;
import net.ranides.test.mockup.reflection.ForBeanModel;
import static net.ranides.assira.junit.NewAssert.*;

import net.ranides.test.mockup.reflection.ForBeanModel2;
import net.ranides.test.mockup.reflection.ForFluentBean;
import net.ranides.test.mockup.reflection.ForIClass;
import org.junit.Test;

/**
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class RBeanModelTest {
    
    private static final Map<String, String> METHODS = new OpenMap<>(
        new String[]{
            "toString", 
            "equals", 
            "hashCode", 
            
            "random", 
            "getMail", 
            "sendMail", 
            "setConfig", 
            "configure", 
            
            "open",
            "connect",
        }, new String[]{
            "RBeanMethod:toString", 
            "RBeanMethod:equals", 
            "RBeanMethod:hashCode", 
            
            "RBeanMethod:random",
            "RBeanMethod:getMail",
            "RBeanMethod:sendMail",
            "RBeanMethod:setConfig",
            "RBeanMethod:configure",
            
            "RBeanMethods:open", 
            "RBeanMethods:connect", 
    });

    @Test
    public void testMethods() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);

        Map<String, String> properties = MapUtils.valueAdapter(model.methods(), p ->
            p.getClass().getSimpleName() + ":" + p.name()
        );
        assertEquals(METHODS, properties);
    }
    
    @Test
    public void testMethod() {
        BeanModel model = BeanModel.typeinfo(ForBeanModel.class);
        assertNotNull(model.method("open"));
        assertThrows(NoSuchElementException.class, ()->{
            model.method("unknown").get();
        });
    }
    
    @Test
    public void testMethodCall() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);
        
        assertEquals(bean.hashCode(), model.method("hashCode").get().invoke(bean));
        assertEquals(false, model.method("equals").get().invoke(bean, 999));
        
        assertEquals(bean.random(), model.method("random").get().invoke(bean));
        assertEquals(bean.getMail("ranides", 777), model.method("getMail").get().invoke(bean, "ranides", 777));
        assertNull(model.method("setConfig").get().invoke(bean, "path", "/root"));
        assertEquals("path=/root", bean.$result);
        
        assertThrows(IReflectiveException.class, ()->{
            model.method("setConfig").get().invoke(null,"home","/root");
        });
        assertThrows(IReflectiveException.class, ()->{
            model.method("setConfig").get().invoke(bean,"home",17);
        });
        assertThrows(IReflectiveException.class, ()->{
            model.method("setConfig").get().invoke(bean,"home");
        });
    }
    
    @Test
    public void testMethodsCall() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typeinfo(ForBeanModel.class);
        
        BeanMethod m1 = model.method("connect").get();
        BeanMethod m2 = model.method("open").get();
        
        assertEquals(null, m1.invoke(bean));
        assertEquals("connect:void", bean.$result);
        
        assertEquals(true, m1.invoke(bean,17));
        assertEquals("connect:int:17", bean.$result);
        
        assertEquals("c17", m1.invoke(bean,17.1f));
        assertEquals("connect:float:17.1", bean.$result);
        
        assertEquals(2, m1.invoke(bean,17.1));
        assertEquals("connect:double:17.1", bean.$result);
        
        assertEquals(null, m1.invoke(bean,5,6));
        assertEquals("connect:5/6", bean.$result);
        
        m2.invoke(bean, "home");
        assertEquals("open:home", bean.$result);
        m2.invoke(bean, "path", 777);
        assertEquals("open:path/777", bean.$result);
        
        assertThrows(IReflectiveException.class, ()->{
            model.method("open").get().invoke(null,"home",777);
        });
        assertThrows(IReflectiveException.class, ()->{
            model.method("open").get().invoke(bean);
        });
        assertThrows(IReflectiveException.class, ()->{
            model.method("open").get().invoke(bean,"home","777");
        });
    }
    
    @Test
    public void testMethodBind() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);
        
        assertEquals(bean.hashCode(), model.method("hashCode").get().bind(bean).call());
        assertEquals(false, model.method("equals").get().bind(bean).call(999));
        
        assertEquals(bean.random(), model.method("random").get().bind(bean).call());
        assertEquals(bean.getMail("ranides", 777), model.method("getMail").get().bind(bean).call("ranides", 777));
        assertNull(model.method("setConfig").get().bind(bean).call("path", "/root"));
        assertEquals("path=/root", bean.$result);
        
        assertThrows(IReflectiveException.class, ()->{
            model.method("setConfig").get().bind(null);
        });
        assertThrows(IReflectiveException.CallException.class, ()->{
            model.method("setConfig").get().bind(bean).call("home",17);
        });
        assertThrows(IReflectiveException.class, ()->{
            model.method("setConfig").get().bind(bean).call("home");
        });
    }
    
    @Test
    public void testMethodsBind() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typeinfo(ForBeanModel.class);
        
        AnyFunction<?> m1 = model.method("connect").get().bind(bean);
        AnyFunction<?> m2 = model.method("open").get().bind(bean);
        
        assertEquals(null, m1.call());
        assertEquals("connect:void", bean.$result);
        
        assertEquals(true, m1.call(17));
        assertEquals("connect:int:17", bean.$result);
        
        assertEquals("c17", m1.call(17.1f));
        assertEquals("connect:float:17.1", bean.$result);
        
        assertEquals(2, m1.call(17.1));
        assertEquals("connect:double:17.1", bean.$result);
        
        assertEquals(null, m1.call(5,6));
        assertEquals("connect:5/6", bean.$result);
        
        m2.call("home");
        assertEquals("open:home", bean.$result);
        m2.call("path", 777);
        assertEquals("open:path/777", bean.$result);
        
        assertThrows(IReflectiveException.class, ()->{
            model.method("open").get().bind(null);
        });
        assertThrows(IReflectiveException.class, ()->{
            m2.call(bean);
        });
        assertThrows(IReflectiveException.class, ()->{
            m2.call("home","777");
        });
    }
    
    @Test
    public void testMethodMatches() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);
        
        assertTrue(model.method("hashCode").get().matches());
        assertFalse(model.method("hashCode").get().matches(Object.class));
        
        assertTrue(model.method("random").get().matches());
        assertFalse(model.method("random").get().matches(Integer.class));
        
        assertTrue(model.method("getMail").get().matches(String.class, int.class));
        assertFalse(model.method("getMail").get().matches(String.class, Integer.class));
    }
    
    @Test
    public void testMethodsMatches() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);
        
        assertTrue(model.method("open").get().matches(String.class));
        assertTrue(model.method("open").get().matches(String.class, int.class));
        
        assertTrue(model.method("connect").get().matches());
        assertTrue(model.method("connect").get().matches(ForIClass.INT));
        assertTrue(model.method("connect").get().matches(float.class));
        assertFalse(model.method("connect").get().matches(Float.class));
        assertTrue(model.method("connect").get().matches(double.class));
        assertTrue(model.method("connect").get().matches(ForIClass.INT, ForIClass.I_INTEGER));
        assertFalse(model.method("connect").get().matches(ForIClass.INT, ForIClass.INT));
    }
    
    @Test
    public void testMethodAccepts() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);
        
        assertTrue(model.method("hashCode").get().accepts());
        assertFalse(model.method("hashCode").get().accepts("333"));
        
        assertTrue(model.method("random").get().accepts());
        assertFalse(model.method("random").get().accepts(444));
        
        assertTrue(model.method("getMail").get().accepts("a",2));
        assertTrue(model.method("getMail").get().accepts("a", Integer.valueOf(4)));
        assertFalse(model.method("getMail").get().accepts("a", null));
    }
    
    @Test
    public void testMethodsAccepts() {
        ForBeanModel bean = new ForBeanModel();
        BeanModel model = BeanModel.typefor(bean);
        
        assertTrue(model.method("open").get().accepts("s"));
        assertTrue(model.method("open").get().accepts("s",1));
        assertTrue(model.method("open").get().accepts(new Object[]{null}));
        assertTrue(model.method("open").get().accepts(null,1));
        
        assertTrue(model.method("connect").get().accepts());
        assertTrue(model.method("connect").get().accepts(4));
        assertTrue(model.method("connect").get().accepts(14.3f));
        assertTrue(model.method("connect").get().accepts(14.3));
        assertFalse(model.method("connect").get().accepts(new Object[]{null}));
        assertTrue(model.method("connect").get().accepts(1, 2));
        assertTrue(model.method("connect").get().accepts(1, null));
        assertFalse(model.method("connect").get().accepts(null,null));
    }
    
    @Test
    public void testConstruct() {
        BeanModel model = BeanModel.typeinfo(ForBeanModel.class);
        
        
        ForBeanModel v1 = (ForBeanModel)model.construct();
        assertNotNull(v1);
        
        Map<String,Object> params = new OpenMap<>(new String[]{"height","width","array"}, new Object[]{
            16,
            20,
            Arrays.asList("1","2")
        });
        ForBeanModel v2 = (ForBeanModel)model.construct(params);
        assertEquals(16, v2.$height);
        assertEquals(20, v2.$width);
        assertArrayEquals(new String[]{"1","2"}, v2.$array);
        
        assertEquals(IClass.typefor(v1), model.type());
        
        ForBeanModel v3 = (ForBeanModel)model.clone(v2);
        assertEquals(0, v3.$height);
        assertEquals(20, v3.$width);
        assertArrayEquals(new String[]{"1","2"}, v3.$array);
    }

    @Test
    public void testGeneric() {
        Point<Integer> point = new Point<>(1, 5, 6);
        BeanModel model = BeanModel.typefor(point);

        assertTrue(model.properties().get("x").isReadable());
        assertTrue(model.properties().get("x").isWritable());
    }

    @Test
    public void testFluent() {
        Point<Integer> point = new Point<>(1, 5, 6);
        BeanModel model = BeanModel.typefor(point);
        Map<String, Object> fluent = model.fluent(point);

        assertEquals(Arrays.asList("length", "updateScale", "x", "y"), new ArrayList<>(new TreeSet<>(fluent.keySet())));
        assertEquals(5, fluent.get("x"));
        assertEquals(6, fluent.get("y"));
        assertEquals(8, fluent.get("length"));

        fluent.put("x", 3);
        fluent.put("y", 4);
        assertEquals(5, fluent.get("length"));
        fluent.put("updateScale", 4);
        assertEquals(20, fluent.get("length"));

        assertThrows(UnsupportedOperationException.class, IReflectiveException.class, () -> {
            fluent.put("length", 4);
        });
        assertThrows(UnsupportedOperationException.class, IReflectiveException.class, () -> {
            fluent.put("other", 4);
        });
    }

    @Test
    public void testPrivateAccess() {
        Object v = ForFluentBean.newHidden("hidden value");
        BeanModel.FluentMap f = BeanModel.typefor(v).fluent(v);

        assertEquals("hidden name", f.get("name"));
        assertEquals("hidden value", f.get("value"));
        assertEquals("hidden value", f.put("value", "other"));
        assertEquals("other", f.get("value"));
    }

    @Test
    public void testPrivateAccessParents() {
        Object v = ForBeanModel2.newHidden();
        BeanModel.FluentMap f = BeanModel.typefor(v).fluent(v);

        ForBeanModel2.PublicIntermediate z = new ForBeanModel2.PublicIntermediate();

        assertNotNull(z.something());
        assertNotNull(z.age());
        assertNotNull(z.name());

        assertEquals(new TreeSet<>(Arrays.asList("something", "age", "name", "mymethod")), new TreeSet<>(f.keySet()));
    }

    public static class Point<N extends Number> {
        private N scale;
        private N x;
        private N y;

        public Point(N scale, N x, N y) {
            this.scale = scale;
            this.x = x;
            this.y = y;
        }

        public N getX() {
            return x;
        }

        public void setX(N x) {
            this.x = x;
        }

        public N getY() {
            return y;
        }

        public void setY(N y) {
            this.y = y;
        }

        public int length() {
            return scale.intValue() * (int)Math.round(Math.sqrt(x.intValue() * x.intValue() + y.intValue() * y.intValue()));
        }

        public void updateScale(N scale) {
            this.scale = scale;
        }
    }
}
