package net.ranides.assira.functional;

import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;

import net.ranides.assira.functional.Compositor.CompositorBuilder;
import net.ranides.assira.functional.Compositor.CompositorDecorator;
import net.ranides.assira.functional.Compositor.CompositorScope;
import net.ranides.assira.functional.Consumers.Consumer0;
import net.ranides.assira.functional.Consumers.Consumer1;
import net.ranides.assira.functional.Consumers.Consumer2;
import net.ranides.assira.functional.Consumers.Consumer3;
import net.ranides.assira.functional.Consumers.Consumer4;
import net.ranides.assira.functional.Functions.*;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.junit.NewAssert;
import net.ranides.assira.reflection.*;

import org.junit.Test;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.junit.Assert.*;

public class CompositorTest {

    //region Helper methods used in examples and tests
    private static int parseNumber(String value) {
        return Integer.parseInt(value);
    }

    private static Ref<Integer> findNumberOptional(String text) {
        return Ref.ofNullable(findNumberAt(text, 0));
    }

    private static Integer findNumber(String text) {
        return findNumberAt(text, 0);
    }

    private static Integer findNumberAt(String text, int position) {
        Matcher hit = Pattern.compile("[0-9]+").matcher(text);
        for(int i=0; i<=position; i++) {
            if(!hit.find()) {
                return null;
            }
        }
        return Integer.parseInt(hit.group());
    }

    public static int countChars(String text, char c) {
        return (int)text.codePoints().filter(ch -> ch == c).count();
    }
    //endregion

    @Test
    public void exampleUse() {
        // without type hints
        Function4<String, String, String, String,Integer> func1 = Compositor
            .compose()
            // method reference
            .withParam(CompositorTest::findNumber)
            // lambda expression, with specified type
            .withParam((String text) -> findNumberAt(text,2))
            // ambiguous method reference
            .withParam((Function1<String, Integer>) Integer::parseInt)
            // method reference
            .withParam(CompositorTest::parseNumber)
            // argument types in lambda expression will be detected
            .bind((a, b, c, d) -> 1000*a + 100*b + 10*c + d)
            // create standard function
            .function();

        // with type hints
        Function4<String, String, String, String,Integer> func2 = Compositor
            // we specified type bounds for input arguments and result type
            .<String, Integer>compose()
            // no change here
            .withParam(CompositorTest::findNumber)
            // so, we don't have to declare argument type
            .withParam(text -> findNumberAt(text,2))
            // so, this reference is not ambiguous anymore
            .withParam(Integer::parseInt)
            // no change here
            .withParam(CompositorTest::parseNumber)
            // no change here
            .bind((a, b, c, d) -> 1000*a + 100*b + 10*c + d)
            // create standard function
            .function();

        // correct arguments
        assertEquals(1375, func1.apply("x1x", "1x7x3x4x5", "7", "5").intValue());
        assertEquals(1375, func2.apply("x1x", "1x7x3x4x5", "7", "5").intValue());

        // null pointer will be thrown
        try {
            assertEquals(0, func1.apply("xxx", "1x7x3x4x5", "7", "5").intValue());
            fail("NullPointerException expected");
        } catch (NullPointerException ex) {
            assertTrue(true);
        }

        // null pointer will be thrown
        try {
            assertEquals(0, func2.apply("xxx", "1x7xxx", "7", "5").intValue());
            fail("NullPointerException expected");
        } catch (NullPointerException ex) {
            assertTrue(true);
        }
    }

