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

import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import org.apache.sis.referencing.datum.AbstractDatum;
import org.apache.sis.xml.bind.Context;
import org.apache.sis.xml.bind.referencing.Code;

// Test dependencies
import org.junit.Test;
import org.opengis.test.Validators;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestCase;

import static org.apache.sis.metadata.iso.citation.Citations.EPSG;
import static org.junit.Assert.*;
import static org.apache.sis.test.Assertions.assertSerializedEquals;
import static org.apache.sis.test.TestUtilities.getSingleton;

// Specific to the main and geoapi-3.1 branches:
import org.opengis.referencing.ReferenceIdentifier;


/**
 * Tests the {@link AbstractIdentifiedObject} class.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 */
@DependsOn({
    IdentifiedObjectsTest.class, NamedIdentifierTest.class,
    org.apache.sis.xml.bind.referencing.CodeTest.class
})
public final class AbstractIdentifiedObjectTest extends TestCase {
    /**
     * Creates a new test case.
     */
    public AbstractIdentifiedObjectTest() {
    }

    /**
     * Creates a map of properties to be given to the {@link AbstractIdentifiedObject} constructor.
     * The values in the map are consistent with the values expected by the {@link #validate} method.
     *
     * @param  identifiers  the value for the {@code "identifiers"} property.
     */
    private static Map<String,Object> properties(final Set<ReferenceIdentifier> identifiers) {
        final Map<String,Object> properties = new HashMap<>(8);
        assertNull(properties.put("name",       "GRS 1980"));
        assertNull(properties.put("identifiers", identifiers.toArray(ReferenceIdentifier[]::new)));
        assertNull(properties.put("codespace",  "EPSG"));
        assertNull(properties.put("version",    "8.3"));
        assertNull(properties.put("alias",      "International 1979"));
        assertNull(properties.put("remarks",    "Adopted by IUGG 1979 Canberra"));
        assertNull(properties.put("remarks_fr", "Adopté par IUGG 1979 Canberra"));
        return properties;
    }

    /**
     * Validates the given object created by the test methods.
     *
     * @param  object       the object to validate.
     * @param  identifiers  the expected value of {@link AbstractIdentifiedObject#getIdentifiers()}.
     * @param  gmlID        the expected value of {@link AbstractIdentifiedObject#getID()}.
     * @return the value of {@link AbstractIdentifiedObject#getIdentifier()}.
     */
    private static ReferenceIdentifier validate(final AbstractIdentifiedObject object,
            final Set<ReferenceIdentifier> identifiers, final String gmlID)
    {
        Validators.validate(object);
        final ReferenceIdentifier name = object.getName();
        assertEquals("name",        "GRS 1980",                      name.getCode());
        assertEquals("codespace",   "EPSG",                          name.getCodeSpace());
        assertEquals("version",     "8.3",                           name.getVersion());
        assertEquals("aliases",     "International 1979",            getSingleton(object.getAlias()).toString());
        assertEquals("names",       name,                            getSingleton(object.getNames()));
        assertEquals("identifiers", identifiers,                     object.getIdentifiers());
        assertEquals("ID",          gmlID,                           object.getID());
        assertEquals("remarks",     "Adopted by IUGG 1979 Canberra", object.getRemarks().toString(Locale.ENGLISH));
        assertEquals("remarks_fr",  "Adopté par IUGG 1979 Canberra", object.getRemarks().toString(Locale.FRENCH));
        final Code code = object.getIdentifier();
        return (code != null) ? code.getIdentifier() : null;
    }

    /**
     * Tests the {@link AbstractIdentifiedObject#AbstractIdentifiedObject(Map)} constructor without name.
     * This is invalid and should throw an exception.
     */
    @Test
    @SuppressWarnings("ResultOfObjectAllocationIgnored")
    public void testMissingName() {
        final Map<String,Object> properties = new HashMap<>(4);
        assertNull(properties.put(AbstractIdentifiedObject.REMARKS_KEY, "Not a name."));
        try {
            new AbstractIdentifiedObject(properties);
            fail("Should not allow unnamed object.");
        } catch (IllegalArgumentException e) {
            /*
             * The message may be in any language, but shall
             * contain at least the missing property name.
             */
            final String message = e.getMessage();
            assertTrue(message, message.contains("name"));
        }
        // Try again, with error messages forced to English.
        assertNull(properties.put(AbstractIdentifiedObject.LOCALE_KEY, Locale.US));
        try {
            new AbstractIdentifiedObject(properties);
            fail("Should not allow unnamed object.");
        } catch (IllegalArgumentException e) {
            assertEquals("Missing value for “name” property.", e.getMessage());
        }
        // "code" with String value is accepted as well.
        assertNull(properties.put("code", "Test"));
        assertEquals("Test", new AbstractIdentifiedObject(properties).getName().getCode());
    }

    /**
     * Tests the {@link AbstractIdentifiedObject#AbstractIdentifiedObject(Map)} constructor without identifier.
     * This method compares the property values against the expected values.
     */
    @Test
    public void testWithoutIdentifier() {
        final Set<ReferenceIdentifier> identifiers = Set.of();
        final AbstractIdentifiedObject object      = new AbstractIdentifiedObject(properties(identifiers));
        final ReferenceIdentifier      gmlId       = validate(object, identifiers, "GRS1980");
        assertNull("gmlId", gmlId);
    }

