/*
 * Copyright (C) 2007 The Guava Authors
 *
 * Licensed 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 com.google.common.collect.testing.google;

import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
import static junit.framework.TestCase.fail;

import com.google.common.annotations.GwtCompatible;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.LinkedHashMultiset;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * A series of tests that support asserting that collections cannot be modified, either through
 * direct or indirect means.
 *
 * @author Robert Konigsberg
 */
@GwtCompatible
@ElementTypesAreNonnullByDefault
public class UnmodifiableCollectionTests {

  public static void assertMapEntryIsUnmodifiable(Entry<?, ?> entry) {
    try {
      // fine because the call is going to fail without modifying the entry
      @SuppressWarnings("unchecked")
      Entry<?, @Nullable Object> nullableValueEntry = (Entry<?, @Nullable Object>) entry;
      nullableValueEntry.setValue(null);
      fail("setValue on unmodifiable Map.Entry succeeded");
    } catch (UnsupportedOperationException expected) {
    }
  }

  /**
   * Verifies that an Iterator is unmodifiable.
   *
   * <p>This test only works with iterators that iterate over a finite set.
   */
  public static void assertIteratorIsUnmodifiable(Iterator<?> iterator) {
    while (iterator.hasNext()) {
      iterator.next();
      try {
        iterator.remove();
        fail("Remove on unmodifiable iterator succeeded");
      } catch (UnsupportedOperationException expected) {
      }
    }
  }

  /**
   * Asserts that two iterators contain elements in tandem.
   *
   * <p>This test only works with iterators that iterate over a finite set.
   */
  public static void assertIteratorsInOrder(
      Iterator<?> expectedIterator, Iterator<?> actualIterator) {
    int i = 0;
    while (expectedIterator.hasNext()) {
      Object expected = expectedIterator.next();

      assertTrue(
          "index " + i + " expected <" + expected + "., actual is exhausted",
          actualIterator.hasNext());

      Object actual = actualIterator.next();
      assertEquals("index " + i, expected, actual);
      i++;
    }
    if (actualIterator.hasNext()) {
      fail("index " + i + ", expected is exhausted, actual <" + actualIterator.next() + ">");
    }
  }