    @Test
    public void exampleUseDefaults() {
        // nullable
        Function4<String, String, String, String,Integer> func1 = Compositor
            .<String, Integer>compose()
            .withNullable(CompositorTest::findNumber, 2)
            .withNullable(CompositorTest::findNumber, 4)
            .withNullable(CompositorTest::findNumber, 6)
            .withNullable(CompositorTest::findNumber, 8)
            .bind((a, b, c, d) -> 1000*a + 100*b + 10*c + d)
            .function();

        // optionals
        Function4<String, String, String, String,Integer> func2 = Compositor
            .<String, Integer>compose()
            .withOptional(CompositorTest::findNumberOptional, 2)
            .withOptional(CompositorTest::findNumberOptional, 4)
            .withOptional(CompositorTest::findNumberOptional, 6)
            .withOptional(CompositorTest::findNumberOptional, 8)
            .bind((a, b, c, d) -> 1000*a + 100*b + 10*c + d)
            .function();

        // correct arguments
        assertEquals(1375, func1.apply("x1x", "3other7text", "here7", "5").intValue());
        assertEquals(1375, func2.apply("x1x", "3other7text", "here7", "5").intValue());

        // default values will be used
        assertEquals(2365, func1.apply("x", "3other7text", "here", "5").intValue());
        assertEquals(2365, func2.apply("x", "3other7text", "here", "5").intValue());
    }

    @Test
    public void exampleUseTypes() {
        // mixed input types, but all generate int
        Function3<String, AtomicInteger, Float, Integer> func1 = Compositor
            .compose()
            .withParam(CompositorTest::parseNumber)
            .withParam(AtomicInteger::intValue)
            .withParam(Float::intValue)
            .bind((a, b, c) -> a * b + c)
            .function();

        // mixed input types, but all generate int
        Function3<String, AtomicInteger, Float, Integer> func2 = Compositor
            .compose()
            .withParam((String input) -> 2 * Integer.parseInt(input))
            .withParam((AtomicInteger value) -> 2 * value.intValue())
            .withParam((Float value) -> 2 * value.intValue())
            .bind((a, b, c) -> a * b + c)
            .function();

        // both arguments and return values have different types
        Function3<int[], AtomicInteger, Float, String> func3 = Compositor
            .compose()
            .withParam((int[] input) -> 2 * input[1])
            .withParam((AtomicInteger value) -> new int[]{value.intValue()})
            .withParam((Float value) -> 4.1 * value)
            // here, we receive: (Integer, int[], Double)
            .bind((a, b, c) -> String.format(Locale.ROOT, "%d %d %.1f", a, b[0], c))
            .function();

        assertEquals(35, func1.apply("4", new AtomicInteger(5), 15.2f).intValue());
        assertEquals(110, func2.apply("4", new AtomicInteger(5), 15.2f).intValue());

        assertEquals("14 5 62.7", func3.apply(new int[]{4,7}, new AtomicInteger(5), 15.3f));
    }

    @Test
    public void exampleUseCompact() {
        // this is ordinary function
        Function3<String, String, String, Integer> func1 = Compositor
            .compose()
            .withParam((String v) -> Integer.parseInt(v.split(":")[0]))
            .withParam((String v) -> Integer.parseInt(v.split(":")[1]))
            .withParam((String v) -> Integer.parseInt(v.split(":")[2]))
            .bind((a, b, c) -> 1000*a + 100 * b + 10 * c)
            .function();

        // this is compacted function
        Function<Object, Integer> func2 = Compositor
            .compose()
            .withParam((String v) -> Integer.parseInt(v.split(":")[0]))
            .withParam((String v) -> Integer.parseInt(v.split(":")[1]))
            .withParam((String v) -> Integer.parseInt(v.split(":")[2]))
            .bind((a, b, c) -> 1000*a + 100 * b + 10 * c)
            .compact();

        // this is compacted function with type hints
        Function<String, Integer> func3 = Compositor
            .<String, Integer>compose()
            .withParam(v -> Integer.parseInt(v.split(":")[0]))
            .withParam(v -> Integer.parseInt(v.split(":")[1]))
            .withParam(v -> Integer.parseInt(v.split(":")[2]))
            // this line won't even compile!
//            .bind((a, b, c, d) -> 1000*a + 100 * b + 10 * c + d)
            .bind((a, b, c) -> 1000*a + 100 * b + 10 * c)
            .compact();

        assertEquals(1230, func1.apply("1:2:3", "1:2:3", "1:2:3").intValue());
        assertEquals(1230, func2.apply("1:2:3").intValue());
        assertEquals(1230, func3.apply("1:2:3").intValue());
    }

