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

import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.lucene.document.BinaryDocValuesField;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.FieldExistsQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.IOUtils;

public class TestMixedDocValuesUpdates extends LuceneTestCase {

  public void testManyReopensAndFields() throws Exception {
    Directory dir = newDirectory();
    final Random random = random();
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random));
    LogMergePolicy lmp = newLogMergePolicy();
    lmp.setMergeFactor(3); // merge often
    conf.setMergePolicy(lmp);
    IndexWriter writer = new IndexWriter(dir, conf);

    final boolean isNRT = random.nextBoolean();
    DirectoryReader reader;
    if (isNRT) {
      reader = DirectoryReader.open(writer);
    } else {
      writer.commit();
      reader = DirectoryReader.open(dir);
    }

    final int numFields = random.nextInt(4) + 3; // 3-7
    final int numNDVFields = random.nextInt(numFields / 2) + 1; // 1-3
    final long[] fieldValues = new long[numFields];
    for (int i = 0; i < fieldValues.length; i++) {
      fieldValues[i] = 1;
    }

    int numRounds = atLeast(15);
    int docID = 0;
    for (int i = 0; i < numRounds; i++) {
      int numDocs = atLeast(5);
      // System.out.println("TEST: round=" + i + ", numDocs=" + numDocs);
      for (int j = 0; j < numDocs; j++) {
        Document doc = new Document();
        doc.add(new StringField("id", "doc-" + docID, Store.NO));
        doc.add(new StringField("key", "all", Store.NO)); // update key
        // add all fields with their current value
        for (int f = 0; f < fieldValues.length; f++) {
          if (f < numNDVFields) {
            doc.add(new NumericDocValuesField("f" + f, fieldValues[f]));
          } else {
            doc.add(
                new BinaryDocValuesField(
                    "f" + f, TestBinaryDocValuesUpdates.toBytes(fieldValues[f])));
          }
        }
        writer.addDocument(doc);
        ++docID;
      }

      int fieldIdx = random.nextInt(fieldValues.length);
      String updateField = "f" + fieldIdx;
      if (fieldIdx < numNDVFields) {
        writer.updateNumericDocValue(new Term("key", "all"), updateField, ++fieldValues[fieldIdx]);
      } else {
        writer.updateBinaryDocValue(
            new Term("key", "all"),
            updateField,
            TestBinaryDocValuesUpdates.toBytes(++fieldValues[fieldIdx]));
      }
      // System.out.println("TEST: updated field '" + updateField + "' to value " +
      // fieldValues[fieldIdx]);

      if (random.nextDouble() < 0.2) {
        int deleteDoc = random.nextInt(docID); // might also delete an already deleted document, ok!
        writer.deleteDocuments(new Term("id", "doc-" + deleteDoc));
        //        System.out.println("[" + Thread.currentThread().getName() + "]: deleted document:
        // doc-" + deleteDoc);
      }

      // verify reader
      if (!isNRT) {
        writer.commit();
      }

      //      System.out.println("[" + Thread.currentThread().getName() + "]: reopen reader: " +
      // reader);
      DirectoryReader newReader = DirectoryReader.openIfChanged(reader);
      assertNotNull(newReader);
      reader.close();
      reader = newReader;
      //      System.out.println("[" + Thread.currentThread().getName() + "]: reopened reader: " +
      // reader);
      assertTrue(reader.numDocs() > 0); // we delete at most one document per round
      for (LeafReaderContext context : reader.leaves()) {
        LeafReader r = context.reader();
        //        System.out.println(((SegmentReader) r).getSegmentName());
        Bits liveDocs = r.getLiveDocs();
        for (int field = 0; field < fieldValues.length; field++) {
          String f = "f" + field;
          BinaryDocValues bdv = r.getBinaryDocValues(f);
          NumericDocValues ndv = r.getNumericDocValues(f);
          if (field < numNDVFields) {
            assertNotNull(ndv);
            assertNull(bdv);
          } else {
            assertNull(ndv);
            assertNotNull(bdv);
          }
          int maxDoc = r.maxDoc();
          for (int doc = 0; doc < maxDoc; doc++) {
            if (liveDocs == null || liveDocs.get(doc)) {
              //              System.out.println("doc=" + (doc + context.docBase) + " f='" + f + "'
              // vslue=" + getValue(bdv, doc, scratch));
              if (field < numNDVFields) {
                assertEquals(doc, ndv.advance(doc));
                assertEquals(
                    "invalid numeric value for doc=" + doc + ", field=" + f + ", reader=" + r,
                    fieldValues[field],
                    ndv.longValue());
              } else {
                assertEquals(doc, bdv.advance(doc));
                assertEquals(
                    "invalid binary value for doc=" + doc + ", field=" + f + ", reader=" + r,
                    fieldValues[field],
                    TestBinaryDocValuesUpdates.getValue(bdv));
              }
            }
          }
        }
      }
      //      System.out.println();
    }

    writer.close();
    IOUtils.close(reader, dir);
  }

  public void testStressMultiThreading() throws Exception {
    final Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random()));
    final IndexWriter writer = new IndexWriter(dir, conf);

    // create index
    final int numFields = TestUtil.nextInt(random(), 2, 4);
    final int numThreads = TestUtil.nextInt(random(), 3, 6);
    final int numDocs = atLeast(2000);
    for (int i = 0; i < numDocs; i++) {
      Document doc = new Document();
      doc.add(new StringField("id", "doc" + i, Store.NO));
      double group = random().nextDouble();
      String g;
      if (group < 0.1) g = "g0";
      else if (group < 0.5) g = "g1";
      else if (group < 0.8) g = "g2";
      else g = "g3";
      doc.add(new StringField("updKey", g, Store.NO));
      for (int j = 0; j < numFields; j++) {
        long value = random().nextInt();
        doc.add(new BinaryDocValuesField("f" + j, TestBinaryDocValuesUpdates.toBytes(value)));
        doc.add(new NumericDocValuesField("cf" + j, value * 2)); // control, always updated to f * 2
      }
      writer.addDocument(doc);
    }

    final CountDownLatch done = new CountDownLatch(numThreads);
    final AtomicInteger numUpdates = new AtomicInteger(atLeast(100));

    // same thread updates a field as well as reopens
    Thread[] threads = new Thread[numThreads];
    for (int i = 0; i < threads.length; i++) {
      threads[i] =
          new Thread("UpdateThread-" + i) {
            @Override
            public void run() {
              DirectoryReader reader = null;
              boolean success = false;
              try {
                Random random = random();
                while (numUpdates.getAndDecrement() > 0) {
                  double group = random.nextDouble();
                  Term t;
                  if (group < 0.1) t = new Term("updKey", "g0");
                  else if (group < 0.5) t = new Term("updKey", "g1");
                  else if (group < 0.8) t = new Term("updKey", "g2");
                  else t = new Term("updKey", "g3");
                  //              System.out.println("[" + Thread.currentThread().getName() + "]
                  // numUpdates=" + numUpdates + " updateTerm=" + t);
                  int field = random().nextInt(numFields);
                  final String f = "f" + field;
                  final String cf = "cf" + field;
                  long updValue = random.nextInt();
                  //              System.err.println("[" + Thread.currentThread().getName() + "] t="
                  // + t + ", f=" + f + ", updValue=" + updValue);
                  writer.updateDocValues(
                      t,
                      new BinaryDocValuesField(f, TestBinaryDocValuesUpdates.toBytes(updValue)),
                      new NumericDocValuesField(cf, updValue * 2));

                  if (random.nextDouble() < 0.2) {
                    // delete a random document
                    int doc = random.nextInt(numDocs);
                    //                System.out.println("[" + Thread.currentThread().getName() + "]
                    // deleteDoc=doc" + doc);
                    writer.deleteDocuments(new Term("id", "doc" + doc));
                  }

                  if (random.nextDouble() < 0.05) { // commit every 20 updates on average
                    //                  System.out.println("[" + Thread.currentThread().getName() +
                    // "] commit");
                    writer.commit();
                  }

                  if (random.nextDouble()
                      < 0.1) { // reopen NRT reader (apply updates), on average once every 10
                    // updates
                    if (reader == null) {
                      //                  System.out.println("[" + Thread.currentThread().getName()
                      // + "] open NRT");
                      reader = DirectoryReader.open(writer);
                    } else {
                      //                  System.out.println("[" + Thread.currentThread().getName()
                      // + "] reopen NRT");
                      DirectoryReader r2 = DirectoryReader.openIfChanged(reader, writer);
                      if (r2 != null) {
                        reader.close();
                        reader = r2;
                      }
                    }
                  }
                }
                //            System.out.println("[" + Thread.currentThread().getName() + "] DONE");
                success = true;
              } catch (IOException e) {
                throw new RuntimeException(e);
              } finally {
                if (reader != null) {
                  try {
                    reader.close();
                  } catch (IOException e) {
                    if (success) { // suppress this exception only if there was another exception
                      throw new RuntimeException(e);
                    }
                  }
                }
                done.countDown();
              }
            }
          };
    }

    for (Thread t : threads) t.start();
    done.await();
    writer.close();

    DirectoryReader reader = DirectoryReader.open(dir);
    for (LeafReaderContext context : reader.leaves()) {
      LeafReader r = context.reader();
      for (int i = 0; i < numFields; i++) {
        BinaryDocValues bdv = r.getBinaryDocValues("f" + i);
        NumericDocValues control = r.getNumericDocValues("cf" + i);
        Bits liveDocs = r.getLiveDocs();
        for (int j = 0; j < r.maxDoc(); j++) {
          if (liveDocs == null || liveDocs.get(j)) {
            assertEquals(j, control.advance(j));
            long ctrlValue = control.longValue();
            assertEquals(j, bdv.advance(j));
            long bdvValue = TestBinaryDocValuesUpdates.getValue(bdv) * 2;
            //              if (ctrlValue != bdvValue) {
            //                System.out.println("seg=" + r + ", f=f" + i + ", doc=" + j + ",
            // group=" + r.storedFields().document(j).get("updKey") + ", ctrlValue=" + ctrlValue +
            // ", bdvBytes=" +
            // scratch);
            //              }
            assertEquals(ctrlValue, bdvValue);
          }
        }
      }
    }
    reader.close();

    dir.close();
  }

  public void testUpdateDifferentDocsInDifferentGens() throws Exception {
    // update same document multiple times across generations
    Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random()));
    conf.setMaxBufferedDocs(4);
    IndexWriter writer = new IndexWriter(dir, conf);
    final int numDocs = atLeast(10);
    for (int i = 0; i < numDocs; i++) {
      Document doc = new Document();
      doc.add(new StringField("id", "doc" + i, Store.NO));
      long value = random().nextInt();
      doc.add(new BinaryDocValuesField("f", TestBinaryDocValuesUpdates.toBytes(value)));
      doc.add(new NumericDocValuesField("cf", value * 2));
      writer.addDocument(doc);
    }

    int numGens = atLeast(5);
    for (int i = 0; i < numGens; i++) {
      int doc = random().nextInt(numDocs);
      Term t = new Term("id", "doc" + doc);
      long value = random().nextLong();
      if (random().nextBoolean()) {
        doUpdate(
            t,
            writer,
            new BinaryDocValuesField("f", TestBinaryDocValuesUpdates.toBytes(value)),
            new NumericDocValuesField("cf", value * 2));
      } else {
        writer.updateDocValues(
            t,
            new BinaryDocValuesField("f", TestBinaryDocValuesUpdates.toBytes(value)),
            new NumericDocValuesField("cf", value * 2));
      }

      DirectoryReader reader = DirectoryReader.open(writer);
      for (LeafReaderContext context : reader.leaves()) {
        LeafReader r = context.reader();
        BinaryDocValues fbdv = r.getBinaryDocValues("f");
        NumericDocValues cfndv = r.getNumericDocValues("cf");
        for (int j = 0; j < r.maxDoc(); j++) {
          assertEquals(j, cfndv.nextDoc());
          assertEquals(j, fbdv.nextDoc());
          assertEquals(cfndv.longValue(), TestBinaryDocValuesUpdates.getValue(fbdv) * 2);
        }
      }
      reader.close();
    }
    writer.close();
    dir.close();
  }

  @Nightly
  public void testTonsOfUpdates() throws Exception {
    // LUCENE-5248: make sure that when there are many updates, we don't use too much RAM
    Directory dir = newDirectory();
    final Random random = random();
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random));
    conf.setRAMBufferSizeMB(IndexWriterConfig.DEFAULT_RAM_BUFFER_SIZE_MB);
    conf.setMaxBufferedDocs(IndexWriterConfig.DISABLE_AUTO_FLUSH); // don't flush by doc
    IndexWriter writer = new IndexWriter(dir, conf);

    // test data: lots of documents (few 10Ks) and lots of update terms (few hundreds)
    final int numDocs = atLeast(20000);
    final int numBinaryFields = atLeast(5);
    final int numTerms = TestUtil.nextInt(random, 10, 100); // terms should affect many docs
    Set<String> updateTerms = new HashSet<>();
    while (updateTerms.size() < numTerms) {
      updateTerms.add(TestUtil.randomSimpleString(random));
    }

    //    System.out.println("numDocs=" + numDocs + " numBinaryFields=" + numBinaryFields + "
    // numTerms=" + numTerms);

    // build a large index with many BDV fields and update terms
    for (int i = 0; i < numDocs; i++) {
      Document doc = new Document();
      int numUpdateTerms = TestUtil.nextInt(random, 1, numTerms / 10);
      for (int j = 0; j < numUpdateTerms; j++) {
        doc.add(new StringField("upd", RandomPicks.randomFrom(random, updateTerms), Store.NO));
      }
      for (int j = 0; j < numBinaryFields; j++) {
        long val = random.nextInt();
        doc.add(new BinaryDocValuesField("f" + j, TestBinaryDocValuesUpdates.toBytes(val)));
        doc.add(new NumericDocValuesField("cf" + j, val * 2));
      }
      writer.addDocument(doc);
    }

    writer.commit(); // commit so there's something to apply to

    // set to flush every 2048 bytes (approximately every 12 updates), so we get
    // many flushes during binary updates
    writer.getConfig().setRAMBufferSizeMB(2048.0 / 1024 / 1024);
    final int numUpdates = atLeast(100);
    //    System.out.println("numUpdates=" + numUpdates);
    for (int i = 0; i < numUpdates; i++) {
      int field = random.nextInt(numBinaryFields);
      Term updateTerm = new Term("upd", RandomPicks.randomFrom(random, updateTerms));
      long value = random.nextInt();
      writer.updateDocValues(
          updateTerm,
          new BinaryDocValuesField("f" + field, TestBinaryDocValuesUpdates.toBytes(value)),
          new NumericDocValuesField("cf" + field, value * 2));
    }

    writer.close();

    DirectoryReader reader = DirectoryReader.open(dir);
    for (LeafReaderContext context : reader.leaves()) {
      for (int i = 0; i < numBinaryFields; i++) {
        LeafReader r = context.reader();
        BinaryDocValues f = r.getBinaryDocValues("f" + i);
        NumericDocValues cf = r.getNumericDocValues("cf" + i);
        for (int j = 0; j < r.maxDoc(); j++) {
          assertEquals(j, cf.nextDoc());
          assertEquals(j, f.nextDoc());
          assertEquals(
              "reader=" + r + ", field=f" + i + ", doc=" + j,
              cf.longValue(),
              TestBinaryDocValuesUpdates.getValue(f) * 2);
        }
      }
    }
    reader.close();

    dir.close();
  }

  public void testTryUpdateDocValues() throws IOException {
    Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig();
    IndexWriter writer = new IndexWriter(dir, conf);
    int numDocs = 1 + random().nextInt(128);
    for (int i = 0; i < numDocs; i++) {
      Document doc = new Document();
      doc.add(new StringField("id", "" + i, Store.YES));
      doc.add(new NumericDocValuesField("numericId", i));
      doc.add(new BinaryDocValuesField("binaryId", new BytesRef(new byte[] {(byte) i})));
      writer.addDocument(doc);
      if (random().nextBoolean()) {
        writer.flush();
      }
    }
    int doc = random().nextInt(numDocs);
    doUpdate(
        new Term("id", "" + doc),
        writer,
        new NumericDocValuesField("numericId", doc + 1),
        new BinaryDocValuesField("binaryId", new BytesRef(new byte[] {(byte) (doc + 1)})));
    IndexReader reader = DirectoryReader.open(writer);
    NumericDocValues numericIdValues = null;
    BinaryDocValues binaryIdValues = null;
    for (LeafReaderContext c : reader.leaves()) {
      TopDocs topDocs =
          new IndexSearcher(c.reader()).search(new TermQuery(new Term("id", "" + doc)), 10);
      if (topDocs.totalHits.value == 1) {
        assertNull(numericIdValues);
        assertNull(binaryIdValues);
        numericIdValues = c.reader().getNumericDocValues("numericId");
        assertEquals(topDocs.scoreDocs[0].doc, numericIdValues.advance(topDocs.scoreDocs[0].doc));
        binaryIdValues = c.reader().getBinaryDocValues("binaryId");
        assertEquals(topDocs.scoreDocs[0].doc, binaryIdValues.advance(topDocs.scoreDocs[0].doc));
      } else {
        assertEquals(0, topDocs.totalHits.value);
      }
    }

    assertNotNull(numericIdValues);
    assertNotNull(binaryIdValues);

    assertEquals(doc + 1, numericIdValues.longValue());
    assertEquals(new BytesRef(new byte[] {(byte) (doc + 1)}), binaryIdValues.binaryValue());
    IOUtils.close(reader, writer, dir);
  }

  public void testTryUpdateMultiThreaded()
      throws IOException, BrokenBarrierException, InterruptedException {
    Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig();
    IndexWriter writer = new IndexWriter(dir, conf);
    ReentrantLock[] locks = new ReentrantLock[25 + random().nextInt(50)];
    Long[] values = new Long[locks.length];

    for (int i = 0; i < locks.length; i++) {
      locks[i] = new ReentrantLock();
      Document doc = new Document();
      values[i] = random().nextLong();
      doc.add(new StringField("id", Integer.toString(i), Store.NO));
      doc.add(new NumericDocValuesField("value", values[i]));
      writer.addDocument(doc);
    }

    int numThreads = TEST_NIGHTLY ? 2 + random().nextInt(3) : 2;
    Thread[] threads = new Thread[numThreads];
    CyclicBarrier barrier = new CyclicBarrier(threads.length + 1);
    for (int i = 0; i < threads.length; i++) {
      threads[i] =
          new Thread(
              () -> {
                try {
                  barrier.await();
                  for (int doc = 0; doc < 1000; doc++) {
                    int docId = random().nextInt(locks.length);
                    locks[docId].lock();
                    try {
                      Long value = rarely() ? null : random().nextLong(); // sometimes reset it
                      if (random().nextBoolean()) {
                        writer.updateDocValues(
                            new Term("id", docId + ""), new NumericDocValuesField("value", value));
                      } else {
                        doUpdate(
                            new Term("id", docId + ""),
                            writer,
                            new NumericDocValuesField("value", value));
                      }
                      values[docId] = value;
                    } catch (IOException e) {
                      throw new AssertionError(e);
                    } finally {
                      locks[docId].unlock();
                    }
                    if (rarely()) {
                      writer.flush();
                    }
                  }
                } catch (Exception e) {
                  throw new AssertionError(e);
                }
              });
      threads[i].start();
    }

    barrier.await();
    for (Thread t : threads) {
      t.join();
    }
    try (DirectoryReader reader = DirectoryReader.open(writer)) {
      for (int i = 0; i < locks.length; i++) {
        locks[i].lock();
        try {
          Long value = values[i];
          TopDocs topDocs =
              new IndexSearcher(reader).search(new TermQuery(new Term("id", "" + i)), 10);
          assertEquals(topDocs.totalHits.value, 1);
          int docID = topDocs.scoreDocs[0].doc;
          List<LeafReaderContext> leaves = reader.leaves();
          int subIndex = ReaderUtil.subIndex(docID, leaves);
          LeafReader leafReader = leaves.get(subIndex).reader();
          docID -= leaves.get(subIndex).docBase;
          NumericDocValues numericDocValues = leafReader.getNumericDocValues("value");
          if (value == null) {
            assertFalse("docID: " + docID, numericDocValues.advanceExact(docID));
          } else {
            assertTrue("docID: " + docID, numericDocValues.advanceExact(docID));
            assertEquals(numericDocValues.longValue(), value.longValue());
          }
        } finally {
          locks[i].unlock();
        }
      }
    }

    IOUtils.close(writer, dir);
  }

  static void doUpdate(Term doc, IndexWriter writer, Field... fields) throws IOException {
    long seqId = -1;
    do { // retry if we just committing a merge
      try (DirectoryReader reader = DirectoryReader.open(writer)) {
        TopDocs topDocs = new IndexSearcher(reader).search(new TermQuery(doc), 10);
        assertEquals(1, topDocs.totalHits.value);
        int theDoc = topDocs.scoreDocs[0].doc;
        seqId = writer.tryUpdateDocValue(reader, theDoc, fields);
      }
    } while (seqId == -1);
  }

  public void testResetValue() throws Exception {
    Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random()));
    IndexWriter writer = new IndexWriter(dir, conf);
    Document doc = new Document();
    doc.add(new StringField("id", "0", Store.NO));
    doc.add(new NumericDocValuesField("val", 5));
    doc.add(new BinaryDocValuesField("val-bin", new BytesRef(new byte[] {(byte) 5})));
    writer.addDocument(doc);

    if (random().nextBoolean()) {
      writer.commit();
    }
    try (DirectoryReader reader = DirectoryReader.open(writer)) {
      assertEquals(1, reader.leaves().size());
      LeafReader r = reader.leaves().get(0).reader();
      NumericDocValues ndv = r.getNumericDocValues("val");
      assertEquals(0, ndv.nextDoc());
      assertEquals(5, ndv.longValue());
      assertEquals(DocIdSetIterator.NO_MORE_DOCS, ndv.nextDoc());

      BinaryDocValues bdv = r.getBinaryDocValues("val-bin");
      assertEquals(0, bdv.nextDoc());
      assertEquals(new BytesRef(new byte[] {(byte) 5}), bdv.binaryValue());
      assertEquals(DocIdSetIterator.NO_MORE_DOCS, bdv.nextDoc());
    }

    writer.updateDocValues(new Term("id", "0"), new BinaryDocValuesField("val-bin", null));
    try (DirectoryReader reader = DirectoryReader.open(writer)) {
      assertEquals(1, reader.leaves().size());
      LeafReader r = reader.leaves().get(0).reader();
      NumericDocValues ndv = r.getNumericDocValues("val");
      assertEquals(0, ndv.nextDoc());
      assertEquals(5, ndv.longValue());
      assertEquals(DocIdSetIterator.NO_MORE_DOCS, ndv.nextDoc());

      BinaryDocValues bdv = r.getBinaryDocValues("val-bin");
      assertEquals(DocIdSetIterator.NO_MORE_DOCS, bdv.nextDoc());
    }
    IOUtils.close(writer, dir);
  }

  public void testResetValueMultipleDocs() throws Exception {
    Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random()));
    IndexWriter writer = new IndexWriter(dir, conf);
    int numDocs = 10 + random().nextInt(50);
    int currentSeqId = 0;
    int[] seqId = new int[] {-1, -1, -1, -1, -1};
    for (int i = 0; i < numDocs; i++) {
      Document doc = new Document();
      int id = random().nextInt(5);
      seqId[id] = currentSeqId;
      doc.add(new StringField("id", "" + id, Store.YES));
      doc.add(new NumericDocValuesField("seqID", currentSeqId++));
      doc.add(new NumericDocValuesField("is_live", 1));
      if (i > 0) {
        writer.updateDocValues(new Term("id", "" + id), new NumericDocValuesField("is_live", null));
      }
      writer.addDocument(doc);
      if (random().nextBoolean()) {
        writer.flush();
      }
    }

    if (random().nextBoolean()) {
      writer.commit();
    }
    int numHits = 0; // check if every doc has been selected at least once
    for (int i : seqId) {
      if (i > -1) {
        numHits++;
      }
    }
    try (DirectoryReader reader = DirectoryReader.open(writer)) {
      IndexSearcher searcher = new IndexSearcher(reader);

      TopDocs is_live = searcher.search(new FieldExistsQuery("is_live"), 5);
      assertEquals(numHits, is_live.totalHits.value);
      StoredFields storedFields = reader.storedFields();
      for (ScoreDoc doc : is_live.scoreDocs) {
        int id = Integer.parseInt(storedFields.document(doc.doc).get("id"));
        int i = ReaderUtil.subIndex(doc.doc, reader.leaves());
        assertTrue(i >= 0);
        LeafReaderContext leafReaderContext = reader.leaves().get(i);
        NumericDocValues seqID = leafReaderContext.reader().getNumericDocValues("seqID");
        assertNotNull(seqID);
        assertTrue(seqID.advanceExact(doc.doc - leafReaderContext.docBase));
        assertEquals(seqId[id], seqID.longValue());
      }
    }
    IOUtils.close(writer, dir);
  }

  public void testUpdateNotExistingFieldDV() throws IOException {
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random()));
    try (Directory dir = newDirectory();
        IndexWriter writer = new IndexWriter(dir, conf)) {
      Document doc = new Document();
      doc.add(new StringField("id", "1", Store.YES));
      doc.add(new NumericDocValuesField("test", 1));
      writer.addDocument(doc);
      if (random().nextBoolean()) {
        writer.commit();
      }
      writer.updateDocValues(new Term("id", "1"), new NumericDocValuesField("not_existing", 1));

      Document doc1 = new Document();
      doc1.add(new StringField("id", "2", Store.YES));
      doc1.add(new BinaryDocValuesField("not_existing", new BytesRef()));
      IllegalArgumentException iae =
          expectThrows(IllegalArgumentException.class, () -> writer.addDocument(doc1));
      assertEquals(
          "cannot change field \"not_existing\" from doc values type=NUMERIC to inconsistent doc values type=BINARY",
          iae.getMessage());

      iae =
          expectThrows(
              IllegalArgumentException.class,
              () ->
                  writer.updateDocValues(
                      new Term("id", "1"),
                      new BinaryDocValuesField("not_existing", new BytesRef())));
      assertEquals(
          "Can't update [BINARY] doc values; the field [not_existing] has inconsistent doc values' type of [NUMERIC].",
          iae.getMessage());
    }
  }

  public void testUpdateFieldWithNoPreviousDocValuesThrowsError() throws IOException {
    IndexWriterConfig conf = newIndexWriterConfig(new MockAnalyzer(random()));
    try (Directory dir = newDirectory();
        IndexWriter writer = new IndexWriter(dir, conf)) {
      Document doc = new Document();
      doc.add(new StringField("id", "1", Store.YES));
      writer.addDocument(doc);
      if (random().nextBoolean()) {
        try (DirectoryReader reader = DirectoryReader.open(writer)) {
          NumericDocValues id = reader.leaves().get(0).reader().getNumericDocValues("id");
          assertNull(id);
        }
      } else if (random().nextBoolean()) {
        writer.commit();
      }
      IllegalArgumentException exception =
          expectThrows(
              IllegalArgumentException.class,
              () ->
                  writer.updateDocValues(new Term("id", "1"), new NumericDocValuesField("id", 1)));
      assertEquals(
          "Can't update [NUMERIC] doc values; the field [id] has inconsistent doc values' type of [NONE].",
          exception.getMessage());
    }
  }
}
