/*
 * 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.search.suggest;

import java.util.AbstractMap.SimpleEntry;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.BytesRef;

public class TestInputIterator extends LuceneTestCase {

  public void testEmpty() throws Exception {
    InputArrayIterator iterator = new InputArrayIterator(new Input[0]);
    try (Directory dir = getDirectory()) {
      InputIterator wrapper = new SortedInputIterator(dir, "sorted", iterator);
      assertNull(wrapper.next());
      wrapper = new UnsortedInputIterator(iterator);
      assertNull(wrapper.next());
    }
  }

  public void testTerms() throws Exception {
    Random random = random();
    int num = atLeast(10000);

    TreeMap<BytesRef, SimpleEntry<Long, BytesRef>> sorted = new TreeMap<>();
    TreeMap<BytesRef, Long> sortedWithoutPayload = new TreeMap<>();
    TreeMap<BytesRef, SimpleEntry<Long, Set<BytesRef>>> sortedWithContext = new TreeMap<>();
    TreeMap<BytesRef, SimpleEntry<Long, SimpleEntry<BytesRef, Set<BytesRef>>>>
        sortedWithPayloadAndContext = new TreeMap<>();
    Input[] unsorted = new Input[num];
    Input[] unsortedWithoutPayload = new Input[num];
    Input[] unsortedWithContexts = new Input[num];
    Input[] unsortedWithPayloadAndContext = new Input[num];
    Set<BytesRef> ctxs;
    for (int i = 0; i < num; i++) {
      BytesRef key;
      BytesRef payload;
      ctxs = new HashSet<>();
      do {
        key = new BytesRef(TestUtil.randomUnicodeString(random));
        payload = new BytesRef(TestUtil.randomUnicodeString(random));
        for (int j = 0; j < atLeast(2); j++) {
          ctxs.add(new BytesRef(TestUtil.randomUnicodeString(random)));
        }
      } while (sorted.containsKey(key));
      long value = random.nextLong();
      sortedWithoutPayload.put(key, value);
      sorted.put(key, new SimpleEntry<>(value, payload));
      sortedWithContext.put(key, new SimpleEntry<>(value, ctxs));
      sortedWithPayloadAndContext.put(
          key, new SimpleEntry<>(value, new SimpleEntry<>(payload, ctxs)));
      unsorted[i] = new Input(key, value, payload);
      unsortedWithoutPayload[i] = new Input(key, value);
      unsortedWithContexts[i] = new Input(key, value, ctxs);
      unsortedWithPayloadAndContext[i] = new Input(key, value, payload, ctxs);
    }

    // test the sorted iterator wrapper with payloads
    try (Directory tempDir = getDirectory()) {
      InputIterator wrapper =
          new SortedInputIterator(tempDir, "sorted", new InputArrayIterator(unsorted));
      Iterator<Map.Entry<BytesRef, SimpleEntry<Long, BytesRef>>> expected =
          sorted.entrySet().iterator();
      while (expected.hasNext()) {
        Map.Entry<BytesRef, SimpleEntry<Long, BytesRef>> entry = expected.next();

        assertEquals(entry.getKey(), wrapper.next());
        assertEquals(entry.getValue().getKey().longValue(), wrapper.weight());
        assertEquals(entry.getValue().getValue(), wrapper.payload());
      }
      assertNull(wrapper.next());
    }

    // test the sorted iterator wrapper with contexts
    try (Directory tempDir = getDirectory()) {
      InputIterator wrapper =
          new SortedInputIterator(tempDir, "sorted", new InputArrayIterator(unsortedWithContexts));
      Iterator<Map.Entry<BytesRef, SimpleEntry<Long, Set<BytesRef>>>> actualEntries =
          sortedWithContext.entrySet().iterator();
      while (actualEntries.hasNext()) {
        Map.Entry<BytesRef, SimpleEntry<Long, Set<BytesRef>>> entry = actualEntries.next();
        assertEquals(entry.getKey(), wrapper.next());
        assertEquals(entry.getValue().getKey().longValue(), wrapper.weight());
        Set<BytesRef> actualCtxs = entry.getValue().getValue();
        assertEquals(actualCtxs, wrapper.contexts());
      }
      assertNull(wrapper.next());
    }

    // test the sorted iterator wrapper with contexts and payload
    try (Directory tempDir = getDirectory()) {
      InputIterator wrapper =
          new SortedInputIterator(
              tempDir, "sorter", new InputArrayIterator(unsortedWithPayloadAndContext));
      Iterator<Map.Entry<BytesRef, SimpleEntry<Long, SimpleEntry<BytesRef, Set<BytesRef>>>>>
          expectedPayloadContextEntries = sortedWithPayloadAndContext.entrySet().iterator();
      while (expectedPayloadContextEntries.hasNext()) {
        Map.Entry<BytesRef, SimpleEntry<Long, SimpleEntry<BytesRef, Set<BytesRef>>>> entry =
            expectedPayloadContextEntries.next();
        assertEquals(entry.getKey(), wrapper.next());
        assertEquals(entry.getValue().getKey().longValue(), wrapper.weight());
        Set<BytesRef> actualCtxs = entry.getValue().getValue().getValue();
        assertEquals(actualCtxs, wrapper.contexts());
        BytesRef actualPayload = entry.getValue().getValue().getKey();
        assertEquals(actualPayload, wrapper.payload());
      }
      assertNull(wrapper.next());
    }

    // test the unsorted iterator wrapper with payloads
    InputIterator wrapper = new UnsortedInputIterator(new InputArrayIterator(unsorted));
    TreeMap<BytesRef, SimpleEntry<Long, BytesRef>> actual = new TreeMap<>();
    BytesRef key;
    while ((key = wrapper.next()) != null) {
      long value = wrapper.weight();
      BytesRef payload = wrapper.payload();
      actual.put(BytesRef.deepCopyOf(key), new SimpleEntry<>(value, BytesRef.deepCopyOf(payload)));
    }
    assertEquals(sorted, actual);

    // test the sorted iterator wrapper without payloads
    try (Directory tempDir = getDirectory()) {
      InputIterator wrapperWithoutPayload =
          new SortedInputIterator(
              tempDir, "sorted", new InputArrayIterator(unsortedWithoutPayload));
      Iterator<Map.Entry<BytesRef, Long>> expectedWithoutPayload =
          sortedWithoutPayload.entrySet().iterator();
      while (expectedWithoutPayload.hasNext()) {
        Map.Entry<BytesRef, Long> entry = expectedWithoutPayload.next();

        assertEquals(entry.getKey(), wrapperWithoutPayload.next());
        assertEquals(entry.getValue().longValue(), wrapperWithoutPayload.weight());
        assertNull(wrapperWithoutPayload.payload());
      }
      assertNull(wrapperWithoutPayload.next());
    }

    // test the unsorted iterator wrapper without payloads
    InputIterator wrapperWithoutPayload =
        new UnsortedInputIterator(new InputArrayIterator(unsortedWithoutPayload));
    TreeMap<BytesRef, Long> actualWithoutPayload = new TreeMap<>();
    while ((key = wrapperWithoutPayload.next()) != null) {
      long value = wrapperWithoutPayload.weight();
      assertNull(wrapperWithoutPayload.payload());
      actualWithoutPayload.put(BytesRef.deepCopyOf(key), value);
    }
    assertEquals(sortedWithoutPayload, actualWithoutPayload);
  }

  public static long asLong(BytesRef b) {
    return (((long) asIntInternal(b, b.offset) << 32)
        | asIntInternal(b, b.offset + 4) & 0xFFFFFFFFL);
  }

  private static int asIntInternal(BytesRef b, int pos) {
    return ((b.bytes[pos++] & 0xFF) << 24)
        | ((b.bytes[pos++] & 0xFF) << 16)
        | ((b.bytes[pos++] & 0xFF) << 8)
        | (b.bytes[pos] & 0xFF);
  }

  private Directory getDirectory() {
    return newDirectory();
  }
}