    @Test
    public void testDecorator() {
        // this functionality is helpful only for developers writing their own chaining components

        CompositorDecorator<Void,Object,Object> decorator1 = new CompositorDecorator<Void,Object,Object>() {
            @Override
            public Function3<Object, Object, Object, Object> after(Function3<Object, Object, Object, Object> action) {
                // return complete new implementation
                return (a,b,c) -> (Integer)a * (Integer)b + (Integer)c;
            }
        };
        CompositorDecorator<Void,Object,Integer> decorator2 = new CompositorDecorator<Void,Object,Integer>() {
            @Override
            public Function3<Object, Object, Object, Integer> after(Function3<Object, Object, Object, Integer> action) {
                // use already bounded implementation, modify arguments and result
                return (a,b,c) -> {
                    int newC = (Integer)c + 7;
                    return 2 * action.apply(a,b,newC) + 1;
                };
            }
        };

        Function3<String, AtomicInteger, Float, Object> func1 = Compositor
            .compose(decorator1)
            .withParam(CompositorTest::parseNumber)
            .withParam(AtomicInteger::intValue)
            .withParam(Float::intValue)
            // we do not even try to bind any code, decorator will ignore it
            .function();

        Function3<String, AtomicInteger, Float, Integer> func2 = Compositor
            .compose(decorator2)
            .withParam(CompositorTest::parseNumber)
            .withParam(AtomicInteger::intValue)
            .withParam(Float::intValue)
            .bind((a, b, c) -> a * b + c)
            .function();

        assertEquals(35, func1.apply("4", new AtomicInteger(5), 15.2f));
        assertEquals(85, func2.apply("4", new AtomicInteger(5), 15.2f).intValue());
    }

    @Test
    public void testCompact() {
        // this functionality is helpful only for developers writing their own chaining components

        Function<Object, Integer> func2 = Compositor
            .compose()
            .withParam((String v) -> 10 * Integer.parseInt(v.split(":")[0]))
            .withParam((String v) -> 100 * Integer.parseInt(v.split(":")[1]))
            .withParam((String v) -> 1000 * Integer.parseInt(v.split(":")[2]))
            .bind((a, b, c) -> 2 * a + 2 * b + 2 * c)
            .compact();

        Function<Object, Integer> func3 = Compositor
            .compose()
            .withParam((String v) -> 10 * Integer.parseInt(v.split(":")[0]))
            .withParam((Float v) -> v.intValue())
            .withParam(v -> v)
            .bind((a, b, c) -> 2 * a + 2 * b)
            .compact();

        Function<CharSequence, Integer> func4 = Compositor
            .<CharSequence, Object>compose()
            .withParam((String v) -> 10 * Integer.parseInt(v.split(":")[0]))
            .withParam(( v) -> 100 * Integer.parseInt(v.toString().split(":")[1]))
            .bind((a, b) -> 2 * a + 2 * b)
            .compact();

        Function<CharSequence, Integer> func5 = Compositor
            .<CharSequence, Object>compose()
            .withParam((String v) -> 10 * Integer.parseInt(v.split(":")[0]))
            .withParam((v) -> 100 * Integer.parseInt(v.toString().split(":")[1]))
            // curiosity: we push constant value into compacted function as 3rd argument
            .withValue(() -> 7000)
            .bind((a, b, c) -> 2 * a + 2 * b + c)
            .compact();

        assertEquals(6420, func2.apply("1:2:3").intValue());

        NewAssert.assertThrows(ClassCastException.class, () -> {
            assertEquals(6420, func3.apply("1:2:3").intValue());
        });

        assertEquals(420, func4.apply("1:2:3").intValue());
        assertEquals(7420, func5.apply("1:2:3").intValue());
    }

