/*
 * Copyright 2013 headius.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.headius.invokebinder;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author headius
 */
public class SmartBinderTest {
    
    public SmartBinderTest() {
    }
    
    @BeforeClass
    public static void setUpClass() {
    }
    
    @AfterClass
    public static void tearDownClass() {
    }
    
    @Before
    public void setUp() {
    }
    
    @After
    public void tearDown() {
    }

    /**
     * Test of from method, of class SmartBinder.
     */
    @Test
    public void testFrom_Signature() throws Throwable {
        SmartHandle handle = SmartBinder
                .from(Signature.returning(String.class).appendArg("int", int.class))
                .invoke(stringInt);
        
        assertEquals("42", (String)handle.handle().invokeExact(42));
    }

    @Test
    public void testDrop() throws Throwable {
        Signature oldSig = Subjects.StringIntegerIntegerIntegerString;

        SmartHandle handle = SmartBinder.from(oldSig)
                .drop("b1")
                .drop("b2")
                .drop("b3")
                .insert(1, "bs", new Integer[]{1, 2, 3})
                .invoke(Subjects.StringIntegersStringHandle);

        assertEquals("[foo, [1, 2, 3], bar]", (String)handle.handle().invokeExact("foo", new Integer(5), new Integer(6), new Integer(7), "bar"));
    }

    @Test
    public void testDropLast() throws Throwable {
        Signature oldSig = Subjects.StringIntegerIntegerIntegerString;

        SmartHandle handle = SmartBinder.from(oldSig)
                .dropLast(4)
                .append("bs", new Integer[]{1, 2, 3})
                .append("c", "baz")
                .invoke(Subjects.StringIntegersStringHandle);

        assertEquals("[foo, [1, 2, 3], baz]", (String)handle.handle().invokeExact("foo", new Integer(5), new Integer(6), new Integer(7), "bar"));
    }

    @Test
    public void testDropFirst() throws Throwable {
        Signature oldSig = Subjects.StringIntegerIntegerIntegerString;

        SmartHandle handle = SmartBinder.from(oldSig)
                .dropFirst(4)
                .prepend("bs", new Integer[]{1, 2, 3})
                .prepend("a", "baz")
                .invoke(Subjects.StringIntegersStringHandle);

        assertEquals("[baz, [1, 2, 3], bar]", (String)handle.handle().invokeExact("foo", new Integer(5), new Integer(6), new Integer(7), "bar"));
    }

    @Test
    public void testCollect() throws Throwable {
        Signature oldSig = Subjects.StringIntegerIntegerIntegerString;

        SmartHandle handle = SmartBinder
                .from(oldSig)
                .collect("bs", "b.*")
                .invoke(Subjects.StringIntegersStringHandle);

        assertEquals("[foo, [1, 2, 3], bar]", (String)handle.handle().invokeExact("foo", new Integer(1), new Integer(2), new Integer(3), "bar"));
    }

    @Test
    public void testInvokeStatic() throws Throwable {
        SmartHandle handle = SmartBinder
                .from(Subjects.StringIntegersString)
                .invokeStatic(LOOKUP, Subjects.class, "stringIntegersString");

        assertEquals(Subjects.StringIntegersString, handle.signature());
        assertEquals(Subjects.StringIntegersString.type(), handle.handle().type());
        assertEquals("[foo, [1, 2, 3], bar]", (String)handle.handle().invokeExact("foo", new Integer[]{1,2,3}, "bar"));

        handle = SmartBinder
                .from(Subjects.StringIntegersString)
                .invokeStatic(LOOKUP, Subjects.class, "stringIntegersString");

        assertEquals(Subjects.StringIntegersString, handle.signature());
        assertEquals(Subjects.StringIntegersString.type(), handle.handle().type());
        assertEquals("[foo, [1, 2, 3], bar]", (String)handle.handle().invokeExact("foo", new Integer[]{1,2,3}, "bar"));
    }

