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

import java.util.Map;
import java.util.Collection;
import java.util.AbstractMap.SimpleEntry;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestCase;
import org.junit.Test;

import static org.apache.sis.test.Assert.*;


/**
 * Tests {@link CharacteristicMap} indirectly, through {@link AbstractAttribute} construction.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.5
 * @since   0.5
 * @module
 */
@DependsOn(CharacteristicTypeMapTest.class)
public final strictfp class CharacteristicMapTest extends TestCase {
    /**
     * Creates an attribute for a temperature measurement of 20°C with a ±0.1°C accuracy.
     *
     * @return a {@code "temperature"} attribute with two characteristics: {@code "accuracy"} and {@code "units"}.
     */
    public static AbstractAttribute<Float> temperature() {
        return new SingletonAttribute<>(CharacteristicTypeMapTest.temperature(), 20f);
    }

    /**
     * Creates an attribute for the given characteristic.
     *
     * @param  temperature  the attribute created by {@link #temperature()}.
     * @param  name         either {@code "accuracy"} or {@code "units"}.
     * @return an attribute for the given name.
     */
    private static AbstractAttribute<?> create(final AbstractAttribute<?> temperature, final String name) {
        return new SingletonAttribute<>(((DefaultAttributeType<?>) temperature.getType()).characteristics().get(name));
    }

    /**
     * Tests adding explicitly a characteristic with {@code CharacteristicMap.put(String, Attribute)}.
     */
    @Test
    public void testPut() {
        final AbstractAttribute<?> temperature = temperature();
        final AbstractAttribute<?> units       = create(temperature, "units");
        final AbstractAttribute<?> accuracy    = create(temperature, "accuracy");
        final Map<String,AbstractAttribute<?>> characteristics = temperature.characteristics();
        /*
         * Verify that the map is initially empty.
         */
        assertTrue  ("isEmpty",       characteristics.isEmpty());
        assertEquals("size", 0,       characteristics.size());
        assertFalse ("containsKey",   characteristics.containsKey("units"));
        assertFalse ("containsKey",   characteristics.containsKey("accuracy"));
        assertFalse ("containsKey",   characteristics.containsKey("temperature"));
        assertNull  ("get",           characteristics.get("units"));
        assertNull  ("get",           characteristics.get("accuracy"));
        assertNull  ("get",           characteristics.get("temperature"));
        assertFalse ("containsValue", characteristics.containsValue(units));
        assertFalse ("containsValue", characteristics.containsValue(accuracy));
        assertFalse ("containsValue", characteristics.containsValue(temperature));
        /*
         * Store "units" characteristic and verify.
         */
        assertNull  ("put",           characteristics.put("units", units));
        assertFalse ("isEmpty",       characteristics.isEmpty());
        assertEquals("size", 1,       characteristics.size());
        assertTrue  ("containsKey",   characteristics.containsKey("units"));
        assertFalse ("containsKey",   characteristics.containsKey("accuracy"));
        assertFalse ("containsKey",   characteristics.containsKey("temperature"));
        assertSame  ("get", units,    characteristics.get("units"));
        assertNull  ("get",           characteristics.get("accuracy"));
        assertNull  ("get",           characteristics.get("temperature"));
        assertTrue  ("containsValue", characteristics.containsValue(units));
        assertFalse ("containsValue", characteristics.containsValue(accuracy));
        assertFalse ("containsValue", characteristics.containsValue(temperature));
        /*
         * Store "accuracy" characteristic and verify.
         */
        assertNull  ("put",           characteristics.put("accuracy", accuracy));
        assertFalse ("isEmpty",       characteristics.isEmpty());
        assertEquals("size", 2,       characteristics.size());
        assertTrue  ("containsKey",   characteristics.containsKey("units"));
        assertTrue  ("containsKey",   characteristics.containsKey("accuracy"));
        assertFalse ("containsKey",   characteristics.containsKey("temperature"));
        assertSame  ("get", units,    characteristics.get("units"));
        assertSame  ("get", accuracy, characteristics.get("accuracy"));
        assertNull  ("get",           characteristics.get("temperature"));
        assertTrue  ("containsValue", characteristics.containsValue(units));
        assertTrue  ("containsValue", characteristics.containsValue(accuracy));
        assertFalse ("containsValue", characteristics.containsValue(temperature));
        /*
         * Overwrite values. Map shall stay unchanged.
         */
        assertSame  ("put",  accuracy, characteristics.put("accuracy", accuracy));
        assertSame  ("put",  units,    characteristics.put("units",    units));
        assertEquals("size", 2,        characteristics.size());
        /*
         * Try putting an attribute for a non-existent name.
         * Map shall stay unchanged.
         */
        try {
            characteristics.put("dummy", units);
            fail("Operation shall not have been allowed.");
        } catch (IllegalArgumentException e) {
            final String message = e.getMessage();
            assertTrue(message, message.contains("dummy"));       // Message shall contain the wrong name.
            assertTrue(message, message.contains("temperature")); // Message shall contain the enclosing attribute name.
        }
        /*
         * Try putting an attribute of the wrong type.
         * Map shall stay unchanged.
         */
        try {
            characteristics.put("units", accuracy);
            fail("Operation shall not have been allowed.");
        } catch (IllegalArgumentException e) {
            final String message = e.getMessage();
            assertTrue(message, message.contains("accuracy"));
            assertTrue(message, message.contains("temperature:units"));
        }
        assertEntriesEqual(units, accuracy, characteristics);
        assertSame(characteristics, temperature.characteristics());
    }

    /**
     * Tests adding a characteristic indirectly with {@code CharacteristicMap.addValue(Attribute)}.
     */
    @Test
    @DependsOnMethod("testPut")
    public void testAddValue() {
        final AbstractAttribute<?> temperature = temperature();
        final AbstractAttribute<?> units       = create(temperature, "units");
        final AbstractAttribute<?> accuracy    = create(temperature, "accuracy");
        final Map<String,AbstractAttribute<?>> characteristics = temperature.characteristics();
        final Collection<AbstractAttribute<?>> values          = characteristics.values();
        /*
         * Verify that the collection is initially empty.
         */
        assertTrue  ("isEmpty",  values.isEmpty());
        assertEquals("size", 0,  values.size());
        assertFalse ("contains", values.contains(units));
        assertFalse ("contains", values.contains(accuracy));
        assertFalse ("contains", values.contains(temperature));
        /*
         * Store "units" characteristic and verify.
         */
        assertTrue  ("add",      values.add(units));
        assertFalse ("isEmpty",  values.isEmpty());
        assertEquals("size", 1,  values.size());
        assertTrue  ("contains", values.contains(units));
        assertFalse ("contains", values.contains(accuracy));
        assertFalse ("contains", values.contains(temperature));
        /*
         * Store "accuracy" characteristic and verify.
         */
        assertTrue  ("add",      values.add(accuracy));
        assertFalse ("isEmpty",  values.isEmpty());
        assertEquals("size", 2,  values.size());
        assertTrue  ("contains", values.contains(units));
        assertTrue  ("contains", values.contains(accuracy));
        assertFalse ("contains", values.contains(temperature));
        /*
         * Overwrite values. Map shall stay unchanged.
         */
        assertFalse ("add",     values.add(accuracy));
        assertFalse ("add",     values.add(units));
        assertEquals("size", 2, values.size());
        /*
         * Try adding an attribute of the wrong type.
         * Map shall stay unchanged.
         */
        try {
            values.add(temperature);
            fail("Operation shall not have been allowed.");
        } catch (IllegalArgumentException e) {
            final String message = e.getMessage();
            assertTrue(message, message.contains("temperature"));
        }
        assertEntriesEqual(units, accuracy, characteristics);
        assertSame(characteristics, temperature.characteristics());
    }

    /**
     * Tests adding a characteristic indirectly with {@link CharacteristicMap#addKey(String)}.
     */
    @Test
    @DependsOnMethod("testPut")
    public void testAddKey() {
        final AbstractAttribute<?> units, accuracy;
        final AbstractAttribute<?> temperature = temperature();
        final Map<String,AbstractAttribute<?>> characteristics = temperature.characteristics();
        final Collection<String> keys = characteristics.keySet();
        /*
         * Verify that the collection is initially empty.
         */
        assertTrue  ("isEmpty",  keys.isEmpty());
        assertEquals("size", 0,  keys.size());
        assertFalse ("contains", keys.contains("units"));
        assertFalse ("contains", keys.contains("accuracy"));
        assertFalse ("contains", keys.contains("temperature"));
        /*
         * Store "units" characteristic and verify.
         */
        assertTrue   ("add",      keys.add("units"));
        assertFalse  ("isEmpty",  keys.isEmpty());
        assertEquals ("size", 1,  keys.size());
        assertTrue   ("contains", keys.contains("units"));
        assertFalse  ("contains", keys.contains("accuracy"));
        assertFalse  ("contains", keys.contains("temperature"));
        assertNotNull("get",      units = characteristics.get("units"));
        /*
         * Store "accuracy" characteristic and verify.
         */
        assertTrue  ("add",       keys.add("accuracy"));
        assertFalse ("isEmpty",   keys.isEmpty());
        assertEquals("size", 2,   keys.size());
        assertTrue  ("contains",  keys.contains("units"));
        assertTrue  ("contains",  keys.contains("accuracy"));
        assertFalse ("contains",  keys.contains("temperature"));
        assertNotNull("get",      accuracy = characteristics.get("accuracy"));
        /*
         * Overwrite values. Map shall stay unchanged.
         */
        assertFalse ("add",     keys.add("accuracy"));
        assertFalse ("add",     keys.add("units"));
        assertEquals("size", 2, keys.size());
        /*
         * Try adding an attribute of the wrong type.
         * Map shall stay unchanged.
         */
        try {
            keys.add("dummy");
            fail("Operation shall not have been allowed.");
        } catch (IllegalArgumentException e) {
            final String message = e.getMessage();
            assertTrue(message, message.contains("temperature"));
            assertTrue(message, message.contains("dummy"));
        }
        assertEntriesEqual(units, accuracy, characteristics);
        assertSame(characteristics, temperature.characteristics());
    }

    /**
     * Verifies that the given characteristics map contains entries for the given attributes.
     *
     * @param  units            the first expected value in iteration order.
     * @param  accuracy         the second expected value in iteration order.
     * @param  characteristics  the map to verify.
     */
    private static void assertEntriesEqual(final AbstractAttribute<?> units, final AbstractAttribute<?> accuracy,
            final Map<String,AbstractAttribute<?>> characteristics)
    {
        assertArrayEquals("keySet", new String[] {"accuracy", "units"}, characteristics.keySet().toArray());
        assertArrayEquals("values", new Object[] { accuracy ,  units }, characteristics.values().toArray());
        assertArrayEquals("entrySet", new Object[] {
                new SimpleEntry<>("accuracy", accuracy),
                new SimpleEntry<>("units",    units)
            }, characteristics.entrySet().toArray());
    }

    /**
     * Sets the accuracy characteristic in the given attribute.
     *
     * @param  temperature  the attribute where to set the accuracy.
     * @param  isFirstTime  {@code true} if the accuracy value is set for the first time.
     * @param  value        the new accuracy value.
     */
    private static void setAccuracy(final AbstractAttribute<Float> temperature, final boolean isFirstTime, final float value) {
        assertEquals("keySet.add", isFirstTime, temperature.characteristics().keySet().add("accuracy"));
        final AbstractAttribute<Float> accuracy = Features.cast(temperature.characteristics().get("accuracy"), Float.class);
        accuracy.setValue(value);
    }

    /**
     * Tests {@link CharacteristicMap#equals(Object)} and (opportunistically) {@link CharacteristicMap#clone()}
     */
    @Test
    @DependsOnMethod("testAddKey")
    public void testEquals() {
        final AbstractAttribute<Float> t1 = temperature();
        final AbstractAttribute<Float> t2 = temperature();
        assertEquals("equals",   t1, t2);
        assertEquals("hashCode", t1.hashCode(), t2.hashCode());
        setAccuracy(t1, true, 0.2f);
        assertFalse("equals",   t1.equals(t2));
        assertFalse("equals",   t2.equals(t1));
        assertFalse("hashCode", t1.hashCode() == t2.hashCode());
        setAccuracy(t2, true, 0.3f);
        assertFalse("equals",   t1.equals(t2));
        assertFalse("equals",   t2.equals(t1));
        assertFalse("hashCode", t1.hashCode() == t2.hashCode());
        setAccuracy(t1, false, 0.3f);
        assertEquals("equals",   t1, t2);
        assertEquals("hashCode", t1.hashCode(), t2.hashCode());
    }

    /**
     * Tests the reconstruction of {@link CharacteristicTypeMap} after serialization.
     */
    @Test
    @DependsOnMethod({"testEquals", "testAddValue"})            // Implementation of readObject use values().addAll(…).
    public void testSerialization() {
        final AbstractAttribute<Float> temperature = temperature();
        setAccuracy(temperature, true, 0.2f);

        final AbstractAttribute<Float> unserialized = assertSerializedEquals(temperature);
        assertNotSame(temperature, unserialized);
        assertNotSame(temperature.characteristics(), unserialized.characteristics());
        assertSame(((DefaultAttributeType<?>) temperature .getType()).characteristics(),
                   ((DefaultAttributeType<?>) unserialized.getType()).characteristics());
    }
}