    @Test
    public void exampleUseChaining() {

        Component component = new Component();

        // Component class integrates with Compositor in a number of ways.
        // This example shows how to store created functions inside Component
        component
            // create rule - enter into Compositor context
            .rule()
            // ordinary Compositor calls
            .withParam((String v) -> 10 * Integer.parseInt(v.split(":")[0]))
            // line below won't compile because Component returned Compositor with input restrictions
//                .with((Number v) -> 10 * v.intValue())
            .withParam((String v) -> 100 * Integer.parseInt(v.split(":")[1]))
            .withParam((String v) -> 1000 * Integer.parseInt(v.split(":")[2]))
            .bind((a, b, c) -> 2 * a + 2 * b + 2 * c)
            // but we use #build here, which internally sends composed function into component
            // returned component reference can be use for method chaining
            .build()
            // ordinary Component methods
            .hello("something")
            .hello("foo")
            // create another rule
            .rule()
            .withParam(text -> Integer.parseInt(text.split("-")[1]))
            .withParam(text -> {
                Matcher hit = Pattern.compile("<([0-9]+)>").matcher(text);
                return hit.find() ? hit : null;
            })
            .withParam(text -> text.split(" +") )
            .bind((code, number, words) -> 10000*code + 100*Integer.parseInt(number.group(1)) + words.length )
            .build()
            .hello("bar")
            // this is second method declared in component:
            // it creates functions too, but component stores them in different field
            // and there are different restrictions on parameters
            .atomic("key")
            .withParam(v -> new AtomicInteger(countChars(v, 'a')))
            .withParam(v -> new AtomicInteger(countChars(v, 'b')))
            .withParam(v -> new AtomicBoolean(v.contains("c")))
            .withParam(v -> new AtomicDouble(1.0 * v.length()))
            .bind((a,b,c,d) -> new StringBuilder().append(a).append(b).append(c).append(d))
            .build();

        // now delegate work to component, which remembers created rules
        assertEquals(6420, component.runRule(0, "1:2:3"));
        assertEquals(493706, component.runRule(1, "CODE-49- numbers inside <37> <5> text"));

        // different functions are invoked in different way
        assertEquals("42false13.0!", component.runAtomic("key", "hanna barbera").append("!").toString());
    }

    @Test
    public void exampleUseDetached() {
        Component component = new Component();

        component
            // this method is designed to store produced function inside component
            .message()
            .withParam(text -> Integer.parseInt(text.split(":")[1]))
            .withParam(text -> text.split(":")[2] )
            .build()
            .hello("other component methods");

        BiFunction<String, String, String> detached = component
            .message()
            .withParam(text -> Arrays.stream(text.split(":")).mapToInt(Integer::parseInt).sum() )
            .withParam(text -> text.toUpperCase(Locale.ROOT))
            // we can call #function instead of #build, to avoid chaining
            // returned function can be used directly, but it won't be fully managed by component
            .function();
        // please note, that creators of component can store functions created in this way too
        // it is just our example component's decision to not do that

        Function4<String, String, String, String, String> generic = component
            .message()
            .withParam(text -> Arrays.stream(text.split(":")).mapToInt(Integer::parseInt).sum() )
            .withParam(text -> text.toUpperCase(Locale.ROOT))
            .withParam(text -> text.toLowerCase(Locale.ROOT))
            .withParam(text -> text.substring(0,1))
            .function();

        // use component to invoke first function
        assertEquals("{code=2 message=text}", component.format(0, "1:2:text"));

        // detached functions can be invoked directly
        assertEquals("{code=24 message=SIMPLE}", detached.apply("12:3:9", "simple"));
        assertEquals("{generic=[24, SIMPLE, low, r]}", generic.apply("12:3:9", "simple", "LOW", "ret"));

        // but detached functions aren't inside component
        assertEquals(1, component.messages.size());
    }

    @Test
    public void exampleUseCustomResult() {
        Component component = new Component();

        // this is ordinary detached function (look at example above)
        BiFunction<Double, Double, Double> func1 = component.math()
            .withParam(v -> v * v)
            .withParam(v -> 2 * v)
            .bind((a, b) -> a + b)
            .function();

        // this is custom object created by component
        // please note, that method #build does not return component chain but
        // completely new type
        ComponentMath func2 = component.math()
            .withParam(v -> v * v)
            .withParam(v -> 2 * v)
            .bind((a, b) -> a + b)
            .build();

        // direct invocation of detached function
        assertEquals(52, func1.apply(4.0, 5.0), 0.1);

        // notice, that created object augmented behaviour of created function
        assertEquals(34, func2.asDouble(3, 4), 0.1);
    }

