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

import net.ranides.assira.reflection.inspect.Resolver;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.assira.generic.Serializer;
import net.ranides.assira.reflection.InspectException;
import net.ranides.assira.text.format.AnsiString;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class ResolverTest {
    
    @Test
    public void testSerialization() throws IOException {
        Resolver a = Resolver.compile("content.item.height");
        Resolver b = Serializer.copy(a);
        
        assertNotSame(a, b);
        assertEquals(a, b);
    }
    
    @Test
    public void testResolve() {
        A context = new A();
        int numbers[] = new int[]{1,2,3,4,5,6};

        // get: getter, field, final field
        assertEquals((Integer)5, Resolver.get(context, "x"));
        assertEquals((Integer)16, Resolver.get(context, "content.item.height"));
        assertEquals((Integer)1000, Resolver.get(context, "content.item.hello"));
        
        // set: setter, field
        Resolver.set(context, "x", 8);
        Resolver.set(context, "content.item.height", 80);
        Resolver.set(context, "content.item.hello", 800);
        assertEquals(8, context.x);
        assertEquals(80, context.content.item.height);
        assertEquals(800, context.content.item.getHello());

        Object newitemE = new E(0, 0, 801);
        Resolver.set(context, "content.item", newitemE);
        assertSame(newitemE, context.content.item);
        
        // get: map, list, array
        assertEquals((Integer)801, Resolver.get(context, "content.item.hello"));
        assertEquals((Long)7L, Resolver.get(context, "list[1].numbers#seven"));
        assertNull(Resolver.get(context, "list[1].numbers#eight"));
        assertEquals("B", Resolver.get(context, "list[1].name"));
        assertEquals("b", Resolver.get(context, "array[1].name"));
        
        // set: map.put
        Resolver.set(context, "list[1].numbers#seven", 77);
        assertEquals((Integer)77, context.list.get(1).numbers.get("seven"));
        
        // set: field (proxy map)
        Map<String,Integer> newmap = new HashMap<>();
        newmap.put("eight", 8);
        Resolver.set(context, "list[1].numbers", newmap);
        assertSame(newmap, context.list.get(1).numbers);
        
        // set: list
        Object newitem = new C("Q");
        Resolver.set(context, "list[1]", newitem);
        assertSame(newitem, context.list.get(1));
        
        // set: field (proxy list)
        List<?> newlist = Arrays.asList(new C("P"),new C("R"),new C("T"),new C("U"));
        Resolver.set(context, "list", newlist);
        assertSame(newlist, context.list);
        
        
        // get: (edge case) property handling
        assertEquals("R", Resolver.get(context, "list[1]name"));
        assertEquals("R", Resolver.get(context, ".list[1]name"));
        assertEquals("R", Resolver.get(context, ".list[1].name"));
        
        // get: (edge case) index handling
        assertEquals("R", Resolver.get(context, "list.1.name"));

        // get: (edge case) index handling if context is an array
        assertEquals((Integer)3, Resolver.get(numbers, ".2"));
        assertEquals((Integer)3, Resolver.get(numbers, "[2]"));
        assertEquals((Integer)3, Resolver.get(numbers, "2"));
        try {
            Resolver.get(numbers, ".[2]");
            fail("invalid format must not be accepted");
        } catch(IllegalArgumentException cause) {
            assertTrue(true);
        }
        try {
            Resolver.get(numbers, "[q]");
            fail("invalid format must not be accepted");
        } catch(IllegalArgumentException cause) {
            assertTrue(true);
        }
        try {
            Resolver.get(numbers, "0q");
            fail("invalid format must not be accepted");
        } catch(InspectException cause) {
            assertTrue(true);
        }        
        
        // get: (edge case) key handling if context is a map
        assertEquals((Integer)8, Resolver.get(newmap, "#eight"));
        assertNull(Resolver.get(newmap, "#unknown"));
        
        // set: (edge case) property handling
        Resolver.set(context, "x", 71);
        Resolver.set(context, ".y", 72);
        Resolver.set(context, "list[0]name", "S1");
        Resolver.set(context, ".list[1]name", "S2");
        Resolver.set(context, ".list[2].name", "S3");
        
        assertEquals(71, context.x);
        assertEquals(72, context.y);
        assertEquals("S1", context.list.get(0).name);
        assertEquals("S2", context.list.get(1).name);
        assertEquals("S3", context.list.get(2).name);
        
        // set: (edge case) index handling
        Resolver.set(context, "list.1.name", "U1");
        assertEquals("U1", context.list.get(1).name);

        // set: (edge case) index handling if context is an array
        Resolver.set(numbers, ".1", 501);
        Resolver.set(numbers, "[2]", 502);
        Resolver.set(numbers, "3", 503);
        assertArrayEquals(new int[]{1,501,502,503,5,6}, numbers);
        
    }
    
    @Test
    public void testMatcher() {
        Matcher hit = Pattern.compile("([a-z]+) ([a-z]+) ([a-z]+)").matcher("one two last");
        assertTrue(hit.find());
        assertEquals("two", Resolver.get(hit, "2"));
        
        try {
            Resolver.set(hit, "2", "text");
            fail("UnsupportedOperationException expected");
        } catch(UnsupportedOperationException cause) {
            assertTrue(cause.getMessage().contains("Matcher"));
        }
        
        try {
            Resolver.set(hit, "2[1]", 'R');
            fail("UnsupportedOperationException expected");
        } catch(UnsupportedOperationException cause) {
            assertTrue(cause.getMessage().contains("CharSequence"));
        }
    }
    
    @Test
    public void testCharSequence() {
        String a = "aeRto";
        AnsiString b = new AnsiString("uAdd");
        assertEquals('R', Resolver.get(a, "2"));
        assertEquals('A', Resolver.get(b, "1"));
        
        try {
            Resolver.set(a, "2", 'S');
            fail("UnsupportedOperationException expected");
        } catch(UnsupportedOperationException cause) {
            assertTrue(cause.getMessage().contains("CharSequence"));
        }
    }
    
    @Test
    public void testReferenceType() {
        A context = new A();
        int numbers[] = new int[]{1,2,3,4,5,6};
        
        assertEquals("int", Resolver.reference(numbers, "1").type().toString() );
        assertEquals("int", Resolver.reference(context, "x").type().toString() );
        
        assertEquals("B", Resolver.reference(context, "content").type().toString() );
        assertEquals("C", Resolver.reference(context, "array[2]").type().toString() );
        
        assertEquals("List<C>", Resolver.reference(context, "list").type().toString() );
        assertEquals("C", Resolver.reference(context, "list[0]").type().toString() );
        assertEquals("CN", Resolver.reference(context, "list[1]").type().toString() );
        
        assertEquals("Map<String, Number>", Resolver.reference(context, "list[1].numbers").type().toString() );
        assertEquals("Integer", Resolver.reference(context, "list[1].numbers#two").type().toString() );
        assertEquals("Long", Resolver.reference(context, "list[1].numbers#seven").type().toString() );
        assertEquals("Number", Resolver.reference(context, "list[1].numbers#unknown").type().toString() );
        
        assertEquals("Map<Integer, C>", Resolver.reference(context, "empty").type().toString() );
        assertEquals("C", Resolver.reference(context, "empty#22").type().toString() );
        assertEquals("String", Resolver.reference(context, "empty#22.name").type().toString() );
        
        assertEquals("Map<Integer, C>", Resolver.reference(context, "nullmap").type().toString() );
        assertEquals("C", Resolver.reference(context, "nullmap#22").type().toString() );
        assertEquals("String", Resolver.reference(context, "nullmap#22.name").type().toString() );
        
        assertEquals("char", Resolver.reference(context, "nullmap#22.name[88]").type().toString() );
        assertEquals("char", Resolver.reference(context, "list[1].name[0]").type().toString() );
        
        Matcher hit = Pattern.compile("([a-z]+) ([a-z]+) ([a-z]+)").matcher("one two last");
        assertTrue(hit.find());
        
        assertEquals("String", Resolver.reference(hit, "[1]").type().toString());
        assertEquals("char", Resolver.reference(hit, "[1][1]").type().toString());
        
        try {
            Resolver.reference(context, "list[1].numbers[77]").type();
            fail("IllegalArgumentException expected");
        } catch(IllegalArgumentException cause) {
            assertTrue( cause.getMessage().contains("Map<String, Number>") );
        }
    }
    
    @Test
    public void testReferencePut() {
        A context = new A();
        int numbers[] = new int[]{1,2,3,4,5,6};
        
        Resolver.reference(numbers, "[1]").put("44");
        assertEquals(44, numbers[1]);
        
        Resolver.reference(context, "x").put("55");
        assertEquals(55, context.x);
        
        Resolver.reference(context, "list[1].name").put("hello");
        assertEquals("hello", context.list.get(1).name);
        
        Resolver.reference(context, "list[1].name").put(99);
        assertEquals("99", context.list.get(1).name);
        
        Resolver.reference(context, "list[1].numbers#key").put(14);
        assertEquals(14, context.list.get(1).numbers.get("key"));
        
        
        assertEquals("Map<String, Number>", Resolver.reference(context, "list[1].numbers").type().toString());
        assertEquals("Number", Resolver.reference(context, "list[1].numbers#ikey").type().toString());

        Resolver.reference(context, "list[1].numbers#ikey").put(15L);
        assertEquals("Integer", Resolver.reference(context, "list[1].numbers#ikey").type().toString());
        assertEquals(15, context.list.get(1).numbers.get("ikey"));
        
        Resolver.reference(context, "list[1].numbers#long").put(0x8FFFF0000L);
        assertEquals("Long", Resolver.reference(context, "list[1].numbers#long").type().toString());
        assertEquals(0x8FFFF0000L, context.list.get(1).numbers.get("long"));
    }
    
    @Test
    public void testReferenceSetGet() {
        // we tested direct get & set methods so we know that internals are OK
        // here we just want to be sure, that "reference" delegates correctly
        int numbers[] = new int[]{1,2,3,4,5,6};
        
        Resolver.reference(numbers, "[1]").set(44);
        assertEquals(44, numbers[1]);
        assertEquals(44, Resolver.reference(numbers, "[1]").get());
    }
    
    @Test
    public void testExpression() {
        assertEquals("list[99].name", Resolver.compile("list[99].name").expression());
        
    }
    
    
    private static class A {
        public int x = 5;
        public int y = 8;
        public B content = new B();
        
        public C[] array = new C[]{new C("a"), new CN("b"), new C("c")};
        
        public List<C> list = Arrays.asList(new C("A"), new CN("B"), new C("C"));
        
        public D home = new D();
        
        public Map<Integer, C> empty = new TreeMap<>();
        
        public Map<Integer, C> nullmap;
    }
    
    private static class B {
        public E item = new E(32,16,1000);
    }
    
    private static class C {
        public String name;
        public Map<String, Number> numbers = new HashMap<>();
        
        public C(String name) {
            this.name = name;
            this.numbers.put("zero", 0);
            this.numbers.put("two", 2);
            this.numbers.put("seven", 7L);
        }
        
    }
    
    private static class CN extends C {

        public CN(String name) {
            super(name);
        }
        
    }
    
    private static class D {
        public int[] values = new int[]{4,5,6,7};
    }
    
    private static class E {
        public final int width;
        public int height;
        private int hellovalue;
        
        public E(int width, int height, int hello) {
            this.width = width;
            this.height = height;
            this.hellovalue = hello;
        }

        public int getHello() {
            return hellovalue;
        }

        public void setHello(int v) {
            hellovalue = v;
        }
    }
    
}
