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

import java.util.Random;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.time.LocalDate;
import org.opengis.metadata.citation.Citation;
import org.opengis.metadata.citation.DateType;
import org.opengis.metadata.citation.PresentationForm;
import org.apache.sis.metadata.iso.citation.DefaultCitation;
import org.apache.sis.metadata.iso.citation.DefaultCitationDate;
import org.apache.sis.util.SimpleInternationalString;
import org.apache.sis.util.collection.DefaultTreeTable;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable;

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

// Specific to the main branch:
import java.util.Date;


/**
 * Tests the {@link TreeNodeChildren} class.
 * Unless otherwise specified, all tests use the {@link MetadataStandard#ISO_19115} constant.
 *
 * <h2>Test dependency</h2>
 * This class uses the {@link TreeNode#getUserObject()} method for comparing the values.
 * We can hardly avoid to use some {@code TreeNode} methods because of the cross-dependencies.
 * However, we try to use nothing else than {@code getUserObject()} because the purpose of this
 * class is not to test {@link TreeNode}.
 *
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class TreeNodeChildrenTest extends TestCase {
    /**
     * Creates a new test case.
     */
    public TreeNodeChildrenTest() {
    }

    /**
     * Creates a shallow metadata object without collections.
     *
     * <pre class="text">
     *   DefaultCitation
     *     ├─Title………………………………………………… Some title
     *     ├─Edition…………………………………………… Some edition
     *     └─Other citation details…… Some other details</pre>
     */
    static DefaultCitation metadataWithoutCollections() {
        final DefaultCitation citation = new DefaultCitation("Some title");
        citation.setEdition(new SimpleInternationalString("Some edition"));
        citation.setOtherCitationDetails(new SimpleInternationalString("Some other details"));
        return citation;
    }

    /**
     * Creates a shallow metadata object with singleton value in collections.
     * This method creates the following metadata:
     *
     * <pre class="text">
     *   DefaultCitation
     *     ├─Title………………………………………………… Some title
     *     ├─Alternate title……………………… First alternate title
     *     ├─Edition…………………………………………… Some edition
     *     ├─Presentation form………………… Map digital
     *     └─Other citation details…… Some other details</pre>
     */
    static DefaultCitation metadataWithSingletonInCollections() {
        final DefaultCitation citation = metadataWithoutCollections();
        assertTrue(citation.getAlternateTitles().add(new SimpleInternationalString("First alternate title")));
        assertTrue(citation.getPresentationForms().add(PresentationForm.MAP_DIGITAL));
        return citation;
    }

    /**
     * Creates a shallow metadata object with multi-occurrences (i.e. more than one value in collections).
     * This method creates the following metadata:
     *
     * <pre class="text">
     *   DefaultCitation
     *     ├─Title………………………………………………………… Some title
     *     ├─Alternate title (1 of 2)……… First alternate title
     *     ├─Alternate title (2 of 2)……… Second alternate title
     *     ├─Edition…………………………………………………… Some edition
     *     ├─Presentation form (1 of 2)… Map digital
     *     ├─Presentation form (2 of 2)… map hardcopy
     *     └─Other citation details…………… Some other details</pre>
     */
    static DefaultCitation metadataWithMultiOccurrences() {
        final DefaultCitation citation = metadataWithSingletonInCollections();
        assertTrue(citation.getAlternateTitles().add(new SimpleInternationalString("Second alternate title")));
        assertTrue(citation.getPresentationForms().add(PresentationForm.MAP_HARDCOPY));
        return citation;
    }

    /**
     * Creates a metadata object with a property that can be simplified.
     * Strictly speaking, the metadata is:
     *
     * <pre class="text">
     *   DefaultCitation
     *     └─Date
     *        ├─Date………………… 2012-01-01
     *        └─Date type…… Creation</pre>
     *
     * However, the tree view should simplify as:
     *
     * <pre class="text">
     *   DefaultCitation
     *     └─Date………………………… 2012-01-01
     *        └─Date type…… Creation</pre>
     *
     * @see <a href="https://issues.apache.org/jira/browse/SIS-298">SIS-298</a>
     */
    static DefaultCitation metadataSimplifiable() {
        final DefaultCitation citation = new DefaultCitation();
        final DefaultCitationDate date = new DefaultCitationDate(LocalDate.of(2012, 1, 1), DateType.CREATION);
        assertTrue(citation.getDates().add(date));
        return citation;
    }

    /**
     * Creates a collection to be tested for the given metadata object and value policy.
     */
    private static TreeNodeChildren create(final DefaultCitation citation, final ValueExistencePolicy valuePolicy) {
        final MetadataStandard standard = MetadataStandard.ISO_19115;
        final TreeTableView    table    = new TreeTableView(standard, citation, Citation.class, valuePolicy);
        final TreeNode         node     = (TreeNode) table.getRoot();
        final PropertyAccessor accessor = standard.getAccessor(new CacheKey(citation.getClass()), true);
        return new TreeNodeChildren(node, citation, accessor);
    }

    /**
     * Tests read-only operations on a list of properties for a shallow metadata object without collections.
     */
    @Test
    public void testReadOnlyWithoutCollections() {
        final DefaultCitation  citation = metadataWithoutCollections();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        final String[] expected = {
            "Some title",
            "Some edition",
            "Some other details"
        };
        assertEquals(-1, children.titleProperty);
        assertFalse (children.isEmpty());
        assertEquals(expected.length, children.size());
        assertAllNextEqual(expected, children.iterator());
    }

    /**
     * Tests read-only operations on a list of properties for a shallow metadata object with singleton
     * values in collections.
     */
    @Test
    public void testReadOnlyWithSingletonInCollections() {
        final DefaultCitation  citation = metadataWithSingletonInCollections();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        final String[] expected = {
            "Some title",
            "First alternate title",
            "Some edition",
            "PresentationForm[MAP_DIGITAL]",
            "Some other details"
        };
        assertEquals(-1, children.titleProperty);
        assertFalse (children.isEmpty());
        assertEquals(expected.length, children.size());
        assertAllNextEqual(expected, children.iterator());
    }

    /**
     * Tests read-only operations on a list of properties for a shallow metadata object with more
     * than one values in collections.
     */
    @Test
    public void testReadOnlyWithMultiOccurrences() {
        final DefaultCitation  citation = metadataWithMultiOccurrences();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        final String[] expected = {
            "Some title",
            "First alternate title",
            "Second alternate title",
            "Some edition",
            "PresentationForm[MAP_DIGITAL]",
            "PresentationForm[MAP_HARDCOPY]",
            "Some other details"
        };
        assertEquals(-1, children.titleProperty);
        assertFalse (children.isEmpty());
        assertEquals(expected.length, children.size());
        assertAllNextEqual(expected, children.iterator());
    }

    /**
     * Tests a metadata that can be simplified by displaying a child property value directly as the parent value.
     */
    @Test
    public void testSimplifiable() {
        final DefaultCitation  citation = metadataSimplifiable();
        /*
         *   DefaultCitation
         *     └─Date
         *        ├─Date………………… 2012-01-01
         *        └─Date type…… Creation
         *
         * We need to perform the tests on the "Date" node, not on the "DefaultCitation" node.
         */
        final TreeTable.Node node = TestUtilities.getSingleton(create(citation, ValueExistencePolicy.COMPACT));
        assertEquals(15340, ((LocalDate) node.getValue(TableColumn.VALUE)).toEpochDay());
        final TreeNodeChildren children = (TreeNodeChildren) node.getChildren();
        final String[] expected = {
            // The "Date" node should be omitted because merged with the parent "Date" node.
            "DateType[CREATION]"
        };
        assertEquals(0, children.titleProperty);
        assertFalse (children.isEmpty());
        assertEquals(expected.length, children.size());
        assertAllNextEqual(expected, children.iterator());
    }

    /**
     * Tests the {@link TreeNodeChildren#add(TreeTable.Node)} method.
     */
    @Test
    public void testAdd() {
        final DefaultCitation  citation = metadataWithMultiOccurrences();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        assertEquals(-1, children.titleProperty);

        final DefaultTreeTable.Node toAdd = new DefaultTreeTable.Node(new DefaultTreeTable(
                TableColumn.IDENTIFIER,
                TableColumn.VALUE));
        final String[] expected = {
            "Some title",
            "First alternate title",
            "Second alternate title",
            "Third alternate title",                // After addition
            "New edition",                          // After "addition" (actually change).
            "PresentationForm[IMAGE_DIGITAL]",      // After addition
            "PresentationForm[MAP_DIGITAL]",
            "PresentationForm[MAP_HARDCOPY]",
            "Some other details"
        };
        toAdd.setValue(TableColumn.IDENTIFIER, "edition");
        toAdd.setValue(TableColumn.VALUE, citation.getEdition());
        assertFalse(children.add(toAdd),"Adding the same value shall be a no-op.");
        toAdd.setValue(TableColumn.VALUE, "New edition");
        try {
            children.add(toAdd);
            fail("Setting a different value shall be refused.");
        } catch (IllegalStateException e) {
            assertTrue(e.getMessage().contains("edition"));
        }
        citation.setEdition(null);                                                  // Clears so we are allowed to add.
        assertTrue(children.add(toAdd), "Setting a new value shall be a change.");

        toAdd.setValue(TableColumn.IDENTIFIER, "presentationForm");
        toAdd.setValue(TableColumn.VALUE, PresentationForm.MAP_DIGITAL);
        assertFalse(children.add(toAdd), "Adding the same value shall be a no-op.");
        toAdd.setValue(TableColumn.VALUE, PresentationForm.IMAGE_DIGITAL);
        assertTrue(children.add(toAdd), "Adding a new value shall be a change.");

        toAdd.setValue(TableColumn.IDENTIFIER, "alternateTitle");
        toAdd.setValue(TableColumn.VALUE, "Third alternate title");
        assertTrue(children.add(toAdd), "Adding a new value shall be a change.");

        assertEquals(expected.length, children.size());
        assertAllNextEqual(expected, children.iterator());
    }

    /**
     * Tests the {@link Iterator#remove()} operation on a list of properties without collections.
     */
    @Test
    public void testRemoveWithoutCollections() {
        final DefaultCitation  citation = metadataWithoutCollections();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        assertEquals(-1, children.titleProperty);
        testRemove(TestUtilities.createRandomNumberGenerator(), children);
    }

    /**
     * Tests the {@link Iterator#remove()} operation on a list of properties with
     * collections containing only one element.
     */
    @Test
    public void testRemoveWithSingletonInCollections() {
        final DefaultCitation  citation = metadataWithSingletonInCollections();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        assertEquals(-1, children.titleProperty);
        testRemove(TestUtilities.createRandomNumberGenerator(), children);
    }

    /**
     * Tests the {@link Iterator#remove()} operation on a list of properties with
     * collections containing more than one element.
     */
    @Test
    public void testRemoveWithMultiOccurrences() {
        final DefaultCitation  citation = metadataWithSingletonInCollections();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        assertEquals(-1, children.titleProperty);
        testRemove(TestUtilities.createRandomNumberGenerator(), children);
    }

    /**
     * Tests the {@link TreeNodeChildren#clear()} method.
     */
    @Test
    public void testClear() {
        final DefaultCitation  citation = metadataWithSingletonInCollections();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
        assertEquals(-1, children.titleProperty);
        assertFalse(children.isEmpty());
        children.clear();
        assertTrue(children.isEmpty());
        assertNull(citation.getTitle());
        assertTrue(citation.getAlternateTitles().isEmpty());
    }

    /**
     * Tests the children list with the {@link ValueExistencePolicy#ALL}.
     */
    @Test
    public void testShowAll() {
        final DefaultCitation  citation = metadataWithMultiOccurrences();
        final TreeNodeChildren children = create(citation, ValueExistencePolicy.ALL);
        final String[] expected = {
            "Some title",
            "First alternate title",
            "Second alternate title",
            null, // dates (collection)
            "Some edition",
            null, // edition date
            null, // identifiers (collection)
            null, // cited responsibly parties (collection)
            "PresentationForm[MAP_DIGITAL]",
            "PresentationForm[MAP_HARDCOPY]",
            null, // series
            "Some other details",
//          null, // collective title                       -- deprecated as of ISO 19115:2014.
            null, // ISBN
            null, // ISSN
            null, // onlineResources (collection)
            null  // graphics (collection)
        };
        assertEquals(-1, children.titleProperty);
        assertFalse (children.isEmpty());
        assertEquals(expected.length, children.size());
        assertAllNextEqual(expected, children.iterator());
    }


    // ------------------------ Support methods for the above tests ------------------------

    /**
     * Returns the string representation of the user object in the given node.
     * This is the value that we are going compare in the assertion methods below.
     *
     * <p>We use only {@link TreeNode#getUserObject()}, nothing else,
     * because the purpose of this class is not to test {@link TreeNode}.</p>
     */
    private static String valueOf(final TreeTable.Node node) {
        final Object value = node.getUserObject();
        return (value != null) ? value.toString() : null;
    }

    /**
     * Asserts that the string representation of user objects of all next element are equal
     * to the expected strings.
     */
    private static void assertAllNextEqual(final String[] expected, final Iterator<TreeTable.Node> it) {
        for (final String e : expected) {
            assertTrue(it.hasNext(), e);
            assertEquals(e, valueOf(it.next()));
        }
        assertFalse(it.hasNext());
    }

    /**
     * Asserts that all next elements traversed by the {@code actual} iterator are equal
     * to the next elements traversed by {@code expected}.
     *
     * @param  expected  the iterator over expected values.
     * @param  actual    the iterator over actual values.
     */
    private static void assertAllNextEqual(final Iterator<?> expected, final Iterator<?> actual) {
        while (expected.hasNext()) {
            assertTrue(actual.hasNext());
            assertEquals(expected.next(), actual.next());
        }
        assertFalse(actual.hasNext());
    }

    /**
     * Tests the {@link Iterator#remove()} operation on the given collection of children.
     * Elements are removed randomly until the collection is empty. After each removal,
     * the remaining elements are compared with the content of a standard Java collection.
     *
     * @param  random    a random number generator.
     * @param  children  the collection from which to remove elements.
     */
    private static void testRemove(final Random random, final TreeNodeChildren children) {
        final List<TreeTable.Node> reference = new ArrayList<>(children);
        assertFalse(reference.isEmpty());
        do {
            final Iterator<TreeTable.Node> rit = reference.iterator();      // The reference iterator.
            final Iterator<TreeTable.Node> cit = children .iterator();      // The children iterator to be tested.
            while (rit.hasNext()) {
                assertTrue(cit.hasNext());
                assertSame(rit.next(), cit.next());
                if (random.nextInt(3) == 0) {                           // Remove only 1/3 of entries on each pass.
                    rit.remove();
                    cit.remove();
                    assertAllNextEqual(reference.iterator(), children.iterator());
                }
            }
        } while (!reference.isEmpty());
        assertTrue(children.isEmpty());
    }
}
