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

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ReflectionFreeAssertThrows.assertThrows;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.collect.testing.features.CollectionFeature;
import com.google.common.collect.testing.features.CollectionSize;
import com.google.common.collect.testing.google.MultisetTestSuiteBuilder;
import com.google.common.collect.testing.google.TestStringMultisetGenerator;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
 * Unit test for {@link AbstractMultiset}.
 *
 * @author Kevin Bourrillion
 * @author Louis Wasserman
 */
@SuppressWarnings("serial") // No serialization is used in this test
@GwtCompatible
@NullMarked
public class SimpleAbstractMultisetTest extends TestCase {
  @J2ktIncompatible
  @GwtIncompatible // suite
  @AndroidIncompatible // test-suite builders
  public static Test suite() {
    TestSuite suite = new TestSuite();
    suite.addTestSuite(SimpleAbstractMultisetTest.class);
    suite.addTest(
        MultisetTestSuiteBuilder.using(
                new TestStringMultisetGenerator() {
                  @Override
                  protected Multiset<String> create(String[] elements) {
                    Multiset<String> ms = new NoRemoveMultiset<>();
                    Collections.addAll(ms, elements);
                    return ms;
                  }
                })
            .named("NoRemoveMultiset")
            .withFeatures(
                CollectionSize.ANY,
                CollectionFeature.ALLOWS_NULL_VALUES,
                CollectionFeature.SUPPORTS_ADD)
            .createTestSuite());
    return suite;
  }

  @SuppressWarnings("ModifiedButNotUsed")
  public void testFastAddAllMultiset() {
    AtomicInteger addCalls = new AtomicInteger();
    Multiset<String> multiset =
        new NoRemoveMultiset<String>() {
          @Override
          public int add(String element, int occurrences) {
            addCalls.incrementAndGet();
            return super.add(element, occurrences);
          }
        };
    ImmutableMultiset<String> adds =
        new ImmutableMultiset.Builder<String>().addCopies("x", 10).build();
    multiset.addAll(adds);
    assertEquals(1, addCalls.get());
  }

  public void testRemoveUnsupported() {
    Multiset<String> multiset = new NoRemoveMultiset<>();
    multiset.add("a");
    assertThrows(UnsupportedOperationException.class, () -> multiset.remove("a"));
    assertTrue(multiset.contains("a"));
  }

  private static class NoRemoveMultiset<E extends @Nullable Object> extends AbstractMultiset<E>
      implements Serializable {
    final Map<E, Integer> backingMap = new HashMap<>();

    @Override
    public int size() {
      return Multisets.linearTimeSizeImpl(this);
    }

    @Override
    public void clear() {
      throw new UnsupportedOperationException();
    }

    @Override
    public int count(@Nullable Object element) {
      for (Entry<E> entry : entrySet()) {
        if (Objects.equals(entry.getElement(), element)) {
          return entry.getCount();
        }
      }
      return 0;
    }

    @Override
    public int add(E element, int occurrences) {
      checkArgument(occurrences >= 0);
      Integer frequency = backingMap.getOrDefault(element, 0);
      if (occurrences == 0) {
        return frequency;
      }
      checkArgument(occurrences <= Integer.MAX_VALUE - frequency);
      backingMap.put(element, frequency + occurrences);
      return frequency;
    }

    @Override
    Iterator<E> elementIterator() {
      return Multisets.elementIterator(entryIterator());
    }

    @Override
    Iterator<Entry<E>> entryIterator() {
      Iterator<Map.Entry<E, Integer>> backingEntries = backingMap.entrySet().iterator();
      return new UnmodifiableIterator<Multiset.Entry<E>>() {
        @Override
        public boolean hasNext() {
          return backingEntries.hasNext();
        }

        @Override
        public Multiset.Entry<E> next() {
          Map.Entry<E, Integer> mapEntry = backingEntries.next();
          return new Multisets.AbstractEntry<E>() {
            @Override
            public E getElement() {
              return mapEntry.getKey();
            }

            @Override
            public int getCount() {
              Integer frequency = backingMap.get(getElement());
              return (frequency == null) ? 0 : frequency;
            }
          };
        }
      };
    }

    @Override
    public Iterator<E> iterator() {
      return Multisets.iteratorImpl(this);
    }

    @Override
    int distinctElements() {
      return backingMap.size();
    }
  }
}
