/*
 * Copyright (C) 2008 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.collect.Iterators.peekingIterator;
import static com.google.common.collect.ReflectionFreeAssertThrows.assertThrows;
import static com.google.common.collect.testing.IteratorFeature.MODIFIABLE;
import static com.google.common.collect.testing.IteratorFeature.UNMODIFIABLE;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.testing.IteratorTester;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import junit.framework.TestCase;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
 * Unit test for {@link PeekingIterator}.
 *
 * @author Mick Killianey
 */
@SuppressWarnings("serial") // No serialization is used in this test
@GwtCompatible
@NullMarked
public class PeekingIteratorTest extends TestCase {

  /**
   * Version of {@link IteratorTester} that compares an iterator over a given collection of elements
   * (used as the reference iterator) against a {@code PeekingIterator} that *wraps* such an
   * iterator (used as the target iterator).
   *
   * <p>This IteratorTester makes copies of the master so that it can later verify that {@link
   * PeekingIterator#remove()} removes the same elements as the reference's iterator {@code
   * remove()}.
   */
  private static class PeekingIteratorTester<T extends @Nullable Object> extends IteratorTester<T> {
    private final Iterable<T> master;
    private @Nullable List<T> targetList;

    PeekingIteratorTester(Collection<T> master) {
      super(master.size() + 3, MODIFIABLE, master, IteratorTester.KnownOrder.KNOWN_ORDER);
      this.master = master;
    }

    @Override
    protected Iterator<T> newTargetIterator() {
      // make copy from master to verify later
      targetList = Lists.newArrayList(master);
      Iterator<T> iterator = targetList.iterator();
      return Iterators.peekingIterator(iterator);
    }

    @Override
    protected void verify(List<T> elements) {
      // verify same objects were removed from reference and target
      assertEquals(elements, targetList);
    }
  }

  private <T extends @Nullable Object> void actsLikeIteratorHelper(List<T> list) {
    // Check with modifiable copies of the list
    new PeekingIteratorTester<T>(list).test();

    // Check with unmodifiable lists
    new IteratorTester<T>(
        list.size() * 2 + 2, UNMODIFIABLE, list, IteratorTester.KnownOrder.KNOWN_ORDER) {
      @Override
      protected Iterator<T> newTargetIterator() {
        Iterator<T> iterator = unmodifiableList(list).iterator();
        return Iterators.peekingIterator(iterator);
      }
    }.test();
  }

  public void testPeekingIteratorBehavesLikeIteratorOnEmptyIterable() {
    actsLikeIteratorHelper(emptyList());
  }

  public void testPeekingIteratorBehavesLikeIteratorOnSingletonIterable() {
    actsLikeIteratorHelper(singletonList(new Object()));
  }

  // TODO(cpovirk): instead of skipping, use a smaller number of steps
  @GwtIncompatible // works but takes 5 minutes to run
  public void testPeekingIteratorBehavesLikeIteratorOnThreeElementIterable() {
    actsLikeIteratorHelper(Lists.newArrayList("A", "B", "C"));
  }

  @GwtIncompatible // works but takes 5 minutes to run
  public void testPeekingIteratorAcceptsNullElements() {
    actsLikeIteratorHelper(Lists.<@Nullable String>newArrayList(null, "A", null));
  }

  public void testPeekOnEmptyList() {
    List<?> list = emptyList();
    Iterator<?> iterator = list.iterator();
    PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator);

    assertThrows(NoSuchElementException.class, () -> peekingIterator.peek());
  }

  public void testPeekDoesntChangeIteration() {
    List<?> list = Lists.newArrayList("A", "B", "C");
    Iterator<?> iterator = list.iterator();
    PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator);

    assertEquals("Should be able to peek() at first element", "A", peekingIterator.peek());
    assertEquals(
        "Should be able to peek() first element multiple times", "A", peekingIterator.peek());
    assertEquals(
        "next() should still return first element after peeking", "A", peekingIterator.next());

    assertEquals("Should be able to peek() at middle element", "B", peekingIterator.peek());
    assertEquals(
        "Should be able to peek() middle element multiple times", "B", peekingIterator.peek());
    assertEquals(
        "next() should still return middle element after peeking", "B", peekingIterator.next());

    assertEquals("Should be able to peek() at last element", "C", peekingIterator.peek());
    assertEquals(
        "Should be able to peek() last element multiple times", "C", peekingIterator.peek());
    assertEquals(
        "next() should still return last element after peeking", "C", peekingIterator.next());

    assertThrows(NoSuchElementException.class, () -> peekingIterator.peek());
    assertThrows(NoSuchElementException.class, () -> peekingIterator.peek());
    assertThrows(NoSuchElementException.class, () -> peekingIterator.next());
  }

  public void testCantRemoveAfterPeek() {
    List<String> list = Lists.newArrayList("A", "B", "C");
    Iterator<String> iterator = list.iterator();
    PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator);

    assertEquals("A", peekingIterator.next());
    assertEquals("B", peekingIterator.peek());

    /* Should complain on attempt to remove() after peek(). */
    assertThrows(IllegalStateException.class, () -> peekingIterator.remove());

    assertEquals(
        "After remove() throws exception, peek should still be ok", "B", peekingIterator.peek());

    /* Should recover to be able to remove() after next(). */
    assertEquals("B", peekingIterator.next());
    peekingIterator.remove();
    assertEquals("Should have removed an element", 2, list.size());
    assertFalse("Second element should be gone", list.contains("B"));
  }

  static class ThrowsAtEndException extends RuntimeException {
    /* nothing */
  }

  /**
   * This Iterator claims to have more elements than the underlying iterable, but when you try to
   * fetch the extra elements, it throws an unchecked exception.
   */
  static class ThrowsAtEndIterator<E> implements Iterator<E> {
    Iterator<E> iterator;

    public ThrowsAtEndIterator(Iterable<E> iterable) {
      this.iterator = iterable.iterator();
    }

    @Override
    public boolean hasNext() {
      return true; // pretend that you have more...
    }

    @Override
    public E next() {
      // ...but throw an unchecked exception when you ask for it.
      if (!iterator.hasNext()) {
        throw new ThrowsAtEndException();
      }
      return iterator.next();
    }

    @Override
    public void remove() {
      iterator.remove();
    }
  }

  public void testPeekingIteratorDoesntAdvancePrematurely() throws Exception {
    /*
     * This test will catch problems where the underlying iterator
     * throws a RuntimeException when retrieving the nth element.
     *
     * If the PeekingIterator is caching elements too aggressively,
     * it may throw the exception on the (n-1)th element (oops!).
     */

    /* Checks the case where the first element throws an exception. */

    List<Integer> list = emptyList();
    Iterator<Integer> iterator = peekingIterator(new ThrowsAtEndIterator<Integer>(list));
    assertNextThrows(iterator);

    /* Checks the case where a later element throws an exception. */

    list = Lists.newArrayList(1, 2);
    iterator = peekingIterator(new ThrowsAtEndIterator<Integer>(list));
    assertTrue(iterator.hasNext());
    iterator.next();
    assertTrue(iterator.hasNext());
    iterator.next();
    assertNextThrows(iterator);
  }

  private void assertNextThrows(Iterator<?> iterator) {
    try {
      iterator.next();
      fail();
    } catch (ThrowsAtEndException expected) {
    }
  }
}