    static class Component {

        private final List<Function<String, Integer>> rules = new ArrayList<>();

        private final Map<String, Function<String, StringBuilder>> atomics = new HashMap<>();

        private final List<Function<String, String>> messages = new ArrayList<>();

        public Component hello(String input) {
            return this;
        }

        public int runRule(int rule, String input) {
            return rules.get(rule).apply(input);
        }

        public StringBuilder runAtomic(String rule, String input) {
            return atomics.get(rule).apply(input);
        }

        public String format(int index, String input) {
            return messages.get(index).apply(input);
        }

        public CompositorScope<Component, String, Integer>.Compositor0<Object, Object, Object, Object, Integer> rule() {
            return Compositor.compose(new CompositorDecorator<Component, String, Integer>() {
                @Override
                public Component chain() {
                    return Component.this;
                }

                @Override
                public boolean accept(CompositorBuilder<String, Integer> builder) {
                    rules.add(builder.compact());
                    return true;
                }
            });
        }

        public CompositorScope<Component, String, StringBuilder>.Compositor0<AtomicInteger, AtomicInteger, AtomicBoolean, AtomicDouble, StringBuilder> atomic(String name) {
            return Compositor.compose(new CompositorDecorator<Component, String, StringBuilder>(this) {
                @Override
                public boolean accept(CompositorBuilder<String, StringBuilder> builder) {
                    atomics.put(name, builder.compact());
                    return true;
                }
            });
        }

        public CompositorScope<Component, String, String>.Compositor0<Object, Object, Object, Object, String> message() {
            return Compositor.compose(new FormatDecorator());
        }

        public CompositorScope<ComponentMath, Double, Double>.Compositor0<Object, Object, Object, Object, Double> math() {
            return Compositor.compose(new MathDecorator());
        }

        private static class MathDecorator extends CompositorDecorator<ComponentMath, Double, Double> {
            @Override
            public Function2<Object, Object, Double> after(Function2<Object, Object, Double> action) {
                return (a, b) -> 2 * action.apply(a, b);
            }

            @Override
            public boolean accept(Function2<Double, Double, Double> action) {
                return true;
            }

            @Override
            public ComponentMath chain(Function2<Double, Double, Double> action) {
                return new ComponentMath(action);
            }
        }

        private class FormatDecorator extends CompositorDecorator<Component, String, String> {
            @Override
            public Component chain() {
                return Component.this;
            }

            @Override
            public boolean accept(CompositorBuilder<String, String> builder) {
                messages.add(builder.compact());
                return true;
            }

            @Override
            public Function1<Object, String> after(Function1<Object, String> action) {
                return (a) -> String.format("{code=%d}", a);
            }

            @Override
            public Function2<Object, Object, String> after(Function2<Object, Object, String> action) {
                return (a,b) -> String.format("{code=%d message=%s}", a, b);
            }

            @Override
            public Function3<Object,Object,Object,String> after(Function3<Object, Object, Object, String> action) {
                return (a,b, c) -> String.format("{code=%d message=%s scope=%s}", a, b, c);
            }

            @Override
            public AnyFunction<String> after(CompositorBuilder<String, String> builder) {
                return (args) -> String.format("{generic=%s}", Arrays.toString(args));
            }
        }

    }

    @RequiredArgsConstructor
    public static class ComponentMath {

        private final BiFunction<Double, Double,Double> func;

        public double asDouble(double a, double b) {
            return func.apply(a,b);
        }

        public BigDecimal asDecimal(double a, double b) {
            return new BigDecimal(Math.round(asDouble(a,b)));
        }

    }

