/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 *
 *      https://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 org.apache.commons.collections4;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections4.functors.EqualPredicate;
import org.apache.commons.collections4.functors.ExceptionClosure;
import org.apache.commons.collections4.functors.FalsePredicate;
import org.apache.commons.collections4.functors.NOPClosure;
import org.apache.commons.collections4.functors.TruePredicate;
import org.junit.jupiter.api.Test;

/**
 * Tests the ClosureUtils class.
 */
class ClosureUtilsTest {

    static class MockClosure<T> implements Closure<T> {
        int count;

        @Override
        public void execute(final T object) {
            count++;
        }

        public void reset() {
            count = 0;
        }
    }

    static class MockTransformer<T> implements Transformer<T, T> {
        int count;

        @Override
        public T transform(final T object) {
            count++;
            return object;
        }
    }

    private static final Object cString = "Hello";

    @Test
    @SuppressWarnings("unchecked")
    void testChainedClosure() {
        MockClosure<Object> a = new MockClosure<>();
        MockClosure<Object> b = new MockClosure<>();
        ClosureUtils.chainedClosure(a, b).accept(null);
        assertEquals(1, a.count);
        assertEquals(1, b.count);

        a = new MockClosure<>();
        b = new MockClosure<>();
        ClosureUtils.<Object>chainedClosure(a, b, a).execute(null);
        assertEquals(2, a.count);
        assertEquals(1, b.count);

        a = new MockClosure<>();
        b = new MockClosure<>();
        final Collection<Closure<Object>> coll = new ArrayList<>();
        coll.add(b);
        coll.add(a);
        coll.add(b);
        ClosureUtils.<Object>chainedClosure(coll).execute(null);
        assertEquals(1, a.count);
        assertEquals(2, b.count);

        assertSame(NOPClosure.INSTANCE, ClosureUtils.<Object>chainedClosure());
        assertSame(NOPClosure.INSTANCE, ClosureUtils.<Object>chainedClosure(Collections.<Closure<Object>>emptyList()));

        assertThrows(NullPointerException.class, () -> ClosureUtils.chainedClosure(null, null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.<Object>chainedClosure((Closure[]) null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.<Object>chainedClosure((Collection<Closure<Object>>) null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.<Object>chainedClosure(null, null));
        final Collection<Closure<Object>> finalColl = new ArrayList<>();
        finalColl.add(null);
        finalColl.add(null);
        assertThrows(NullPointerException.class, () -> ClosureUtils.chainedClosure(finalColl));
    }

    @Test
    void testDoWhileClosure() {
        MockClosure<Object> cmd = new MockClosure<>();
        ClosureUtils.doWhileClosure(cmd, FalsePredicate.falsePredicate()).execute(null);
        assertEquals(1, cmd.count);

        cmd = new MockClosure<>();
        ClosureUtils.doWhileClosure(cmd, PredicateUtils.uniquePredicate()).execute(null);
        assertEquals(2, cmd.count);

        assertThrows(NullPointerException.class, () -> ClosureUtils.doWhileClosure(null, null));
    }

    @Test
    void testExceptionClosure() {
        assertNotNull(ClosureUtils.exceptionClosure());
        assertSame(ClosureUtils.exceptionClosure(), ClosureUtils.exceptionClosure());

        assertThrows(FunctorException.class, () -> ClosureUtils.exceptionClosure().execute(null));
        assertThrows(FunctorException.class, () -> ClosureUtils.exceptionClosure().execute(cString));
    }

    @Test
    void testForClosure() {
        final MockClosure<Object> cmd = new MockClosure<>();
        ClosureUtils.forClosure(5, cmd).execute(null);
        assertEquals(5, cmd.count);
        assertSame(NOPClosure.INSTANCE, ClosureUtils.forClosure(0, new MockClosure<>()));
        assertSame(NOPClosure.INSTANCE, ClosureUtils.forClosure(-1, new MockClosure<>()));
        assertSame(NOPClosure.INSTANCE, ClosureUtils.forClosure(1, null));
        assertSame(NOPClosure.INSTANCE, ClosureUtils.forClosure(3, null));
        assertSame(cmd, ClosureUtils.forClosure(1, cmd));
    }

    @Test
    void testIfClosure() {
        MockClosure<Object> a = new MockClosure<>();
        MockClosure<Object> b;
        ClosureUtils.ifClosure(TruePredicate.truePredicate(), a).execute(null);
        assertEquals(1, a.count);

        a = new MockClosure<>();
        ClosureUtils.ifClosure(FalsePredicate.<Object>falsePredicate(), a).execute(null);
        assertEquals(0, a.count);

        a = new MockClosure<>();
        b = new MockClosure<>();
        ClosureUtils.ifClosure(TruePredicate.<Object>truePredicate(), a, b).execute(null);
        assertEquals(1, a.count);
        assertEquals(0, b.count);

        a = new MockClosure<>();
        b = new MockClosure<>();
        ClosureUtils.ifClosure(FalsePredicate.<Object>falsePredicate(), a, b).execute(null);
        assertEquals(0, a.count);
        assertEquals(1, b.count);
    }

    @Test
    void testInvokeClosure() {
        StringBuilder buf = new StringBuilder("Hello"); // Only StringBuffer has setLength() method
        ClosureUtils.invokerClosure("reverse").execute(buf);
        assertEquals("olleH", buf.toString());
        buf = new StringBuilder("Hello");
        ClosureUtils.invokerClosure("setLength", new Class[] {Integer.TYPE}, new Object[] {Integer.valueOf(2)}).execute(buf);
        assertEquals("He", buf.toString());
    }

    @Test
    void testNopClosure() {
        final StringBuilder buf = new StringBuilder("Hello");
        ClosureUtils.nopClosure().execute(null);
        assertEquals("Hello", buf.toString());
        ClosureUtils.nopClosure().execute("Hello");
        assertEquals("Hello", buf.toString());
    }

    /**
     * Test that all Closure singletons hold singleton pattern in
     * serialization/deserialization process.
     */
    @Test
    void testSingletonPatternInSerialization() throws ClassNotFoundException, IOException {
        final Object[] singletons = {
            ExceptionClosure.INSTANCE,
            NOPClosure.INSTANCE,
        };

        for (final Object original : singletons) {
            TestUtils.assertSameAfterSerialization(
                    "Singleton pattern broken for " + original.getClass(),
                    original
            );
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    void testSwitchClosure() {
        final MockClosure<String> a = new MockClosure<>();
        final MockClosure<String> b = new MockClosure<>();
        ClosureUtils.<String>switchClosure(
            new Predicate[] { EqualPredicate.equalPredicate("HELLO"), EqualPredicate.equalPredicate("THERE") },
            new Closure[] { a, b }).execute("WELL");
        assertEquals(0, a.count);
        assertEquals(0, b.count);

        a.reset();
        b.reset();
        ClosureUtils.<String>switchClosure(
            new Predicate[] { EqualPredicate.equalPredicate("HELLO"), EqualPredicate.equalPredicate("THERE") },
            new Closure[] { a, b }).execute("HELLO");
        assertEquals(1, a.count);
        assertEquals(0, b.count);

        a.reset();
        b.reset();
        final MockClosure<String> c = new MockClosure<>();
        ClosureUtils.<String>switchClosure(
            new Predicate[] { EqualPredicate.equalPredicate("HELLO"), EqualPredicate.equalPredicate("THERE") },
            new Closure[] { a, b }, c).execute("WELL");
        assertEquals(0, a.count);
        assertEquals(0, b.count);
        assertEquals(1, c.count);

        a.reset();
        b.reset();
        final Map<Predicate<String>, Closure<String>> map = new HashMap<>();
        map.put(EqualPredicate.equalPredicate("HELLO"), a);
        map.put(EqualPredicate.equalPredicate("THERE"), b);
        ClosureUtils.<String>switchClosure(map).execute(null);
        assertEquals(0, a.count);
        assertEquals(0, b.count);

        a.reset();
        b.reset();
        map.clear();
        map.put(EqualPredicate.equalPredicate("HELLO"), a);
        map.put(EqualPredicate.equalPredicate("THERE"), b);
        ClosureUtils.switchClosure(map).execute("THERE");
        assertEquals(0, a.count);
        assertEquals(1, b.count);

        a.reset();
        b.reset();
        c.reset();
        map.clear();
        map.put(EqualPredicate.equalPredicate("HELLO"), a);
        map.put(EqualPredicate.equalPredicate("THERE"), b);
        map.put(null, c);
        ClosureUtils.switchClosure(map).execute("WELL");
        assertEquals(0, a.count);
        assertEquals(0, b.count);
        assertEquals(1, c.count);

        assertEquals(NOPClosure.INSTANCE, ClosureUtils.<String>switchClosure(new Predicate[0], new Closure[0]));
        assertEquals(NOPClosure.INSTANCE, ClosureUtils.<String>switchClosure(new HashMap<>()));
        map.clear();
        map.put(null, null);
        assertEquals(NOPClosure.INSTANCE, ClosureUtils.switchClosure(map));

        assertThrows(NullPointerException.class, () -> ClosureUtils.switchClosure(null, null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.<String>switchClosure((Predicate<String>[]) null, (Closure<String>[]) null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.<String>switchClosure((Map<Predicate<String>, Closure<String>>) null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.<String>switchClosure(new Predicate[2], new Closure[2]));
        assertThrows(IllegalArgumentException.class,
                () -> ClosureUtils.<String>switchClosure(new Predicate[] { TruePredicate.<String>truePredicate() }, new Closure[] { a, b }));
    }

    @Test
    void testSwitchClosureDoesNotMutateInputMap() {
        final MockClosure<String> def = new MockClosure<>();
        final MockClosure<String> match = new MockClosure<>();
        final Map<Predicate<String>, Closure<String>> predicateMap = new HashMap<>();
        predicateMap.put(null, def);
        predicateMap.put(EqualPredicate.equalPredicate("HELLO"), match);
        final Closure<String> closure = ClosureUtils.switchClosure(predicateMap);
        assertTrue(predicateMap.containsKey(null));
        closure.execute("HELLO");
        closure.execute("WORLD");
        assertEquals(1, match.count);
        assertEquals(1, def.count);

        final MockClosure<String> mapDef = new MockClosure<>();
        final MockClosure<String> mapMatch = new MockClosure<>();
        final Map<String, Closure<String>> objectMap = new HashMap<>();
        objectMap.put(null, mapDef);
        objectMap.put("HELLO", mapMatch);
        final Closure<String> mapClosure = ClosureUtils.switchMapClosure(objectMap);
        assertTrue(objectMap.containsKey(null));
        mapClosure.execute("HELLO");
        mapClosure.execute("WORLD");
        assertEquals(1, mapMatch.count);
        assertEquals(1, mapDef.count);
    }

    @Test
    void testSwitchMapClosure() {
        final MockClosure<String> a = new MockClosure<>();
        final MockClosure<String> b = new MockClosure<>();
        final Map<String, Closure<String>> map = new HashMap<>();
        map.put("HELLO", a);
        map.put("THERE", b);
        ClosureUtils.switchMapClosure(map).execute(null);
        assertEquals(0, a.count);
        assertEquals(0, b.count);

        a.reset();
        b.reset();
        map.clear();
        map.put("HELLO", a);
        map.put("THERE", b);
        ClosureUtils.switchMapClosure(map).execute("THERE");
        assertEquals(0, a.count);
        assertEquals(1, b.count);

        a.reset();
        b.reset();
        map.clear();
        final MockClosure<String> c = new MockClosure<>();
        map.put("HELLO", a);
        map.put("THERE", b);
        map.put(null, c);
        ClosureUtils.switchMapClosure(map).execute("WELL");
        assertEquals(0, a.count);
        assertEquals(0, b.count);
        assertEquals(1, c.count);

        assertEquals(NOPClosure.INSTANCE, ClosureUtils.switchMapClosure(new HashMap<>()));

        assertThrows(NullPointerException.class, () -> ClosureUtils.switchMapClosure(null));
    }

    @Test
    void testTransformerClosure() {
        final MockTransformer<Object> mock = new MockTransformer<>();
        final Closure<Object> closure = ClosureUtils.asClosure(mock);
        closure.execute(null);
        assertEquals(1, mock.count);
        closure.execute(null);
        assertEquals(2, mock.count);

        assertEquals(ClosureUtils.nopClosure(), ClosureUtils.asClosure(null));
    }

    @Test
    void testWhileClosure() {
        MockClosure<Object> cmd = new MockClosure<>();
        ClosureUtils.whileClosure(FalsePredicate.falsePredicate(), cmd).execute(null);
        assertEquals(0, cmd.count);

        cmd = new MockClosure<>();
        ClosureUtils.whileClosure(PredicateUtils.uniquePredicate(), cmd).execute(null);
        assertEquals(1, cmd.count);

        assertThrows(NullPointerException.class, () -> ClosureUtils.whileClosure(null, ClosureUtils.nopClosure()));
        assertThrows(NullPointerException.class, () -> ClosureUtils.whileClosure(FalsePredicate.falsePredicate(), null));
        assertThrows(NullPointerException.class, () -> ClosureUtils.whileClosure(null, null));
    }

}
