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

import java.io.IOException;
import java.util.*;
import net.ranides.assira.collection.SetUtils;
import net.ranides.assira.generic.Function;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.io.FileFilter.AcceptMode;
import net.ranides.assira.math.HashHelper;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.reflection.TypeFactory;
import net.ranides.assira.reflection.util.ClassUtils;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author ranides
 */
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public class LexicalCastTest {

    public LexicalCastTest() {
    }


    private static Map<Integer, Integer> DATA_MAP = new TreeMap<Integer, Integer>() {{
        put(1, 1);
        put(2, 1);
        put(3, 2);
        put(4, 3);
        put(5, 5);
        put(6, 8);
        put(7, 13);
    }};

    List<Integer> dataList = Arrays.asList(1, 3, 5, 2, 4);

    Set<Integer> dataSet = SetUtils.asTreeSet(1, 3, 5, 2, 4);

    @Test
    public void testAsList_Iterable() { // NOPMD
        assertEquals(
            Arrays.asList("1", "3", "5", "2", "4"),
            LexicalCast.asList(dataList)
        );

        assertEquals(
            Arrays.asList("1","2","3","4","5"),
            LexicalCast.asList(dataSet)
        );
    }

    @Test
    public void testAsList_Iterable_Function() { // NOPMD
        final String normal[] = new String[]{ "?","A","B","C","D","E" };

        final String random[] = new String[]{ "?","D","C","E","A","B" };

        assertEquals(
            Arrays.asList("A", "C", "E", "B", "D"),
            LexicalCast.asList(dataList, new Function<String, Integer>() {
                @Override
                public String apply(Integer source) { return normal[source]; }
            })
        );

        assertEquals(
            Arrays.asList("D", "C", "E", "A", "B"),
            LexicalCast.asList(dataSet, new Function<String, Integer>() {
                @Override
                public String apply(Integer source) { return random[source]; }
            })
        );
    }

    @Test
    public void testAsList_Map() { // NOPMD
        List<String> list = LexicalCast.asList(DATA_MAP);
        assertEquals(
            "1=1 | 2=1 | 3=2 | 4=3 | 5=5 | 6=8 | 7=13",
            StringUtils.join(list, " | ")
        );
    }

    @Test
    public void testAsList_Map_Function() { // NOPMD
        List<String> list = LexicalCast.asList(DATA_MAP, new Function<String, Map.Entry<Integer, Integer>>() {
            @Override
            public String apply(Map.Entry<Integer, Integer> source) {
                return source.getKey() + " : " + source.getValue();
            }
        });
        assertEquals(
            "1 : 1 | 2 : 1 | 3 : 2 | 4 : 3 | 5 : 5 | 6 : 8 | 7 : 13",
            StringUtils.join(list, " | ")
        );
    }

    @Test
    public void testAsText_Map() { // NOPMD
        assertEquals(
            "1=1\n2=1\n3=2\n4=3\n5=5\n6=8\n7=13",
            LexicalCast.asText(DATA_MAP)
        );
    }

    @Test
    public void testAsText_Collection() { // NOPMD
        assertEquals(
            " - 1\n - 3\n - 5\n - 2\n - 4",
            LexicalCast.asText(dataList)
        );
    }

    @Test
    public void testApply() {
        assertEquals("", LexicalCast.asText(null));
        assertEquals("\"\"", LexicalCast.asText(""));
        assertEquals("\"text\"", LexicalCast.asText("text"));
        assertEquals("\"z\"", LexicalCast.asText('z'));
        assertEquals("17", LexicalCast.asText(17));
        assertEquals("true", LexicalCast.asText(true));
        
        Date date = new GregorianCalendar(1996, GregorianCalendar.APRIL, 20, 21, 30, 15).getTime();
        assertEquals("1996-04-20 21:30:15", LexicalCast.asText(date));
        
        assertEquals(" - 1\n - 2\n - 3", LexicalCast.asText(Arrays.asList(1,2,3)));
        
        assertEquals(
            "1=1\n2=1\n3=2\n4=3\n5=5\n6=8\n7=13",
            LexicalCast.asText(DATA_MAP)
        );
        
        assertEquals("zero", LexicalCast.asText(new Lexicable() {
            @Override
            public String toLexicalString() {
                return "zero";
            }
        }));
        
        assertEquals(" - 1\n - 2\n - 3", LexicalCast.asText(new int[]{1,2,3}));
    }
    
    @Test
    public void testJacksonErrors() throws IOException {
        try {
            LexicalCast.toJSON(new Object(){});
            fail("LexicalCastException expected");
        } catch(LexicalCastException cause) {
            assertTrue(true);
        }
        try {
            LexicalCast.fromJSON("Z", Integer.class);
            fail("LexicalCastException expected");
        } catch(LexicalCastException cause) {
            assertTrue(true);
        }
        try {
            LexicalCast.fromJSON("Z", new TypeToken<Integer>() {});
            fail("LexicalCastException expected");
        } catch(LexicalCastException cause) {
            assertTrue(true);
        }
    }
    
    @Test
    public void testJackson() throws IOException {

        Integer value = LexicalCast.fromJSON("23", Integer.class);
        assertEquals( (Integer)23, value);

        int value2 = LexicalCast.fromJSON("23", Integer.class);
        assertEquals( 23, value2);

        int value3 = LexicalCast.fromJSON("23", int.class);
        assertEquals( 23, value3);

        BeanRecord record = new BeanRecord();
        record.setHeight(31);
        record.setWidth(8);
        record.setName("window");
        record.setTime( new GregorianCalendar(1996, GregorianCalendar.APRIL, 20, 21, 30, 15).getTime() );
        record.setItems( Arrays.asList( (Double)1.2, (Integer)77, (Object)"a") );

        ComplexList clist = new ComplexList(Arrays.asList(
            new TPair("a", "44"),
            new TPair("c", "77"),
            new TPair("b", "89")
        ));

        // to nam sprawdza, czy po prostu formatuje
        assertEquals( 7, LexicalCast.toJSON(record).split("\n").length );

        // to nam sprawdza poprawność serializacji
        assertEquals( record, LexicalCast.fromJSON(LexicalCast.toJSON(record), BeanRecord.class) );

        assertEquals( clist, LexicalCast.fromJSON(LexicalCast.toJSON(clist), ComplexList.class) );
    }

    @Test
    public void testJacksonLists() {
        LinkedList<String> source = new LinkedList<>(Arrays.asList("1","2","3"));
        List<Long> target = LexicalCast.cast(source, new TypeToken<List<Long>>() {});

        assertEquals(ArrayList.class, target.getClass());
        assertEquals(Arrays.asList(1L, 2L, 3L), target);

        GenericClass type = TypeFactory.construct(List.class, TypeFactory.construct(Long.class));
        List<Long> target2 = LexicalCast.cast(source, type);
        assertEquals(ArrayList.class, target2.getClass());
        assertEquals(Arrays.asList(1L, 2L, 3L), target2);
    }
    
    @Test
    public void testPrimitiveCast() {
        assertNull(LexicalCast.cast(null, int.class));

        try {
            LexicalCast.cast(new Object(){}, int.class);
            fail("ClassCastException expected");
        } catch(ClassCastException cause) {
            assertTrue(true);
        }
        
        assertEquals( (int)7, (int)LexicalCast.cast(7, int.class) );
        assertEquals( (Integer)7, LexicalCast.cast(7, Integer.class) );
        
        List<String> value = new ArrayList<>();
        value.add("a");
        value.add("b");
        value.add("c");
        assertNotSame(value, LexicalCast.cast(value, value.getClass()) );
        assertEquals(value, LexicalCast.cast(value, value.getClass()) );
        
        assertEquals("true", LexicalCast.cast(true, String.class) );
        
        assertCast(35_841_147_034_490_000L, long.class, "35841147034490000");
        
        assertCast(1_800_500_250, int.class, "1800500250");
        
        assertCast((short)16500, short.class, "16500");
        
        assertCast((byte)47, byte.class, "47");
        assertCast((byte)227, byte.class, "227");
        assertCast((byte)-29, byte.class, "227");
        assertCast((byte)227, byte.class, "-29");
        assertCast((byte)-29, byte.class, "-29");
        
        // 0.75 should be represented exactly
        assertCast(0.75f, float.class, "0.75");
        assertCast(0.75f, float.class, "7.5e-1");
        
        assertCast(0.75, double.class, "0.75");
        assertCast(0.75, double.class, "7.5e-1");
        
        assertCast(true, boolean.class, "true");
        
        assertCast(1024L, long.class, 1024);
        
        assertCast(1024, int.class, 1024L);
        
        assertCast((short)1024, short.class, 1024L);
        
        assertCast((byte)32, byte.class, 32L);
        
        // 0.75 should be represented exactly
        assertCast(0.75f, float.class, 0.75);
        
        assertCast(0.75, double.class, 0.75f);
        
        assertCast("1024", String.class, 1024);
        
    }
    
    @Test
    public void testEnumSet() {
        Set<AcceptMode> input = EnumSet.of(AcceptMode.ANY_FILE, AcceptMode.RECURSIVE, AcceptMode.FILE);

        String text = LexicalCast.cast(input, String.class);

        Set<AcceptMode> output = LexicalCast.cast(text, new TypeToken<EnumSet<AcceptMode>>(){} );

        assertEquals(input, output);
    }

    private static void assertCast(Object expValue, Class<?> rtype, Object input) {
        Class<?> btype = ClassUtils.box(rtype);
        assertCast(btype, expValue, btype, input);
        assertCast(btype, expValue, rtype, input);
    }
    
    private static void assertCast(Class<?> expType, Object expValue, Class<?> type, Object input) {
        Object output = LexicalCast.cast(input, type);
        assertEquals(expValue, output);
        assertEquals(expType, output.getClass());
    }

    private static class BeanRecord {

        protected int width;
        protected int height;
        protected String name;
        protected List<?> items;
        protected Date time;

        public BeanRecord() {
            /* do nothing */
        }

        public int getHeight() {
            return height;
        }

        public List<?> getItems() {
            return items;
        }

        public String getName() {
            return name;
        }

        public Date getTime() {
            return new Date(time.getTime());
        }

        public int getWidth() {
            return width;
        }

        public void setHeight(int height) {
            this.height = height;
        }

        public void setItems(List<?> items) {
            this.items = items;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void setTime(Date time) {
            this.time = new Date(time.getTime());
        }

        public void setWidth(int width) {
            this.width = width;
        }

        @Override
        public boolean equals(Object object) {
            if (object == null || getClass() != object.getClass()) {
                return false;
            }
            final BeanRecord other = (BeanRecord) object;
            return
                ValueUtils.equals(this.width,  other.width) &&
                ValueUtils.equals(this.height, other.height) &&
                ValueUtils.equals(this.name,   other.name) &&
                ValueUtils.equals(this.items,  other.items) &&
                ValueUtils.equals(this.time,   other.time);
        }

        @Override
        public int hashCode() {
            return HashHelper.hashValues(3, 67, width, height, name, time);
        }



    }

    private static class TPair {
        private String key;
        private String value;

        public TPair() {
            /* do nothing */
        }

        public TPair(String key, String value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        @Override
        public boolean equals(Object object) {
            if (object == null || getClass() != object.getClass()) {
                return false;
            }
            final TPair other = (TPair) object;
            return ValueUtils.equals(other.key, this.key) && ValueUtils.equals(other.value, this.value);
        }

        @Override
        public int hashCode() {
            return HashHelper.hashValues(7, 97, key, value);
        }

        @Override
        public String toString() {
            return "Pair{" + "key=" + key + ", value=" + value + '}';
        }

    }

    private static class ComplexList {

        private List<TPair> list;

        public ComplexList() {
            /* do nothing */
        }

        public ComplexList(List<TPair> list) {
            this.list = list;
        }

        public List<TPair> getList() {
            return list;
        }

        public void setList(List<TPair> list) {
            this.list = list;
        }

        @Override
        public boolean equals(Object object) {
            if (object == null || getClass() != object.getClass()) {
                return false;
            }
            final ComplexList other = (ComplexList) object;
            return ValueUtils.equals(this.list, other.list);
        }

        @Override
        public int hashCode() {
            return (this.list != null ? this.list.hashCode() : 47);
        }

    }

}