    @Test
    public void coverageCompositor0() {
        Function0<Integer> func1 = Compositor.compose().bind(() -> 17).function();
        Function1<Object, Integer> func2 = Compositor.compose().bind(() -> 19).compact();
        AnyFunction<Object> func3 = Compositor.compose().bind(() -> 21).build();
        Function1<Object, Integer> func4 = Compositor.compose().withValue(() -> 24).bind(x -> 3 * x).function();

        assertEquals(17, func1.apply().intValue());
        assertEquals(19, func2.apply("ignored").intValue());
        assertEquals(21, func3.call());
        assertEquals(72, func4.apply("ignored").intValue());
    }

    @Test
    public void coverageCompact() {
        Function1<Integer, String> func1 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .bind((a) -> String.format("%d", a))
            .compact();
        Function1<Integer, String> func2 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .withParam((Integer v) -> 4 * v)
            .bind((a,b) -> String.format("%d:%d", a,b))
            .compact();
        Function1<Integer, String> func3 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .withParam((Integer v) -> 4 * v)
            .withParam((Integer v) -> 8 * v)
            .bind((a,b,c) -> String.format("%d:%d:%d", a,b,c))
            .compact();
        Function1<Integer, String> func4 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .withParam((Integer v) -> 4 * v)
            .withParam((Integer v) -> 8 * v)
            .withParam((Integer v) -> 16 * v)
            .bind((a,b,c,d) -> String.format("%d:%d:%d:%d", a,b,c,d))
            .compact();


        assertEquals("6", func1.apply(3));
        assertEquals("6:12", func2.apply(3));
        assertEquals("6:12:24", func3.apply(3));
        assertEquals("6:12:24:48", func4.apply(3));
    }

    @Test
    public void coverageBuild() {
        AnyFunction<String> func1 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .bind((a) -> String.format("%d", a))
            .build();
        AnyFunction<String> func2 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .withParam((Integer v) -> 4 * v)
            .bind((a,b) -> String.format("%d:%d", a,b))
            .build();
        AnyFunction<String> func3 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .withParam((Integer v) -> 4 * v)
            .withParam((Integer v) -> 8 * v)
            .bind((a,b,c) -> String.format("%d:%d:%d", a,b,c))
            .build();
        AnyFunction<String> func4 = Compositor.<Integer, String>compose()
            .withParam((Integer v) -> 2 * v)
            .withParam((Integer v) -> 4 * v)
            .withParam((Integer v) -> 8 * v)
            .withParam((Integer v) -> 16 * v)
            .bind((a,b,c,d) -> String.format("%d:%d:%d:%d", a,b,c,d))
            .build();


        assertEquals("6", func1.call(3));
        assertEquals("6:4", func2.call(3,1));
        assertEquals("6:4:56", func3.call(3,1,7));
        assertEquals("6:4:56:64", func4.call(3,1,7,4));

        // this is not well defined behaviour, but it is important to point it out:
        // additional arguments in AnyFunction are silently ignored
        assertEquals("6", func1.call(3,1,7,4,9,2));
        assertEquals("6:4", func2.call(3,1,7,4,9,2));
        assertEquals("6:4:56", func3.call(3,1,7,4,9,2));
        assertEquals("6:4:56:64", func4.call(3,1,7,4,9,2));
    }

    @Test
    public void coverageArgumentsCount() {
        CompositorDecorator<Integer,Integer, String> decorator1 = new CompositorDecorator<Integer,Integer, String>() {
            @Override
            public boolean accept(CompositorBuilder<Integer, String> function) {
                return true;
            }

            public Integer chain(CompositorBuilder<Integer, String> function) {
                return function.arguments();
            }
        };

        int args0 = Compositor.compose(decorator1).build();
        assertEquals(0, args0);

        int args1 = Compositor.compose(decorator1)
            .withValue(() -> 0)
            .build();
        assertEquals(1, args1);

        int args2 = Compositor.compose(decorator1)
            .withValue(() -> 0)
            .withValue(() -> 0)
            .build();
        assertEquals(2, args2);
        int args3 = Compositor.compose(decorator1)
            .withValue(() -> 0)
            .withValue(() -> 0)
            .withValue(() -> 0)
            .build();
        assertEquals(3, args3);
        int args4 = Compositor.compose(decorator1)
            .withValue(() -> 0)
            .withValue(() -> 0)
            .withValue(() -> 0)
            .withValue(() -> 0)
            .build();
        assertEquals(4, args4);
    }

