/*
 * 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.testing.IteratorFeature.MODIFIABLE;
import static com.google.common.collect.testing.IteratorFeature.UNMODIFIABLE;
import static java.util.Collections.emptyList;
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.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import junit.framework.TestCase;
/**
 * Unit test for {@link PeekingIterator}.
 *
 * @author Mick Killianey
 */
@SuppressWarnings("serial") // No serialization is used in this test
@GwtCompatible(emulated = true)
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).
   *
   * 
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 extends IteratorTester {
    private Iterable master;
    private List targetList;
    public PeekingIteratorTester(Collection master) {
      super(master.size() + 3, MODIFIABLE, master, IteratorTester.KnownOrder.KNOWN_ORDER);
      this.master = master;
    }
    @Override
    protected Iterator newTargetIterator() {
      // make copy from master to verify later
      targetList = Lists.newArrayList(master);
      Iterator iterator = targetList.iterator();
      return Iterators.peekingIterator(iterator);
    }
    @Override
    protected void verify(List elements) {
      // verify same objects were removed from reference and target
      assertEquals(elements, targetList);
    }
  }
  private  void actsLikeIteratorHelper(final List list) {
    // Check with modifiable copies of the list
    new PeekingIteratorTester(list).test();
    // Check with unmodifiable lists
    new IteratorTester(
        list.size() * 2 + 2, UNMODIFIABLE, list, IteratorTester.KnownOrder.KNOWN_ORDER) {
      @Override
      protected Iterator newTargetIterator() {
        Iterator iterator = Collections.unmodifiableList(list).iterator();
        return Iterators.peekingIterator(iterator);
      }
    }.test();
  }
  public void testPeekingIteratorBehavesLikeIteratorOnEmptyIterable() {
    actsLikeIteratorHelper(Collections.emptyList());
  }
  public void testPeekingIteratorBehavesLikeIteratorOnSingletonIterable() {
    actsLikeIteratorHelper(Collections.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.newArrayList(null, "A", null));
  }
  public void testPeekOnEmptyList() {
    List> list = Collections.emptyList();
    Iterator> iterator = list.iterator();
    PeekingIterator> peekingIterator = Iterators.peekingIterator(iterator);
    try {
      peekingIterator.peek();
      fail("Should throw NoSuchElementException if nothing to peek()");
    } catch (NoSuchElementException e) {
      /* expected */
    }
  }
  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());
    try {
      peekingIterator.peek();
      fail("Should throw exception if no next to peek()");
    } catch (NoSuchElementException e) {
      /* expected */
    }
    try {
      peekingIterator.peek();
      fail("Should continue to throw exception if no next to peek()");
    } catch (NoSuchElementException e) {
      /* expected */
    }
    try {
      peekingIterator.next();
      fail("next() should still throw exception after the end of iteration");
    } catch (NoSuchElementException e) {
      /* expected */
    }
  }
  public void testCantRemoveAfterPeek() {
    List list = Lists.newArrayList("A", "B", "C");
    Iterator iterator = list.iterator();
    PeekingIterator> peekingIterator = Iterators.peekingIterator(iterator);
    assertEquals("A", peekingIterator.next());
    assertEquals("B", peekingIterator.peek());
    /* Should complain on attempt to remove() after peek(). */
    try {
      peekingIterator.remove();
      fail("remove() should throw IllegalStateException after a peek()");
    } catch (IllegalStateException e) {
      /* expected */
    }
    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 implements Iterator {
    Iterator iterator;
    public ThrowsAtEndIterator(Iterable 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 list = emptyList();
    Iterator iterator = peekingIterator(new ThrowsAtEndIterator(list));
    assertNextThrows(iterator);
    /* Checks the case where a later element throws an exception. */
    list = Lists.newArrayList(1, 2);
    iterator = peekingIterator(new ThrowsAtEndIterator(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) {
    }
  }
}