/*
 * Copyright (C) 2009 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 com.google.common.collect.testing.features.CollectionFeature.ALLOWS_NULL_VALUES;
import static com.google.common.collect.testing.features.CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION;
import static com.google.common.collect.testing.features.CollectionFeature.RESTRICTS_ELEMENTS;
import static com.google.common.collect.testing.features.CollectionFeature.SUPPORTS_ADD;
import static com.google.common.collect.testing.features.CollectionFeature.SUPPORTS_REMOVE;
import static com.google.common.collect.testing.features.CollectionSize.SEVERAL;
import static com.google.common.collect.testing.features.CollectionSize.ZERO;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.testing.Helpers;
import com.google.common.collect.testing.features.CollectionFeature;
import com.google.common.collect.testing.features.CollectionSize;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import org.junit.Ignore;

/**
 * Common superclass for {@link MultisetSetCountUnconditionallyTester} and {@link
 * MultisetSetCountConditionallyTester}. It is used by those testers to test calls to the
 * unconditional {@code setCount()} method and calls to the conditional {@code setCount()} method
 * when the expected present count is correct.
 *
 * @author Chris Povirk
 */
@GwtCompatible(emulated = true)
@Ignore // Affects only Android test runner, which respects JUnit 4 annotations on JUnit 3 tests.
public abstract class AbstractMultisetSetCountTester<E> extends AbstractMultisetTester<E> {
  /*
   * TODO: consider adding MultisetFeatures.SUPPORTS_SET_COUNT. Currently we
   * assume that using setCount() to increase the count is permitted iff add()
   * is permitted and similarly for decrease/remove(). We assume that a
   * setCount() no-op is permitted if either add() or remove() is permitted,
   * though we also allow it to "succeed" if neither is permitted.
   */

  private void assertSetCount(E element, int count) {
    setCountCheckReturnValue(element, count);

    assertEquals(
        "multiset.count() should return the value passed to setCount()",
        count,
        getMultiset().count(element));

    int size = 0;
    for (Multiset.Entry<E> entry : getMultiset().entrySet()) {
      size += entry.getCount();
    }
    assertEquals(
        "multiset.size() should be the sum of the counts of all entries",
        size,
        getMultiset().size());
  }

  /** Call the {@code setCount()} method under test, and check its return value. */
  abstract void setCountCheckReturnValue(E element, int count);

  /**
   * Call the {@code setCount()} method under test, but do not check its return value. Callers
   * should use this method over {@link #setCountCheckReturnValue(Object, int)} when they expect
   * {@code setCount()} to throw an exception, as checking the return value could produce an
   * incorrect error message like "setCount() should return the original count" instead of the
   * message passed to a later invocation of {@code fail()}, like "setCount should throw
   * UnsupportedOperationException."
   */
  abstract void setCountNoCheckReturnValue(E element, int count);

  private void assertSetCountIncreasingFailure(E element, int count) {
    try {
      setCountNoCheckReturnValue(element, count);
      fail("a call to multiset.setCount() to increase an element's count should throw");
    } catch (UnsupportedOperationException expected) {
    }
  }

  private void assertSetCountDecreasingFailure(E element, int count) {
    try {
      setCountNoCheckReturnValue(element, count);
      fail("a call to multiset.setCount() to decrease an element's count should throw");
    } catch (UnsupportedOperationException expected) {
    }
  }

  // Unconditional setCount no-ops.

  private void assertZeroToZero() {
    assertSetCount(e3(), 0);
  }

  private void assertOneToOne() {
    assertSetCount(e0(), 1);
  }

  private void assertThreeToThree() {
    initThreeCopies();
    assertSetCount(e0(), 3);
  }

  @CollectionFeature.Require(SUPPORTS_ADD)
  public void testSetCount_zeroToZero_addSupported() {
    assertZeroToZero();
  }

  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_zeroToZero_removeSupported() {
    assertZeroToZero();
  }