    @Test
    public void testInvokeVirtual() throws Throwable {
        Subjects subjects = new Subjects();
        Signature thisSig = Subjects.StringIntegersString.prependArg("this", Subjects.class);

        SmartHandle handle = SmartBinder
                .from(thisSig)
                .invokeVirtual(LOOKUP, "stringIntegersString2");

        assertEquals(thisSig, handle.signature());
        assertEquals(thisSig.type(), handle.handle().type());
        assertEquals("[foo, [1, 2, 3], bar]", (String)handle.handle().invokeExact(subjects, "foo", new Integer[]{1,2,3}, "bar"));

        handle = SmartBinder
                .from(thisSig)
                .invokeVirtual(LOOKUP, "stringIntegersString2");

        assertEquals(thisSig, handle.signature());
        assertEquals(thisSig.type(), handle.handle().type());
        assertEquals("[foo, [1, 2, 3], bar]", (String)handle.handle().invokeExact(subjects, "foo", new Integer[]{1,2,3}, "bar"));
    }

    @Test
    public void testInsert() throws Throwable {
        MethodHandle target = Subjects.concatHandle();
        SmartHandle handle = SmartBinder
                .from(String.class, "arg0", String.class)
                .insert(1, "arg1", "world")
                .invoke(target);

        assertEquals(MethodType.methodType(String.class, String.class), handle.signature().type());
        assertEquals("Hello, world", (String) handle.handle().invokeExact("Hello, "));

        MethodHandle target2 = Subjects.concatCharSequenceHandle();
        SmartHandle handle2 = SmartBinder
                .from(String.class, "arg0", String.class)
                .insert(1, "arg1", CharSequence.class, "world")
                .invoke(target2);

        assertEquals(MethodType.methodType(String.class, String.class), handle2.signature().type());
        assertEquals("Hello, world", (String) handle2.handle().invokeExact("Hello, "));
    }

    @Test
    public void testAppend() throws Throwable {
        MethodHandle target = Subjects.concatHandle();
        SmartHandle handle = SmartBinder
                .from(String.class, new String[] {"arg0", "arg1"}, String.class, Object.class)
                .append("arg2", "world")
                .drop("arg1")
                .invoke(target);

        assertEquals(MethodType.methodType(String.class, String.class, Object.class), handle.signature().type());
        assertEquals("Hello, world", (String) handle.handle().invokeExact("Hello, ", new Object()));

        MethodHandle target2 = Subjects.concatCharSequenceHandle();
        SmartHandle handle2 = SmartBinder
                .from(String.class, new String[] {"arg0", "arg1"}, String.class, Object.class)
                .append("arg2", CharSequence.class, "world")
                .drop("arg1")
                .invoke(target2);

        assertEquals(MethodType.methodType(String.class, String.class, Object.class), handle2.signature().type());
        assertEquals("Hello, world", (String) handle2.handle().invokeExact("Hello, ", new Object()));
    }

    @Test
    public void testPrepend() throws Throwable {
        MethodHandle target = Subjects.concatHandle();
        SmartHandle handle = SmartBinder
                .from(String.class, new String[]{"arg1", "arg2"}, Object.class, String.class)
                .prepend("arg0", "Hello, ")
                .drop("arg1")
                .invoke(target);

        assertEquals(MethodType.methodType(String.class, Object.class, String.class), handle.signature().type());
        assertEquals("Hello, world", (String) handle.handle().invokeExact(new Object(), "world"));

        MethodHandle target2 = Subjects.concatHandle();
        SmartHandle handle2 = SmartBinder
                .from(String.class, new String[] {"arg1", "arg2"}, Object.class, String.class)
                .prepend("arg0", String.class, "Hello, ")
                .drop("arg1")
                .invoke(target2);

        assertEquals(MethodType.methodType(String.class, Object.class, String.class), handle2.signature().type());
        assertEquals("Hello, world", (String) handle2.handle().invokeExact(new Object(), "world"));
    }