    /**
     * Tests the {@link AbstractIdentifiedObject#AbstractIdentifiedObject(Map)} constructor
     * with only one identifier. The methods of interest for this test are:
     *
     * <ul>
     *   <li>{@link AbstractIdentifiedObject#getIdentifiers()}</li>
     *   <li>{@link AbstractIdentifiedObject#getIdentifier()}</li>
     *   <li>{@link AbstractIdentifiedObject#getID()}</li>
     * </ul>
     */
    @Test
    @DependsOnMethod("testWithoutIdentifier")
    public void testWithSingleIdentifier() {
        final ReferenceIdentifier      identifier  = new ImmutableIdentifier(null, "EPSG", "7019");
        final Set<ReferenceIdentifier> identifiers = Set.of(identifier);
        final AbstractIdentifiedObject object      = new AbstractIdentifiedObject(properties(identifiers));
        final ReferenceIdentifier      gmlId       = validate(object, identifiers, "epsg-7019");
        assertNotNull("gmlId",                   gmlId);
        assertEquals ("gmlId.codespace", "EPSG", gmlId.getCodeSpace());
        assertEquals ("gmlId.code",      "7019", gmlId.getCode());
    }

    /**
     * Tests the {@link AbstractIdentifiedObject#AbstractIdentifiedObject(Map)} constructor
     * with more than one identifier. This method also tries a different identifier implementation
     * than the one used by {@link #testWithSingleIdentifier()}.
     */
    @Test
    @DependsOnMethod("testWithSingleIdentifier")
    public void testWithManyIdentifiers() {
        final Set<ReferenceIdentifier> identifiers = new LinkedHashSet<>(4);
        assertTrue(identifiers.add(new NamedIdentifier(EPSG, "7019")));
        assertTrue(identifiers.add(new NamedIdentifier(EPSG, "IgnoreMe")));
        final AbstractIdentifiedObject object = new AbstractIdentifiedObject(properties(identifiers));
        final ReferenceIdentifier      gmlId  = validate(object, identifiers, "epsg-7019");
        assertNotNull("gmlId",                   gmlId);
        assertEquals ("gmlId.codespace", "EPSG", gmlId.getCodeSpace());
        assertEquals ("gmlId.code",      "7019", gmlId.getCode());
    }

    /**
     * Tests {@link AbstractIdentifiedObject#getIdentifier()} with a sub-type of {@code AbstractIdentifiedObject}.
     * The use of a subtype will allow {@code getIdentifier()} to build a URN and {@code getId()} to know what to
     * insert between {@code "epsg-"} and the code.
     */
    @Test
    @DependsOnMethod("testWithManyIdentifiers")
    public void testAsSubtype() {
        final ReferenceIdentifier      identifier  = new NamedIdentifier(EPSG, "7019");
        final Set<ReferenceIdentifier> identifiers = Set.of(identifier);
        final AbstractIdentifiedObject object      = new AbstractDatum(properties(identifiers));
        final ReferenceIdentifier      gmlId       = validate(object, identifiers, "epsg-datum-7019");
        assertNotNull("gmlId",                   gmlId);
        assertEquals ("gmlId.codespace", "EPSG", gmlId.getCodeSpace());
        assertEquals ("gmlId.code",      "7019", gmlId.getCode());
    }

    /**
     * Tests two {@code AbstractIdentifiedObject} declaring the same identifier in the same XML document.
     * The {@code getID()} method should detect the collision and select different identifier.
     */
    @Test
    @DependsOnMethod("testWithManyIdentifiers")
    public void testIdentifierCollision() {
        final Map<String,Object> properties = new HashMap<>(4);
        assertNull(properties.put("name", "GRS 1980"));
        assertNull(properties.put("identifiers", new NamedIdentifier(EPSG, "7019")));
        final AbstractIdentifiedObject o1 = new AbstractIdentifiedObject(properties);
        final AbstractIdentifiedObject o2 = new AbstractIdentifiedObject(properties);
        final AbstractIdentifiedObject o3 = new AbstractIdentifiedObject(properties);
        final AbstractIdentifiedObject o4 = new AbstractIdentifiedObject(properties);
        final Context context = new Context(0, null, null, null, null, null, null, null, null);
        try {
            final String c1, c2, c3, c4;
            assertEquals("o1", "epsg-7019", c1 = o1.getID());
            assertEquals("o2", "GRS1980",   c2 = o2.getID());
            assertEquals("o3", "GRS1980-1", c3 = o3.getID());
            assertEquals("o4", "GRS1980-2", c4 = o4.getID());
            assertSame  ("o1", c1, o1.getID());  // Verify that values are remembered.
            assertSame  ("o2", c2, o2.getID());
            assertSame  ("o3", c3, o3.getID());
            assertSame  ("o4", c4, o4.getID());
        } finally {
            context.finish();
        }
    }

    /**
     * Tests serialization.
     */
    @Test
    @DependsOnMethod("testWithoutIdentifier")
    public void testSerialization() {
        final Set<ReferenceIdentifier> identifiers = Set.of();
        final AbstractIdentifiedObject object = new AbstractIdentifiedObject(properties(identifiers));
        final AbstractIdentifiedObject actual = assertSerializedEquals(object);
        assertNotSame(object, actual);
        assertNull("gmlId", validate(actual, identifiers, "GRS1980"));
    }
}