  @CollectionFeature.Require(absent = {SUPPORTS_ADD, SUPPORTS_REMOVE})
  public void testSetCount_zeroToZero_unsupported() {
    try {
      assertZeroToZero();
    } catch (UnsupportedOperationException tolerated) {
    }
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(SUPPORTS_ADD)
  public void testSetCount_oneToOne_addSupported() {
    assertOneToOne();
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_oneToOne_removeSupported() {
    assertOneToOne();
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(absent = {SUPPORTS_ADD, SUPPORTS_REMOVE})
  public void testSetCount_oneToOne_unsupported() {
    try {
      assertOneToOne();
    } catch (UnsupportedOperationException tolerated) {
    }
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(SUPPORTS_ADD)
  public void testSetCount_threeToThree_addSupported() {
    assertThreeToThree();
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_threeToThree_removeSupported() {
    assertThreeToThree();
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(absent = {SUPPORTS_ADD, SUPPORTS_REMOVE})
  public void testSetCount_threeToThree_unsupported() {
    try {
      assertThreeToThree();
    } catch (UnsupportedOperationException tolerated) {
    }
  }

  // Unconditional setCount size increases:

  @CollectionFeature.Require(SUPPORTS_ADD)
  public void testSetCount_zeroToOne_supported() {
    assertSetCount(e3(), 1);
  }

  @CollectionFeature.Require({SUPPORTS_ADD, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
  public void testSetCountZeroToOneConcurrentWithIteration() {
    try {
      Iterator<E> iterator = collection.iterator();
      assertSetCount(e3(), 1);
      iterator.next();
      fail("Expected ConcurrentModificationException");
    } catch (ConcurrentModificationException expected) {
      // success
    }
  }

  @CollectionFeature.Require({SUPPORTS_ADD, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
  public void testSetCountZeroToOneConcurrentWithEntrySetIteration() {
    try {
      Iterator<Entry<E>> iterator = getMultiset().entrySet().iterator();
      assertSetCount(e3(), 1);
      iterator.next();
      fail("Expected ConcurrentModificationException");
    } catch (ConcurrentModificationException expected) {
      // success
    }
  }

  @CollectionFeature.Require(SUPPORTS_ADD)
  public void testSetCount_zeroToThree_supported() {
    assertSetCount(e3(), 3);
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(SUPPORTS_ADD)
  public void testSetCount_oneToThree_supported() {
    assertSetCount(e0(), 3);
  }

  @CollectionFeature.Require(absent = SUPPORTS_ADD)
  public void testSetCount_zeroToOne_unsupported() {
    assertSetCountIncreasingFailure(e3(), 1);
  }

  @CollectionFeature.Require(absent = SUPPORTS_ADD)
  public void testSetCount_zeroToThree_unsupported() {
    assertSetCountIncreasingFailure(e3(), 3);
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(absent = SUPPORTS_ADD)
  public void testSetCount_oneToThree_unsupported() {
    assertSetCountIncreasingFailure(e3(), 3);
  }

  // Unconditional setCount size decreases:

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_oneToZero_supported() {
    assertSetCount(e0(), 0);
  }

  @CollectionFeature.Require({SUPPORTS_REMOVE, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
  @CollectionSize.Require(absent = ZERO)
  public void testSetCountOneToZeroConcurrentWithIteration() {
    try {
      Iterator<E> iterator = collection.iterator();
      assertSetCount(e0(), 0);
      iterator.next();
      fail("Expected ConcurrentModificationException");
    } catch (ConcurrentModificationException expected) {
      // success
    }
  }

  @CollectionFeature.Require({SUPPORTS_REMOVE, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
  @CollectionSize.Require(absent = ZERO)
  public void testSetCountOneToZeroConcurrentWithEntrySetIteration() {
    try {
      Iterator<Entry<E>> iterator = getMultiset().entrySet().iterator();
      assertSetCount(e0(), 0);
      iterator.next();
      fail("Expected ConcurrentModificationException");
    } catch (ConcurrentModificationException expected) {
      // success
    }
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_threeToZero_supported() {
    initThreeCopies();
    assertSetCount(e0(), 0);
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_threeToOne_supported() {
    initThreeCopies();
    assertSetCount(e0(), 1);
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
  public void testSetCount_oneToZero_unsupported() {
    assertSetCountDecreasingFailure(e0(), 0);
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
  public void testSetCount_threeToZero_unsupported() {
    initThreeCopies();
    assertSetCountDecreasingFailure(e0(), 0);
  }

  @CollectionSize.Require(SEVERAL)
  @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
  public void testSetCount_threeToOne_unsupported() {
    initThreeCopies();
    assertSetCountDecreasingFailure(e0(), 1);
  }

  // setCount with nulls:

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require({SUPPORTS_REMOVE, ALLOWS_NULL_VALUES})
  public void testSetCount_removeNull_nullSupported() {
    initCollectionWithNullElement();
    assertSetCount(null, 0);
  }

  @CollectionFeature.Require(
    value = {SUPPORTS_ADD, ALLOWS_NULL_VALUES},
    absent = RESTRICTS_ELEMENTS
  )
  public void testSetCount_addNull_nullSupported() {
    assertSetCount(null, 1);
  }

  @CollectionFeature.Require(value = SUPPORTS_ADD, absent = ALLOWS_NULL_VALUES)
  public void testSetCount_addNull_nullUnsupported() {
    try {
      setCountNoCheckReturnValue(null, 1);
      fail("adding null with setCount() should throw NullPointerException");
    } catch (NullPointerException expected) {
    }
  }

  @CollectionFeature.Require(ALLOWS_NULL_VALUES)
  public void testSetCount_noOpNull_nullSupported() {
    try {
      assertSetCount(null, 0);
    } catch (UnsupportedOperationException tolerated) {
    }
  }

  @CollectionFeature.Require(absent = ALLOWS_NULL_VALUES)
  public void testSetCount_noOpNull_nullUnsupported() {
    try {
      assertSetCount(null, 0);
    } catch (NullPointerException | UnsupportedOperationException tolerated) {
    }
  }

  @CollectionSize.Require(absent = ZERO)
  @CollectionFeature.Require(ALLOWS_NULL_VALUES)
  public void testSetCount_existingNoNopNull_nullSupported() {
    initCollectionWithNullElement();
    try {
      assertSetCount(null, 1);
    } catch (UnsupportedOperationException tolerated) {
    }
  }

  // Negative count.

  @CollectionFeature.Require(SUPPORTS_REMOVE)
  public void testSetCount_negative_removeSupported() {
    try {
      setCountNoCheckReturnValue(e3(), -1);
      fail("calling setCount() with a negative count should throw IllegalArgumentException");
    } catch (IllegalArgumentException expected) {
    }
  }

  @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
  public void testSetCount_negative_removeUnsupported() {
    try {
      setCountNoCheckReturnValue(e3(), -1);
      fail(
          "calling setCount() with a negative count should throw "
              + "IllegalArgumentException or UnsupportedOperationException");
    } catch (IllegalArgumentException | UnsupportedOperationException expected) {
    }
  }

  // TODO: test adding element of wrong type

  /**
   * Returns {@link Method} instances for the {@code setCount()} tests that assume multisets support
   * duplicates so that the test of {@code Multisets.forSet()} can suppress them.
   */
  @GwtIncompatible // reflection
  public static List<Method> getSetCountDuplicateInitializingMethods() {
    return Arrays.asList(
        getMethod("testSetCount_threeToThree_removeSupported"),
        getMethod("testSetCount_threeToZero_supported"),
        getMethod("testSetCount_threeToOne_supported"));
  }

  @GwtIncompatible // reflection
  private static Method getMethod(String methodName) {
    return Helpers.getMethod(AbstractMultisetSetCountTester.class, methodName);
  }
}