    @Test
    public void testCast() throws Throwable {
        MethodHandle target = LOOKUP.unreflect(String.class.getMethod("split", String.class));

        // cast for virtual call using full static signature
        SmartHandle handle  = SmartBinder
                .from(Object.class, new String[]{"this", "regex"}, Object.class, Object.class)
                .cast(String[].class, String.class, String.class)
                .invoke(target);

        assertArrayEquals(new String[]{"foo", "bar"}, (String[])(Object)handle.handle().invokeExact((Object)"foo,bar", (Object)","));

        // cast for virtual call using ret, this, args
        handle  = SmartBinder
                .from(new Signature(Object.class, Object.class, new Class[]{Object.class}, "this", "regex"))
                .castVirtual(String[].class, String.class, new Class[]{String.class})
                .invoke(target);

        assertArrayEquals(new String[]{"foo", "bar"}, (String[])(Object)handle.handle().invokeExact((Object)"foo,bar", (Object)","));
    }

    @Test
    public void testFilter() throws Throwable {
        MethodHandle target = Subjects.concatHandle();
        MethodHandle filter = MethodHandles.insertArguments(Subjects.concatHandle(), 1, "goodbye");
        MethodHandle handle = SmartBinder
                .from(String.class, new String[]{"arg1", "arg2"}, String.class, String.class)
                .filter("arg.*", filter)
                .invoke(target).handle();

        assertEquals(MethodType.methodType(String.class, String.class, String.class), handle.type());
        assertEquals("foogoodbyebargoodbye", (String)handle.invokeExact("foo", "bar"));
    }

    @Test   
    public void testIdentity() throws Throwable {
        MethodHandle handle = SmartBinder
                .from(String.class, "i", int.class)
                .fold("s", stringInt)
                .dropLast()
                .identity()
                .handle();

        assertEquals(MethodType.methodType(String.class, int.class), handle.type());
        assertEquals("15", (String)handle.invokeExact(15));
    }


    @Test
    public void testFoldStatic() throws Throwable {
        MethodHandle handle = SmartBinder
                .from(Subjects.StringIntegerIntegerInteger)
                .foldStatic("x", Subjects.class, "foldStringIntegerIntegerInteger")
                .drop("a")
                .drop("b1")
                .drop("b2")
                .drop("b3")
                .invokeStatic(MethodHandles.lookup(), Subjects.class, "upperCase")
                .handle();

        assertEquals("FORTY_TWO", (String)handle.invokeExact("foo",
                Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)));
    }

    @Test
    public void testFoldVirtual() throws Throwable {
        MethodHandle handle = SmartBinder
                .from(Subjects.StringIntegerIntegerInteger)
                .prepend("this", new Subjects())
                .foldVirtual("x", "foldVirtualStringIntegerIntegerInteger")
                .drop("this")
                .drop("a")
                .drop("b1")
                .drop("b2")
                .drop("b3")
                .invokeStatic(MethodHandles.lookup(), Subjects.class, "upperCase")
                .handle();

        assertEquals("FORTY_TWO", (String)handle.invokeExact("foo",
                Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)));
    }

    @Test
    public void testFoldVirtualWithLookup() throws Throwable {
        MethodHandle handle = SmartBinder
                .from(Subjects.StringIntegerIntegerInteger)
                .prepend("this", new Subjects())
                .foldVirtual("x", MethodHandles.lookup(), "foldVirtualStringIntegerIntegerInteger")
                .drop("this")
                .drop("a")
                .drop("b1")
                .drop("b2")
                .drop("b3")
                .invokeStatic(MethodHandles.lookup(), Subjects.class, "upperCase")
                .handle();

        assertEquals("FORTY_TWO", (String)handle.invokeExact("foo",
                Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)));
    }

    @Test
    public void testPrintSignature() throws Throwable {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(baos);
        MethodHandle handle = SmartBinder
                .from(Subjects.StringIntegerIntegerInteger)
                .foldStatic("x", Subjects.class, "foldStringIntegerIntegerInteger")
                .drop("a").printSignature(ps)
                .drop("b1")
                .drop("b2")
                .drop("b3").printSignature(ps)
                .invokeStatic(MethodHandles.lookup(), Subjects.class, "upperCase")
                .handle();

        String result = baos.toString();

        assertEquals(
                "(String x, Integer b1, Integer b2, Integer b3)String\n" +
                "(String x)String\n",
                result);
    }


    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
    
    private static final MethodHandle stringInt = Binder
            .from(String.class, int.class)
            .invokeStaticQuiet(LOOKUP, Integer.class, "toString");

}