    @Test
    public void coverageWithValue() {
        AnyFunction<Object> function4 = Compositor
            .compose()
            .withValue(() -> 1)
            .withValue(() -> 3)
            .withValue(() -> 6)
            .withValue(() -> 8)
            .bind((a, b, c, d) -> 1000 * a + 100 * b + 10 * c + d)
            .build();

        Function1<Object, Integer> compacted = Compositor
            .compose()
            .withValue(() -> 1)
            .withValue(() -> 3)
            .withValue(() -> 6)
            .withValue(() -> 8)
            .bind((a, b, c, d) -> 1000 * a + 100 * b + 10 * c + d)
            .compact();

        assertEquals(1368, function4.call("ignored", "ignored", "ignored", "ignored"));

        // please note, that #withValue is not fully supported
        // we have to pass 4 arguments into the function even, if they are ignored
        try {
            assertEquals(1368, function4.call("ignored", "second"));
            fail("Some exception expected");
        } catch (RuntimeException cause) {
            assertTrue(true);
        }
        try {
            assertEquals(1368, function4.call());
            fail("Some exception expected");
        } catch (RuntimeException cause) {
            assertTrue(true);
        }

        // compacted function requires one argument, because internally it is applied to all parameters
        assertEquals(1368, compacted.apply("ignored").intValue());
        assertEquals(1368, compacted.apply(null).intValue());
    }

    @Test
    public void coverageWithClass() {
        List<String> out = new ArrayList<>();

        Consumer0 consumer0 = Compositor
            .compose()
            .bind(() -> out.add("0#"))
            .consumer();
        Consumer1<String> consumer1 = Compositor
            .compose()
            .withParam(String.class)
            .bind((a) -> out.add("1#" + a))
            .consumer();
        Consumer2<String, String> consumer2 = Compositor
            .compose()
            .withParam(String.class)
            .withParam(String.class)
            .bind((a, b) -> out.add("2#" + a + b))
            .consumer();
        Consumer3<String, String, String> consumer3 = Compositor
            .compose()
            .withParam(String.class)
            .withParam(String.class)
            .withParam(String.class)
            .bind((a, b, c) -> out.add("3#" + a + b + c))
            .consumer();
        Consumer4<String, String, String, String> consumer4 = Compositor
            .compose()
            .withParam(String.class)
            .withParam(String.class)
            .withParam(String.class)
            .withParam(String.class)
            .bind((a, b, c, d) -> out.add("4#" + a + b + c + d))
            .consumer();

        consumer0.accept();
        consumer1.accept("x");
        consumer2.accept("p", "q");
        consumer3.accept("a", "b", "c");
        consumer4.accept("1", "2", "3", "4");

        assertEquals(Arrays.asList("0#", "1#x", "2#pq", "3#abc", "4#1234"), out);
    }

    @Test
    public void coverageWithIClass() {
        List<String> out = new ArrayList<>();

        Consumer0 consumer0 = Compositor
            .compose()
            .bind(() -> out.add("0#"))
            .consumer();
        Consumer1<String> consumer1 = Compositor
            .compose()
            .withParam(IClass.STRING)
            .bind((a) -> out.add("1#" + a))
            .consumer();
        Consumer2<String, String> consumer2 = Compositor
            .compose()
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .bind((a, b) -> out.add("2#" + a + b))
            .consumer();
        Consumer3<String, String, String> consumer3 = Compositor
            .compose()
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .bind((a, b, c) -> out.add("3#" + a + b + c))
            .consumer();
        Consumer4<String, String, String, String> consumer4 = Compositor
            .compose()
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .bind((a, b, c, d) -> out.add("4#" + a + b + c + d))
            .consumer();

        consumer0.accept();
        consumer1.accept("x");
        consumer2.accept("p", "q");
        consumer3.accept("a", "b", "c");
        consumer4.accept("1", "2", "3", "4");

        assertEquals(Arrays.asList("0#", "1#x", "2#pq", "3#abc", "4#1234"), out);
    }

