/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.lucene.util;

import com.carrotsearch.randomizedtesting.annotations.Repeat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestMergedIterator extends LuceneTestCase {
  private static final int REPEATS = 2;
  private static final int VALS_TO_MERGE = 15000;

  @SuppressWarnings({"rawtypes", "unchecked"})
  public void testMergeEmpty() {
    Iterator<Integer> merged = new MergedIterator<>();
    assertFalse(merged.hasNext());

    merged = new MergedIterator<>(new ArrayList<Integer>().iterator());
    assertFalse(merged.hasNext());

    Iterator<Integer>[] itrs = new Iterator[random().nextInt(100)];
    for (int i = 0; i < itrs.length; i++) {
      itrs[i] = new ArrayList<Integer>().iterator();
    }
    merged = new MergedIterator<>(itrs);
    assertFalse(merged.hasNext());
  }

  @Repeat(iterations = REPEATS)
  public void testNoDupsRemoveDups() {
    testCase(1, 1, true);
  }

  @Repeat(iterations = REPEATS)
  public void testOffItrDupsRemoveDups() {
    testCase(3, 1, true);
  }

  @Repeat(iterations = REPEATS)
  public void testOnItrDupsRemoveDups() {
    testCase(1, 3, true);
  }

  @Repeat(iterations = REPEATS)
  public void testOnItrRandomDupsRemoveDups() {
    testCase(1, -3, true);
  }

  @Repeat(iterations = REPEATS)
  public void testBothDupsRemoveDups() {
    testCase(3, 3, true);
  }

  @Repeat(iterations = REPEATS)
  public void testBothDupsWithRandomDupsRemoveDups() {
    testCase(3, -3, true);
  }

  @Repeat(iterations = REPEATS)
  public void testNoDupsKeepDups() {
    testCase(1, 1, false);
  }

  @Repeat(iterations = REPEATS)
  public void testOffItrDupsKeepDups() {
    testCase(3, 1, false);
  }

  @Repeat(iterations = REPEATS)
  public void testOnItrDupsKeepDups() {
    testCase(1, 3, false);
  }

  @Repeat(iterations = REPEATS)
  public void testOnItrRandomDupsKeepDups() {
    testCase(1, -3, false);
  }

  @Repeat(iterations = REPEATS)
  public void testBothDupsKeepDups() {
    testCase(3, 3, false);
  }

  @Repeat(iterations = REPEATS)
  public void testBothDupsWithRandomDupsKeepDups() {
    testCase(3, -3, false);
  }

  private void testCase(int itrsWithVal, int specifiedValsOnItr, boolean removeDups) {
    // Build a random number of lists
    List<Integer> expected = new ArrayList<>();
    Random random = new Random(random().nextLong());
    int numLists = itrsWithVal + random.nextInt(1000 - itrsWithVal);
    @SuppressWarnings({"rawtypes", "unchecked"})
    List<Integer>[] lists = new List[numLists];
    for (int i = 0; i < numLists; i++) {
      lists[i] = new ArrayList<>();
    }
    int start = random.nextInt(1000000);
    int end = start + VALS_TO_MERGE / itrsWithVal / Math.abs(specifiedValsOnItr);
    for (int i = start; i < end; i++) {
      int maxList = lists.length;
      int maxValsOnItr = 0;
      int sumValsOnItr = 0;
      for (int itrWithVal = 0; itrWithVal < itrsWithVal; itrWithVal++) {
        int list = random.nextInt(maxList);
        int valsOnItr =
            specifiedValsOnItr < 0 ? (1 + random.nextInt(-specifiedValsOnItr)) : specifiedValsOnItr;
        maxValsOnItr = Math.max(maxValsOnItr, valsOnItr);
        sumValsOnItr += valsOnItr;
        for (int valOnItr = 0; valOnItr < valsOnItr; valOnItr++) {
          lists[list].add(i);
        }
        maxList = maxList - 1;
        ArrayUtil.swap(lists, list, maxList);
      }
      int maxCount = removeDups ? maxValsOnItr : sumValsOnItr;
      for (int count = 0; count < maxCount; count++) {
        expected.add(i);
      }
    }
    // Now check that they get merged cleanly
    @SuppressWarnings({"rawtypes", "unchecked"})
    Iterator<Integer>[] itrs = new Iterator[numLists];
    for (int i = 0; i < numLists; i++) {
      itrs[i] = lists[i].iterator();
    }

    MergedIterator<Integer> mergedItr = new MergedIterator<>(removeDups, itrs);
    Iterator<Integer> expectedItr = expected.iterator();
    while (expectedItr.hasNext()) {
      assertTrue(mergedItr.hasNext());
      assertEquals(expectedItr.next(), mergedItr.next());
    }
    assertFalse(mergedItr.hasNext());
  }
}