  /**
   * Verifies that a collection is immutable.
   *
   * <p>A collection is considered immutable if:
   *
   * <ol>
   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
   *       underlying contents.
   *   <li>All methods that return objects that can indirectly mutate the collection throw
   *       UnsupportedOperationException when those mutators are called.
   * </ol>
   *
   * @param collection the presumed-immutable collection
   * @param sampleElement an element of the same type as that contained by {@code collection}.
   *     {@code collection} may or may not have {@code sampleElement} as a member.
   */
  public static <E extends @Nullable Object> void assertCollectionIsUnmodifiable(
      Collection<E> collection, E sampleElement) {
    Collection<E> siblingCollection = new ArrayList<>();
    siblingCollection.add(sampleElement);

    Collection<E> copy = new ArrayList<>();
    copy.addAll(collection);

    try {
      collection.add(sampleElement);
      fail("add succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }

    assertCollectionsAreEquivalent(copy, collection);

    try {
      collection.addAll(siblingCollection);
      fail("addAll succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(copy, collection);

    try {
      collection.clear();
      fail("clear succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(copy, collection);

    assertIteratorIsUnmodifiable(collection.iterator());
    assertCollectionsAreEquivalent(copy, collection);

    try {
      collection.remove(sampleElement);
      fail("remove succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(copy, collection);

    try {
      collection.removeAll(siblingCollection);
      fail("removeAll succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(copy, collection);

    try {
      collection.retainAll(siblingCollection);
      fail("retainAll succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(copy, collection);
  }

  /**
   * Verifies that a set is immutable.
   *
   * <p>A set is considered immutable if:
   *
   * <ol>
   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
   *       underlying contents.
   *   <li>All methods that return objects that can indirectly mutate the set throw
   *       UnsupportedOperationException when those mutators are called.
   * </ol>
   *
   * @param set the presumed-immutable set
   * @param sampleElement an element of the same type as that contained by {@code set}. {@code set}
   *     may or may not have {@code sampleElement} as a member.
   */
  public static <E extends @Nullable Object> void assertSetIsUnmodifiable(
      Set<E> set, E sampleElement) {
    assertCollectionIsUnmodifiable(set, sampleElement);
  }

  /**
   * Verifies that a multiset is immutable.
   *
   * <p>A multiset is considered immutable if:
   *
   * <ol>
   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
   *       underlying contents.
   *   <li>All methods that return objects that can indirectly mutate the multiset throw
   *       UnsupportedOperationException when those mutators are called.
   * </ol>
   *
   * @param multiset the presumed-immutable multiset
   * @param sampleElement an element of the same type as that contained by {@code multiset}. {@code
   *     multiset} may or may not have {@code sampleElement} as a member.
   */
  public static <E extends @Nullable Object> void assertMultisetIsUnmodifiable(
      Multiset<E> multiset, E sampleElement) {
    Multiset<E> copy = LinkedHashMultiset.create(multiset);
    assertCollectionsAreEquivalent(multiset, copy);

    // Multiset is a collection, so we can use all those tests.
    assertCollectionIsUnmodifiable(multiset, sampleElement);

    assertCollectionsAreEquivalent(multiset, copy);

    try {
      multiset.add(sampleElement, 2);
      fail("add(Object, int) succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(multiset, copy);

    try {
      multiset.remove(sampleElement, 2);
      fail("remove(Object, int) succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(multiset, copy);

    try {
      multiset.removeIf(x -> false);
      fail("removeIf(Predicate) succeeded on unmodifiable collection");
    } catch (UnsupportedOperationException expected) {
    }
    assertCollectionsAreEquivalent(multiset, copy);

    assertSetIsUnmodifiable(multiset.elementSet(), sampleElement);
    assertCollectionsAreEquivalent(multiset, copy);

    assertSetIsUnmodifiable(
        multiset.entrySet(),
        new Multiset.Entry<E>() {
          @Override
          public int getCount() {
            return 1;
          }

          @Override
          public E getElement() {
            return sampleElement;
          }
        });
    assertCollectionsAreEquivalent(multiset, copy);
  }

  /**
   * Verifies that a multimap is immutable.
   *
   * <p>A multimap is considered immutable if:
   *
   * <ol>
   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
   *       underlying contents.
   *   <li>All methods that return objects that can indirectly mutate the multimap throw
   *       UnsupportedOperationException when those mutators
   * </ol>
   *
   * @param multimap the presumed-immutable multimap
   * @param sampleKey a key of the same type as that contained by {@code multimap}. {@code multimap}
   *     may or may not have {@code sampleKey} as a key.
   * @param sampleValue a key of the same type as that contained by {@code multimap}. {@code
   *     multimap} may or may not have {@code sampleValue} as a key.
   */
  public static <K extends @Nullable Object, V extends @Nullable Object>
      void assertMultimapIsUnmodifiable(Multimap<K, V> multimap, K sampleKey, V sampleValue) {
    List<Entry<K, V>> originalEntries =
        Collections.unmodifiableList(Lists.newArrayList(multimap.entries()));

    assertMultimapRemainsUnmodified(multimap, originalEntries);

    Collection<V> sampleValueAsCollection = Collections.singleton(sampleValue);

    // Test #clear()
    try {
      multimap.clear();
      fail("clear succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }

    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test asMap().entrySet()
    assertSetIsUnmodifiable(
        multimap.asMap().entrySet(), Maps.immutableEntry(sampleKey, sampleValueAsCollection));

    // Test #values()

    assertMultimapRemainsUnmodified(multimap, originalEntries);
    if (!multimap.isEmpty()) {
      Collection<V> values = multimap.asMap().entrySet().iterator().next().getValue();

      assertCollectionIsUnmodifiable(values, sampleValue);
    }

    // Test #entries()
    assertCollectionIsUnmodifiable(multimap.entries(), Maps.immutableEntry(sampleKey, sampleValue));
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Iterate over every element in the entry set
    for (Entry<K, V> entry : multimap.entries()) {
      assertMapEntryIsUnmodifiable(entry);
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #keys()
    assertMultisetIsUnmodifiable(multimap.keys(), sampleKey);
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #keySet()
    assertSetIsUnmodifiable(multimap.keySet(), sampleKey);
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #get()
    if (!multimap.isEmpty()) {
      K key = multimap.keySet().iterator().next();
      assertCollectionIsUnmodifiable(multimap.get(key), sampleValue);
      assertMultimapRemainsUnmodified(multimap, originalEntries);
    }

    // Test #put()
    try {
      multimap.put(sampleKey, sampleValue);
      fail("put succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #putAll(K, Collection<V>)
    try {
      multimap.putAll(sampleKey, sampleValueAsCollection);
      fail("putAll(K, Iterable) succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #putAll(Multimap<K, V>)
    Multimap<K, V> multimap2 = ArrayListMultimap.create();
    multimap2.put(sampleKey, sampleValue);
    try {
      multimap.putAll(multimap2);
      fail("putAll(Multimap<K, V>) succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #remove()
    try {
      multimap.remove(sampleKey, sampleValue);
      fail("remove succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #removeAll()
    try {
      multimap.removeAll(sampleKey);
      fail("removeAll succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #replaceValues()
    try {
      multimap.replaceValues(sampleKey, sampleValueAsCollection);
      fail("replaceValues succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    // Test #asMap()
    try {
      multimap.asMap().remove(sampleKey);
      fail("asMap().remove() succeeded on unmodifiable multimap");
    } catch (UnsupportedOperationException expected) {
    }
    assertMultimapRemainsUnmodified(multimap, originalEntries);

    if (!multimap.isEmpty()) {
      K presentKey = multimap.keySet().iterator().next();
      try {
        multimap.asMap().get(presentKey).remove(sampleValue);
        fail("asMap().get().remove() succeeded on unmodifiable multimap");
      } catch (UnsupportedOperationException expected) {
      }
      assertMultimapRemainsUnmodified(multimap, originalEntries);

      try {
        multimap.asMap().values().iterator().next().remove(sampleValue);
        fail("asMap().values().iterator().next().remove() succeeded on unmodifiable multimap");
      } catch (UnsupportedOperationException expected) {
      }

      try {
        ((Collection<?>) multimap.asMap().values().toArray()[0]).clear();
        fail("asMap().values().toArray()[0].clear() succeeded on unmodifiable multimap");
      } catch (UnsupportedOperationException expected) {
      }
    }

    assertCollectionIsUnmodifiable(multimap.values(), sampleValue);
    assertMultimapRemainsUnmodified(multimap, originalEntries);
  }

  private static <E extends @Nullable Object> void assertCollectionsAreEquivalent(
      Collection<E> expected, Collection<E> actual) {
    assertIteratorsInOrder(expected.iterator(), actual.iterator());
  }

  private static <K extends @Nullable Object, V extends @Nullable Object>
      void assertMultimapRemainsUnmodified(Multimap<K, V> expected, List<Entry<K, V>> actual) {
    assertIteratorsInOrder(expected.entries().iterator(), actual.iterator());
  }
}
