/*
 *  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
 *
 *    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 groovy.lang;

import junit.framework.TestCase;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import static org.junit.jupiter.api.Assertions.assertNotEquals;

/**
 * Provides unit tests for ranges of numbers.
 */
public abstract class NumberRangeTestCase extends TestCase {

    /**
     * Records the values passed to a closure.
     */
    protected static class RecordingClosure extends Closure {
        /**
         * Holds the values passed in
         */
        final List callLog;

        /**
         * Creates a new <code>RecordingClosure</code>
         *
         * @param callLog is filled with the values passed to <code>doCall</code>
         */
        RecordingClosure(final List callLog) {
            super(null);
            this.callLog = callLog;
        }

        /**
         * Stores <code>params</code> in the <code>callLog</code>.
         *
         * @param params the parameters.
         * @return null
         */
        public Object doCall(final Object params) {
            callLog.add(params);
            return null;
        }
    }

    /**
     * Creates a {@link Range} to test.
     *
     * @param from the first value in the range.
     * @param to   the last value in the range.
     * @return a {@link Range} to test
     */
    protected abstract Range createRange(final int from, final int to);

    /**
     * Creates a value in the range.
     *
     * @param value the value to create.
     * @return a value in the range.
     */
    protected abstract Comparable createValue(final int value);

    /**
     * Tests <code>hashCode</code> and <code>equals</code> comparing one {@link IntRange} to another {@link IntRange}.
     */
    public final void testHashCodeAndEquals() {
        Range a = createRange(1, 11);
        Range b = createRange(1, 11);
        Range c = createRange(2, 11);

        assertEquals("hashcode", a.hashCode(), b.hashCode());
        assertTrue("hashcode", a.hashCode() != c.hashCode());

        assertEquals("a and b", a, b);
        assertNotEquals(a, c, "a != c");
    }

    /**
     * Tests using different classes for 'from' and 'to'.
     */
    public void testDifferentClassesForFromAndTo() {
        final Integer from = Integer.valueOf(1);
        final Comparable to = createValue(5);
        final Range range = new ObjectRange(from, to);

        checkRangeValues(from, to, range);
    }

    protected void checkRangeValues(Integer from, Comparable to, Range range) {
        assertEquals("wrong 'from' value", from, range.getFrom());
        assertEquals("wrong 'to' value", to, range.getTo());
    }

    /**
     * Tests a <code>null</code> 'from' value.
     */
    public void testNullFrom() {
        try {
            new ObjectRange(null, createValue(5));
            fail("null 'from' accepted");
        }
        catch (IllegalArgumentException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests a <code>null</code> 'to' value.
     */
    public void testNullTo() {
        try {
            new ObjectRange(createValue(23), null);
            fail("null 'to' accepted");
        }
        catch (IllegalArgumentException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests stepping through a range by two with a closure.
     */
    public void testStepByTwoWithClosure() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(0, 4);
        range.step(2, closure);

        assertEquals("wrong number of calls to closure", 3, callLog.size());
        final Iterator iter = callLog.iterator();
        for (int i = 0; i <= 4; i += 2) {
            assertEquals("wrong argument passed to closure", createValue(i), iter.next());
        }
    }

    /**
     * Tests iterating over a one-element range.
     */
    public void testOneElementRange() {
        final Range range = createRange(1, 1);
        int next = 1;
        for (Object value : range) {
            final Number number = (Number) value;
            assertEquals("wrong number", createValue(next++), number);
        }
        assertEquals("wrong number of elements in iteration", 2, next);
    }

    /**
     * Tests stepping through a reversed range by two with a closure.
     */
    public void testReverseStepByTwoWithClosure() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(4, 0);
        range.step(2, closure);

        assertEquals("wrong number of calls to closure", 3, callLog.size());
        final Iterator iter = callLog.iterator();
        for (int i = 4; i >= 0; i -= 2) {
            assertEquals("wrong argument passed to closure", createValue(i), iter.next());
        }
    }

    /**
     * Tests stepping through a range with a closure.
     */
    public void testStepByOneWithClosure() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(1, 5);
        range.step(1, closure);

        assertEquals("wrong number of calls to closure", 5, callLog.size());
        final Iterator iter = callLog.iterator();
        for (int i = 1; i <= 5; i++) {
            assertEquals("wrong argument passed to closure", createValue(i), iter.next());
        }
    }

    /**
     * Tests stepping through a reversed range by one with a closure.
     */
    public void testReverseStepByOneWithClosure() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(5, 1);
        range.step(1, closure);

        assertEquals("wrong number of calls to closure", 5, callLog.size());
        final Iterator iter = callLog.iterator();
        for (int i = 5; i >= 1; i--) {
            assertEquals("wrong argument passed to closure", createValue(i), iter.next());
        }
    }

