/*
 * 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 org.apache.sis.measure;

import java.util.Locale;
import java.time.Instant;

// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.apache.sis.test.Assertions.assertMessageContains;
import static org.apache.sis.test.Assertions.assertSerializedEquals;
import org.apache.sis.test.TestCase;


/**
 * Tests the {@link Range} class.
 *
 * @author  Joe White
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class RangeTest extends TestCase {
    /**
     * Creates a new test case.
     */
    public RangeTest() {
    }

    /**
     * Tests the creation of {@link Range} objects under normal conditions.
     */
    @Test
    public void testConstructor() {
        Range<Integer> range = new Range<>(Integer.class, 3, true, 5, true);
        assertEquals(Integer.valueOf(3), range.getMinValue());
        assertEquals(Integer.valueOf(5), range.getMaxValue());
        assertTrue  (range.isMaxIncluded());
        assertTrue  (range.isMinIncluded());
        assertFalse (range.isEmpty());

        range = new Range<>(Integer.class, 3, false, 5, true);
        assertEquals(Integer.valueOf(3), range.getMinValue());
        assertEquals(Integer.valueOf(5), range.getMaxValue());
        assertTrue  (range.isMaxIncluded());
        assertFalse (range.isMinIncluded());
        assertFalse (range.isEmpty());

        range = new Range<>(Integer.class, 2, true, 7, false);
        assertEquals(Integer.valueOf(2), range.getMinValue());
        assertEquals(Integer.valueOf(7), range.getMaxValue());
        assertFalse (range.isMaxIncluded());
        assertTrue  (range.isMinIncluded());
        assertFalse (range.isEmpty());

        range = new Range<>(Integer.class, 3, false, 10, false);
        assertEquals(Integer.valueOf( 3), range.getMinValue());
        assertEquals(Integer.valueOf(10), range.getMaxValue());
        assertFalse (range.isMaxIncluded());
        assertFalse (range.isMinIncluded());
        assertFalse (range.isEmpty());

        range = new Range<>(Integer.class, 10, true, 2, true);
        assertEquals(Integer.valueOf(10), range.getMinValue());
        assertEquals(Integer.valueOf( 2), range.getMaxValue());
        assertTrue (range.isEmpty());
    }

    /**
     * Tests the detection of illegal arguments at {@link Range} creation time.
     * Note that such error should never happen when parameterized types are used.
     * The check performed by the constructor is a safety in case the user bypass
     * the parameterized type check by using the raw type instead.
     *
     * <p>This test requires assertions to be enabled.</p>
     */
    @Test
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void testConstructorErrors00() {
        assertTrue(Range.class.desiredAssertionStatus());
        var e = assertThrows(IllegalArgumentException.class, () -> new Range(Double.class, "error", true, "blast", true));
        assertMessageContains(e);
    }

    /**
     * Tests the detection of illegal arguments at {@link Range} creation time.
     * Note that such error should never happen when parameterized types are used.
     * The check performed by the constructor is a safety in case the user bypass
     * the parameterized type check by using the raw type instead.
     *
     * <p>This test requires assertions to be enabled.</p>
     */
    @Test
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void testConstructorErrors01() {
        assertTrue(Range.class.desiredAssertionStatus());
        var e = assertThrows(IllegalArgumentException.class, () -> new Range(String.class, 123.233, true, 8740.09, true));
        assertMessageContains(e);
    }

    /**
     * Tests {@link Range#isEmpty()}.
     */
    @Test
    public void testIsEmpty() {
        assertFalse(new Range<>(Float.class, 3f,        true, 5f,        true).isEmpty());
        assertFalse(new Range<>(Float.class, 3f,        true, 3f,        true).isEmpty());
        assertTrue (new Range<>(Float.class, 3f,        true, 3f,       false).isEmpty());
        assertTrue (new Range<>(Float.class, 3f,       false, 3f,        true).isEmpty());
        assertTrue (new Range<>(Float.class, 3f,       false, 3f,       false).isEmpty());
        assertFalse(new Range<>(Float.class, Float.NaN, true, 5f,        true).isEmpty());
        assertFalse(new Range<>(Float.class, 3f,        true, Float.NaN, true).isEmpty());
        assertTrue (new Range<>(Float.class, Float.NaN, true, Float.NaN, true).isEmpty());
    }

    /**
     * Tests the {@link Range#contains(Comparable)} method.
     */
    @Test
    public void testContains() {
        final Range<Integer> range = new Range<>(Integer.class, 3, true, 5, true);
        assertTrue (range.contains(4));
        assertFalse(range.contains(6));
        assertFalse(range.contains(2));
        assertTrue (range.contains(3));
        assertTrue (range.contains(5));
    }

    /**
     * Tests the {@link Range#contains(Comparable)} method with an exclusive minimal value.
     */
    @Test
    public void testContainsNotInclusiveMinimum() {
        final Range<Integer> range = new Range<>(Integer.class, 2, false, 5, true);
        assertTrue (range.contains(4));
        assertFalse(range.contains(6));
        assertFalse(range.contains(2));
        assertTrue (range.contains(3));
        assertTrue (range.contains(5));

    }

    /**
     * Tests the {@link Range#contains(Comparable)} method with an exclusive maximal value.
     */
    @Test
    public void testContainsNotInclusiveMaximum() {
        final Range<Integer> range = new Range<>(Integer.class, 3, true, 6, false);
        assertTrue (range.contains(4));
        assertFalse(range.contains(6));
        assertFalse(range.contains(2));
        assertTrue (range.contains(3));
        assertTrue (range.contains(5));
    }

    /**
     * Tests the {@link Range#contains(Comparable)} method without lower endpoint.
     */
    @Test
    public void testContainsNoLowerEndpoint() {
        final Range<Integer> range = new Range<>(Integer.class, null, true, 5, true);
        assertTrue (range.contains(-555));
        assertTrue (range.contains(5));
        assertFalse(range.contains(6));
    }

    /**
     * Tests the {@link Range#contains(Comparable)} method without upper endpoint.
     */
    @Test
    public void testContainsNoUpperEndpoint() {
        final Range<Integer> range = new Range<>(Integer.class, 3, true, null, true);
        assertFalse(range.contains(1));
        assertTrue (range.contains(3));
        assertTrue (range.contains(10000));
    }

    /**
     * Tests the {@link Range#contains(Comparable)} method without lower or upper endpoints.
     */
    @Test
    public void testContainsNoEndpoints() {
        final Range<Integer> range = new Range<>(Integer.class, null, true, null, true);
        assertTrue(range.contains(-55555));
        assertTrue(range.contains(100000));
    }

    /**
     * Tests the {@link Range#contains(Range)} method.
     */
    @Test
    public void testContainsRange() {
        final Range<Integer> range  = new Range<>(Integer.class, -10, true, 10, true);
        final Range<Integer> inside = new Range<>(Integer.class,  -5, true,  5, true);

        assertTrue(range.contains(inside));
        assertFalse(inside.contains(range));
    }

    /**
     * Tests the {@link Range#contains(Range)} method without lower endpoint.
     */
    @Test
    public void testContainsRangeNoLowerEndpoint() {
        final Range<Integer> range  = new Range<>(Integer.class,  null, true, 500, true);
        final Range<Integer> inside = new Range<>(Integer.class, -2500, true, 305, true);

        assertTrue (range.contains(inside));
        assertFalse(inside.contains(range));
    }

    /**
     * Tests the {@link Range#contains(Range)} method without upper endpoint.
     */
    @Test
    public void testContainsRangeNoUpperEndpoint() {
        final Range<Integer> range  = new Range<>(Integer.class, -2500, true, null, true);
        final Range<Integer> inside = new Range<>(Integer.class,    17, true,  305, true);

        assertTrue(range.contains(inside));
        assertFalse(inside.contains(range));
    }

    /**
     * Tests the {@link Range#intersects(Range)} method.
     */
    @Test
    public void testIntersects() {
        final Range<Character> range1 = new Range<>(Character.class, 'a', true, 'g', true);
        final Range<Character> range2 = new Range<>(Character.class, 'c', true, 'm', true);
        final Range<Character> range3 = new Range<>(Character.class, 'o', true, 'z', true);

        assertTrue (range1.intersects(range2));
        assertTrue (range2.intersects(range1));
        assertFalse(range1.intersects(range3));
        assertFalse(range3.intersects(range1));
    }

    /**
     * Tests the {@link Range#intersect(Range)} method.
     */
    @Test
    @SuppressWarnings("UnnecessaryBoxing")
    public void testIntersection() {
        final Range<Integer> range1 = new Range<>(Integer.class, 1, true, 5, true);
        final Range<Integer> range2 = new Range<>(Integer.class, 4, true, 6, true);

        final Range<?> intersection = range1.intersect(range2);
        assertEquals(Integer.class, intersection.getElementType());
        assertEquals(Integer.valueOf(4), intersection.getMinValue());
        assertEquals(Integer.valueOf(5), intersection.getMaxValue());
    }

    /**
     * Tests the {@link Range#intersect(Range)} method with arguments resulting in empty range.
     */
    @Test
    public void testIntersectionOfNonIntersectingRanges() {
        final Range<Integer> range1 = new Range<>(Integer.class, 1, true,  5, true);
        final Range<Integer> range2 = new Range<>(Integer.class, 8, true, 10, true);

        final Range<?> intersection = range1.intersect(range2);
        assertEquals(Integer.class, intersection.getElementType());
        assertTrue(intersection.isEmpty());
    }

    /**
     * Tests the {@link Range#union(Range)} method.
     */
    @Test
    @SuppressWarnings("UnnecessaryBoxing")
    public void testUnion() {
        final Range<Character> range1 = new Range<>(Character.class, 'a', true, 'f', true);
        final Range<Character> range2 = new Range<>(Character.class, 'd', true, 'h', true);

        final Range<?> union = range1.union(range2);
        assertFalse(union.isEmpty());
        assertEquals(Character.valueOf('a'), union.getMinValue());
        assertEquals(Character.valueOf('h'), union.getMaxValue());
    }

    /**
     * Tests the {@link Range#union(Range)} method with disjoint ranges.
     */
    @Test
    @SuppressWarnings("UnnecessaryBoxing")
    public void testDisjointUnion() {
        final Range<Character> range1 = new Range<>(Character.class, 'a', true, 'f', true);
        final Range<Character> range2 = new Range<>(Character.class, 'm', true, 'v', true);

        final Range<?> unionRange = range1.union(range2);
        assertFalse(unionRange.isEmpty());
        assertEquals(Character.valueOf('a'), unionRange.getMinValue());
        assertEquals(Character.valueOf('v'), unionRange.getMaxValue());
    }

    /**
     * Tests the {@link Range#subtract(Range)} method.
     */
    @Test
    public void testSubtract() {
        final Range<Integer> range1 = new Range<>(Integer.class, 10, true, 40, true);
        final Range<Integer> range2 = new Range<>(Integer.class, 20, true, 25, true);
        final Range<Integer>[] subtract = range1.subtract(range2);
        assertEquals(2, subtract.length);
        assertEquals(new Range<>(Integer.class, 10, true,  20, false), subtract[0]);
        assertEquals(new Range<>(Integer.class, 25, false, 40, true),  subtract[1]);
    }

    /**
     * Tests the {@link Range#toString()} method.
     */
    @Test
    public void testToString() {
        assertEquals("{}",        new Range<>(Integer.class, 10, false, 10, false).toString());
        assertEquals("{10}",      new Range<>(Integer.class, 10, true,  10, true ).toString());
        assertEquals("[10 … 20]", new Range<>(Integer.class, 10, true,  20, true ).toString());
        assertEquals("(10 … 20)", new Range<>(Integer.class, 10, false, 20, false).toString());
        assertEquals("[10 … 20)", new Range<>(Integer.class, 10, true,  20, false).toString());
        assertEquals("(10 … 20]", new Range<>(Integer.class, 10, false, 20, true ).toString());
    }

    /**
     * Tests the {@link Range#formatTo(Formatter, int, int, int)} method.
     */
    @Test
    public void testFormatTo() {
        final Range<Integer> range = new Range<>(Integer.class, 10, true,  20, false);
        assertEquals("[10 … 20)",    String.format(Locale.CANADA, "%s", range));
        assertEquals("[10 … 20)",    String.format(Locale.CANADA, "%4s", range));
        assertEquals("[10 … 20)   ", String.format(Locale.CANADA, "%-12s", range));
        assertEquals("   [10 … 20)", String.format(Locale.CANADA, "%12s", range));
        assertEquals("[10 … 20[   ", String.format(Locale.CANADA, "%#-12s", range));
    }

    /**
     * Tests the {@link Range#formatTo(Formatter, int, int, int)} method with a range of instants.
     *
     * @see RangeFormatTest#testFormatInstant()
     */
    @Test
    public void testFormatInstantTo() {
        final Range<Instant> range = new Range<>(Instant.class,
                Instant.parse("2019-12-23T06:00:00Z"), true,
                Instant.parse("2020-05-14T18:00:00Z"), true);
        /*
         * The result depends on time zone and we have no API for forcing the time zone to some specific
         * value in a call to 'Range.formatTo(Formatter, …)`. So we verify only the year and the month.
         */
        final String result = String.format(Locale.CANADA_FRENCH, "%s", range);
        assertTrue(result.matches("\\[2019-12-.+ … 2020-05-.+\\]"), result);
    }

    /**
     * Tests the {@link Range#equals(Object)} method.
     */
    @Test
    public void testEquality() {
        // Positive test - success case
        final Range<Character> range1 = new Range<>(Character.class, 'a', true, 'f', true);
        final Range<Character> range2 = new Range<>(Character.class, 'a', true, 'f', true);
        assertTrue(range1.equals(range2));

        // Positive test - failure case
        final Range<Character> range3 = new Range<>(Character.class, 'a', true, 'g', true);
        assertFalse(range1.equals(range3));

        // Failure due to type incompatibility
        final Range<String> range4 = new Range<>(String.class, "a", true, "g", true);
        assertFalse(range3.equals(range4));

        final Range<Character> range5 = new Range<>(Character.class, 'g', true, 'a', true);
        final Range<Character> range6 = new Range<>(Character.class, 'g', true, 'a', true);
        assertTrue(range5.isEmpty());
        assertTrue(range6.isEmpty());
        assertTrue(range5.equals(range6));
    }

    /**
     * Tests serialization.
     */
    @Test
    public void testSerialization() {
        final Range<Integer> range  = new Range<>(Integer.class, -10, true, 10, true);
        assertNotSame(range, assertSerializedEquals(range));
    }
}