    @Test
    public void coverageSource() {
        List<String> out = new ArrayList<>();
        List<String> raw = new ArrayList<>();

        CompositorDecorator<Void, Object, Object> decorator = new CompositorDecorator<Void, Object, Object>() {
            @Override
            public AnyFunction<Object> before(CompositorBuilder<Object, Object> function) {
                return (args) -> raw.add("(" + function.source().apply(args) + ")");
            }
        };

        Consumer0 consumer0 = Compositor
            .compose(decorator)
            .bind(() -> out.add("0#"))
            .consumer();
        Consumer1<String> consumer1 = Compositor
            .compose(decorator)
            .withParam(IClass.STRING)
            .bind((a) -> out.add("1#" + a))
            .consumer();
        Consumer2<String, String> consumer2 = Compositor
            .compose(decorator)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .bind((a, b) -> out.add("2#" + a + b))
            .consumer();
        Consumer3<String, String, String> consumer3 = Compositor
            .compose(decorator)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .bind((a, b, c) -> out.add("3#" + a + b + c))
            .consumer();
        Consumer4<String, String, String, String> consumer4 = Compositor
            .compose(decorator)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .withParam(IClass.STRING)
            .bind((a, b, c, d) -> out.add("4#" + a + b + c + d))
            .consumer();

        consumer0.accept();
        consumer1.accept("x");
        consumer2.accept("p", "q");
        consumer3.accept("a", "b", "c");
        consumer4.accept("1", "2", "3", "4");

        assertEquals(Arrays.asList("0#", "1#x", "2#pq", "3#abc", "4#1234"), out);
        assertEquals(Arrays.asList("(true)", "(true)", "(true)", "(true)", "(true)"), raw);
    }


    @Test
    public void testConditionalEvaluation() {
        CompositorDecorator<Void, Object, String> decorator1 = new CompositorDecorator<Void, Object, String>() {
            @Override
            public Function2<Object, Object, String> before(Function2<Object, Object, String> action) {
                return (a,b) -> {
                    if(a instanceof Integer && b instanceof Integer) {
                        return action.apply(a,b);
                    } else {
                        return "#1";
                    }
                };
            }
        };
        CompositorDecorator<Void, Object, String> decorator2 = new CompositorDecorator<Void, Object, String>() {
            @Override
            public AnyFunction<String> before(CompositorBuilder<Object, String> function) {
                return (args) -> {
                    if(args.length == 2 && args[0] instanceof Integer && args[1] instanceof Integer) {
                        return function.body().apply(args);
                    } else {
                        return "#2";
                    }
                };
            }
        };

        Function2<Object, Object, String> function1 = Compositor.compose(decorator1)
            .withParam(value -> 10 * (Integer)value)
            .withParam(value -> 10 * (Integer)value)
            .bind((a,b) -> String.format("%s:%s", a,b))
            .function();

        Function2<Object, Object, String> function2 = Compositor.compose(decorator2)
            .withParam(value -> 10 * (Integer)value)
            .withParam(value -> 10 * (Integer)value)
            .bind((a,b) -> String.format("%s:%s", a,b))
            .function();

        assertEquals("50:60", function1.apply(5, 6));
        assertEquals("10:60", function1.apply(1, 6));
        assertEquals("#1", function1.apply("e", "z"));

        assertEquals("50:60", function2.apply(5, 6));
        assertEquals("10:60", function2.apply(1, 6));
        assertEquals("#2", function2.apply("e", "z"));
    }

    @AllArgsConstructor
    private static final class AtomicDouble extends Number {

        private final double value;

        @Override
        public int intValue() {
            return (int) value;
        }

        @Override
        public long longValue() {
            return (long) value;
        }

        @Override
        public float floatValue() {
            return (float) value;
        }

        @Override
        public double doubleValue() {
            return value;
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }
    }

}