    /**
     * Tests stepping backwards through a range with a closure.
     */
    public void testNegativeStepByOneWithClosure() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(1, 5);
        range.step(-1, closure);

        assertEquals("wrong number of calls to closure", 5, callLog.size());
        final Iterator iter = callLog.iterator();
        for (int i = 5; i >= 1; i--) {
            assertEquals("wrong argument passed to closure", createValue(i), iter.next());
        }
    }

    /**
     * Tests stepping backwards through a reversed range with a closure.
     */
    public void testNegativeReverseStepByOneWithClosure() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(5, 1);
        range.step(-1, closure);

        assertEquals("wrong number of calls to closure", 5, callLog.size());
        final Iterator iter = callLog.iterator();
        for (int i = 1; i <= 5; i++) {
            assertEquals("wrong argument passed to closure", createValue(i), iter.next());
        }
    }

    /**
     * Tests stepping backwards through a range with a step size greater than the range size.
     */
    public void testStepLargerThanRange() {
        final List callLog = new ArrayList();
        final Closure closure = new RecordingClosure(callLog);

        final Range range = createRange(1, 5);

        range.step(6, closure);
        assertEquals("wrong number of calls to closure", 1, callLog.size());
        assertEquals("wrong value", createValue(1), callLog.get(0));

        final List stepList = range.step(6);
        assertEquals("wrong number of values in result", 1, stepList.size());
        assertEquals("wrong value", createValue(1), callLog.get(0));
    }

    /**
     * Tests stepping through a range by one.
     */
    public void testStepByOne() {
        final Range range = createRange(1, 5);
        final List result = range.step(1);

        assertEquals("wrong number of calls", 5, result.size());
        final Iterator iter = result.iterator();
        for (int i = 1; i <= 5; i++) {
            assertEquals("incorrect value in result", createValue(i), iter.next());
        }
    }

    /**
     * Tests stepping through a range by two.
     */
    public void testStepByTwo() {
        final Range range = createRange(1, 5);
        final List result = range.step(2);

        assertEquals("wrong number of calls", 3, result.size());
        final Iterator iter = result.iterator();
        for (int i = 1; i <= 5; i += 2) {
            assertEquals("incorrect value in result", createValue(i), iter.next());
        }
    }

    /**
     * Tests getting the size.
     */
    public void testSize() {
        Range range = createRange(0, 10);
        assertEquals("Size of " + range, 11, range.size());
        range = createRange(0, 1);
        assertEquals("Size of " + range, 2, range.size());
        range = createRange(0, 0);
        assertEquals("Size of " + range, 1, range.size());
    }

    /**
     * Tests asking for an index outside of the valid range
     */
    public void testGetOutOfRange() {
        Range r = createRange(10, 20);

        try {
            r.get(-1);
            fail("Should have thrown IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
            assertTrue("expected exception thrown", true);
        }
        try {
            r.get(11);
            fail("Should have thrown IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
            assertTrue("expected exception thrown", true);
        }

    }

    /**
     * Tests getting a sub list.
     */
    public void testSubList() {
        Range range = createRange(0, 5);

        List subList = range.subList(2, 4);
        assertEquals("size", 2, subList.size());

        assertTrue("sublist not a range", subList instanceof Range);
        Range subListRange = (Range) subList;

        assertEquals("from", createValue(2), subListRange.getFrom());
        assertEquals("to", createValue(3), subListRange.getTo());

        subList = range.subList(0, 6);
        assertEquals("size", 6, subList.size());

        assertTrue("sublist not a range", subList instanceof Range);
        subListRange = (Range) subList;

        assertEquals("from", createValue(0), subListRange.getFrom());
        assertEquals("to", createValue(5), subListRange.getTo());
    }

    /**
     * Tests creating a sub list with a negative "from" index.
     */
    public void testSubListNegativeFrom() {
        try {
            final Range range = createRange(1, 5);
            range.subList(-1, 3);
            fail("accepted sub list with negative index");
        }
        catch (IndexOutOfBoundsException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests creating a sub list with an out of range "to" index.
     */
    public void testSubListOutOfRangeTo() {
        try {
            final Range range = createRange(0, 3);
            range.subList(0, 5);
            fail("accepted sub list with invalid 'to'");
        }
        catch (IndexOutOfBoundsException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests creating a sub list with "from" grater than "to."
     */
    public void testSubListFromGreaterThanTo() {
        try {
            final Range range = createRange(1, 5);
            range.subList(3, 2);
            fail("accepted sub list with 'from' greater than 'to'");
        }
        catch (IllegalArgumentException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests creating an empty sub list.
     */
    public void testEmptySubList() {
        final Range range = createRange(1, 5);

        List subList = range.subList(0, 0);
        assertEquals("wrong number of elements in sub list", 0, subList.size());

        subList = range.subList(2, 2);
        assertEquals("wrong number of elements in sub list", 0, subList.size());
    }

    /**
     * Tests iterating over a non-reversed range.
     */
    public void testIterate() {
        final Range range = createRange(1, 5);
        int next = 1;
        final Iterator iter = range.iterator();
        while (iter.hasNext()) {
            final Object value = iter.next();
            assertEquals("wrong next value", createValue(next++), value);
        }
        assertEquals("wrong number of elements in iteration", 6, next);
        try {
            iter.next();
            fail("successfully got element from exhausted iterator");
        } catch(NoSuchElementException ignore) {
        }
    }

    /**
     * Tests removing an element from the range using an iterator (not supported).
     */
    public void testRemoveFromIterator() {
        final Range range = createRange(1, 5);

        try {
            final Iterator iter = range.iterator();
            iter.remove();
            fail("successfully removed an element using an iterator");
        }
        catch (UnsupportedOperationException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests iterating over a reversed range.
     */
    public void testIterateReversed() {
        final Range range = createRange(5, 1);
        int next = 5;
        for (Object value : range) {
            assertEquals("wrong number", createValue(next--), value);
        }
        assertEquals("wrong number of elements in iteration", 0, next);
    }

    /**
     * Tests creating an <code>IntRange</code> with from > to.
     */
    public void testFromGreaterThanTo() {
        final int from = 9;
        final int to = 0;
        final Range range = createRange(from, to);

        assertTrue("range not reversed", range.isReverse());

        // make sure to/from are swapped
        assertEquals("from incorrect", createValue(to), range.getFrom());
        assertEquals("to incorrect", createValue(from), range.getTo());

        assertEquals("wrong size", 10, range.size());

        assertEquals("wrong first element", createValue(9), range.get(0));
        assertEquals("wrong last element", createValue(0), range.get(9));
    }

    /**
     * Tests creating an <code>IntRange</code> with from == to.
     */
    public void testFromEqualsTo() {
        final Range range = createRange(5, 5);

        assertFalse("range reversed", range.isReverse());
        assertEquals("wrong size", 1, range.size());
    }

    /**
     * Tests creating an <code>IntRange</code> with from < to.
     */
    public void testFromLessThanTo() {
        final int from = 1;
        final int to = 4;
        final Range range = createRange(from, to);

        assertFalse("range reversed", range.isReverse());

        assertEquals("to incorrect", createValue(from), range.getFrom());
        assertEquals("from incorrect", createValue(to), range.getTo());

        assertEquals("wrong size", 4, range.size());
    }

    /**
     * Making a range equal a list is not actually possible, since list.equals(range) will not evaluate to
     * <code>true</code> and <code>equals</code> should be symmetric.
     */
    public void testEqualsList() {
        final List list = new ArrayList();
        list.add(createValue(1));
        list.add(createValue(2));

        final Range range = createRange(1, 2);

        // cast to Object to test routing through equals(Object)
        assertTrue("range does not equal list", range.equals((Object) list));
        assertTrue("list does not equal range", list.equals(range));
        assertEquals("hash codes are not equal", range.hashCode(), list.hashCode());

        // compare lists that are the same size but contain different elements
        list.set(0, createValue(3));
        assertNotEquals(range, list, "range equals list");
        assertNotEquals(list, range, "list equals range");
        assertFalse("hash codes are equal", range.hashCode() == list.hashCode());

        // compare a list longer than the range
        list.set(0, createValue(1));
        list.add(createValue(3));
        assertNotEquals(range, list, "range equals list");
        assertNotEquals(list, range, "list equals range");
        assertFalse("hash are equal", range.hashCode() == list.hashCode());

        // compare a list shorter than the range
        list.remove(2);
        list.remove(1);
        assertNotEquals(range, list, "range equals list");
        assertNotEquals(list, range, "list equals range");
        assertFalse("hash are equal", range.hashCode() == list.hashCode());
    }

    /**
     * Tests comparing {@link Range} to an object that is not a {@link Range}.
     */
    public void testEqualsNonRange() {
        final Range range = createRange(1, 5);
        assertNotEquals(range, "hello", "range equal to string");
    }

    /**
     * Tests comparing a {@link Range} cast to an {@link Object}
     */
    public void testEqualsRangeAsObject() {
        final Range range1 = createRange(1, 5);
        final Range range2 = createRange(1, 5);
        assertTrue("ranges not equal", range1.equals((Object) range2));
    }

    /**
     * Tests comparing two {@link Range}s to each other.
     */
    public void testEqualsRange() {
        final Range range1 = createRange(1, 5);
        Range range2 = createRange(1, 5);
        assertTrue("ranges not equal", range1.equals((Object) range2));
        assertTrue("ranges not equal", range2.equals((Object) range1));
        assertEquals("hash codes not equal", range1.hashCode(), range2.hashCode());

        range2 = createRange(0, 5);
        assertNotEquals(range1, (Object) range2, "ranges equal");
        assertNotEquals(range2, (Object) range1, "ranges equal");
        assertFalse("hash codes equal", range1.hashCode() == range2.hashCode());

        range2 = createRange(1, 6);
        assertNotEquals(range1, (Object) range2, "ranges equal");
        assertNotEquals(range2, (Object) range1, "ranges equal");
        assertFalse("hash codes equal", range1.hashCode() == range2.hashCode());

        range2 = createRange(0, 6);
        assertNotEquals(range1, (Object) range2, "ranges equal");
        assertNotEquals(range2, (Object) range1, "ranges equal");
        assertFalse("hash codes equal", range1.hashCode() == range2.hashCode());

        range2 = createRange(2, 4);
        assertNotEquals(range1, (Object) range2, "ranges equal");
        assertNotEquals(range2, (Object) range1, "ranges equal");
        assertFalse("hash codes equal", range1.hashCode() == range2.hashCode());

        range2 = createRange(5, 1);
        assertNotEquals(range1, (Object) range2, "ranges equal");
        assertNotEquals(range2, (Object) range1, "ranges equal");
        assertFalse("hash codes equal", range1.hashCode() == range2.hashCode());
    }

    /**
     * Tests <code>toString</code> and <code>inspect</code>
     */
    public void testToStringAndInspect() {
        Range range = createRange(1, 5);
        String expected = range.getFrom() + ".." + range.getTo();
        assertEquals("wrong string representation", expected, range.toString());
        assertEquals("wrong string representation", expected, range.inspect());

        range = createRange(5, 1);
        expected = range.getTo() + ".." + range.getFrom();
        assertEquals("wrong string representation", expected, range.toString());
        assertEquals("wrong string representation", expected, range.inspect());
    }

    /**
     * Tests <code>getFrom</code> and <code>getTo</code>.
     */
    public void testGetFromAndTo() {
        final int from = 1, to = 5;
        final Range range = createRange(from, to);

        assertEquals("wrong 'from' value", createValue(from), range.getFrom());
        assertEquals("wrong 'to' value", createValue(to), range.getTo());
    }

    /**
     * Tests comparing a {@link Range} to <code>null</code>.
     */
    public void testEqualsNull() {
        final Range range = createRange(1, 5);
        assertNotEquals(range, null, "range equal to null");
        assertNotEquals(range, (Object) null, "range equal to null Object");
        assertNotEquals(range, (Range) null, "range equal to null Range");
        assertNotEquals(range, (List) null, "range equal to null List");
    }

    /**
     * Tests attempting to add a value to a range.
     */
    public void testAddValue() {
        try {
            final Range range = createRange(1, 5);
            range.add(createValue(20));
            fail("expected exception not thrown");
        }
        catch (UnsupportedOperationException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    /**
     * Tests attempting to remove a value from a range.
     */
    public void testRemoveValue() {
        try {
            final Range range = createRange(1, 5);
            range.remove(0);
            fail("expected exception not thrown");
        }
        catch (UnsupportedOperationException e) {
            assertTrue("expected exception thrown", true);
        }
    }

    private void doTestContains(int from, int to, Range range) {
        // test integers
        assertTrue("missing 'from' value", range.contains(createValue(from)));
        assertTrue("missing 'to' value", range.contains(createValue(to)));
        assertTrue("missing mid point", range.contains(createValue((from + to) / 2)));
        assertFalse("contains out of range value", range.contains(createValue(from - 1)));
        assertFalse("contains out of range value", range.contains(createValue(to + 1)));

        // test ranges
        assertTrue("missing same range", range.containsAll(createRange(from, to)));
        assertTrue("missing same range", range.containsAll(createRange(to, from)));
        assertTrue("missing strict subset", range.containsAll(createRange(from + 1, to - 1)));
        assertTrue("missing subset", range.containsAll(createRange(from, to - 1)));
        assertTrue("missing subset", range.containsAll(createRange(from + 1, to)));
        assertFalse("contains non-subset", range.containsAll(createRange(from - 1, to)));
        assertFalse("contains non-subset", range.containsAll(createRange(from, to + 1)));
        assertFalse("contains non-subset", range.containsAll(createRange(from - 2, from - 1)));

        // ranges don't contain other ranges
        assertFalse("range contains sub-range", range.contains(createRange(from + 1, to - 1)));

        // test list
        final List list = new ArrayList();
        list.add(createValue(from));
        list.add(createValue(to));
        assertTrue("missing strict subset", range.containsAll(list));

        // test non-integer number
        float v = (float) ((to + from) / 2.0 + 0.3);
        assertFalse("contains Float", range.contains(v));
    }

    /**
     * Tests whether the range contains a {@link Comparable} object which is not comparable with a {@link Number}.
     */
    public void testContainsIncompatibleComparable() {
        final Range range = createRange(1, 5);
        assertFalse("range contains string", range.contains("hello"));
        assertFalse("range contains string", range.contains("1"));
    }

    /**
     * Tests whether the range contains a non-comparable object.
     */
    public void testContainsNonComparable() {
        final Range range = createRange(1, 5);
        assertFalse("range contains hash map", range.contains(new HashMap()));
    }

    /**
     * Tests whether a {@link Range} contains another {@link Range} or a specific integer.
     */
    public void testContains() {
        final int from = 1, to = 5;
        doTestContains(from, to, createRange(from, to));
        doTestContains(from, to, createRange(to, from));
    }

    /**
     * Tests <code>get</code> from a reversed range.
     */
    public void testGetFromReversedRange() {
        final Range range = createRange(5, 1);

        for (int i = 0; i < 5; i++) {
            assertEquals("wrong element at position " + i, createValue(5 - i), range.get(i));
        }
    }

    /**
     * Tests getting values from the range.
     */
    public void testGet() {
        final Range range = createRange(10, 20);
        for (int i = 0; i <= 10; i++) {
            assertEquals("Item at index: " + i, createValue(i + 10), range.get(i));
        }
    }
}
