/*
 * 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 java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;

public class TestIndexWriterFromReader extends LuceneTestCase {

  // Pull NRT reader immediately after writer has committed
  public void testRightAfterCommit() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(1, r.maxDoc());
    w.close();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());

    IndexWriter w2 = new IndexWriter(dir, iwc);
    r.close();

    assertEquals(1, w2.getDocStats().maxDoc);
    w2.addDocument(new Document());
    assertEquals(2, w2.getDocStats().maxDoc);
    w2.close();
    
    IndexReader r2 = DirectoryReader.open(dir);
    assertEquals(2, r2.maxDoc());
    r2.close();
    dir.close();
  }

  // Open from non-NRT reader
  public void testFromNonNRTReader() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.close();

    DirectoryReader r = DirectoryReader.open(dir);
    assertEquals(1, r.maxDoc());
    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());

    IndexWriter w2 = new IndexWriter(dir, iwc);
    assertEquals(1, r.maxDoc());
    r.close();

    assertEquals(1, w2.getDocStats().maxDoc);
    w2.addDocument(new Document());
    assertEquals(2, w2.getDocStats().maxDoc);
    w2.close();
    
    IndexReader r2 = DirectoryReader.open(dir);
    assertEquals(2, r2.maxDoc());
    r2.close();
    dir.close();
  }

  // Pull NRT reader from a writer on a new index with no commit:
  public void testWithNoFirstCommit() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    DirectoryReader r = DirectoryReader.open(w);
    w.rollback();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());

    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
      new IndexWriter(dir, iwc);
    });
    assertEquals("cannot use IndexWriterConfig.setIndexCommit() when index has no commit", expected.getMessage());
      
    r.close();
    dir.close();
  }

  // Pull NRT reader after writer has committed and then indexed another doc:
  public void testAfterCommitThenIndex() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();
    w.addDocument(new Document());

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(2, r.maxDoc());
    w.close();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());

    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
      new IndexWriter(dir, iwc);
    });
    assertTrue(expected.getMessage().contains("the provided reader is stale: its prior commit file"));

    r.close();
    dir.close();
  }

  // NRT rollback: pull NRT reader after writer has committed and then before indexing another doc
  public void testNRTRollback() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(1, r.maxDoc());

    // Add another doc
    w.addDocument(new Document());
    assertEquals(2, w.getDocStats().maxDoc);
    w.close();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());
    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
      new IndexWriter(dir, iwc);
    });
    assertTrue(expected.getMessage().contains("the provided reader is stale: its prior commit file"));      

    r.close();
    dir.close();
  }

  public void testRandom() throws Exception {
    Directory dir = newDirectory();

    int numOps = atLeast(100);

    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());

    // We must have a starting commit for this test because whenever we rollback with
    // an NRT reader, the commit before that NRT reader must exist
    w.commit();

    DirectoryReader r = DirectoryReader.open(w);
    int nrtReaderNumDocs = 0;
    int writerNumDocs = 0;

    boolean commitAfterNRT = false;

    Set<Integer> liveIDs = new HashSet<>();
    Set<Integer> nrtLiveIDs = new HashSet<>();

    for(int op=0;op<numOps;op++) {
      if (VERBOSE) {
        System.out.println("\nITER op=" + op + " nrtReaderNumDocs=" + nrtReaderNumDocs + " writerNumDocs=" + writerNumDocs + " r=" + r + " r.numDocs()=" + r.numDocs());
      }

      assertEquals(nrtReaderNumDocs, r.numDocs());
      int x = random().nextInt(5);

      switch(x) {

      case 0:
        if (VERBOSE) {
          System.out.println("  add doc id=" + op);
        }
        // add doc
        Document doc = new Document();
        doc.add(newStringField("id", ""+op, Field.Store.NO));
        w.addDocument(doc);
        liveIDs.add(op);
        writerNumDocs++;
        break;

      case 1:
        if (VERBOSE) {
          System.out.println("  delete doc");
        }
        // delete docs
        if (liveIDs.size() > 0) {
          int id = random().nextInt(op);
          if (VERBOSE) {
            System.out.println("    id=" + id);
          }
          w.deleteDocuments(new Term("id", ""+id));
          if (liveIDs.remove(id)) {
            if (VERBOSE) {
              System.out.println("    really deleted");
            }
            writerNumDocs--;
          }
        } else {
          if (VERBOSE) {
            System.out.println("    nothing to delete yet");
          }
        }
        break;

      case 2:
        // reopen NRT reader
        if (VERBOSE) {
          System.out.println("  reopen NRT reader");
        }
        DirectoryReader r2 = DirectoryReader.openIfChanged(r);
        if (r2 != null) {
          r.close();
          r = r2;
          if (VERBOSE) {
            System.out.println("    got new reader oldNumDocs=" + nrtReaderNumDocs + " newNumDocs=" + writerNumDocs);
          }
          nrtReaderNumDocs = writerNumDocs;
          nrtLiveIDs = new HashSet<>(liveIDs);
        } else {
          if (VERBOSE) {
            System.out.println("    reader is unchanged");
          }
          assertEquals(nrtReaderNumDocs, r.numDocs());
        }
        commitAfterNRT = false;
        break;

      case 3:
        if (commitAfterNRT == false) {
          // rollback writer to last nrt reader
          if (random().nextBoolean()) {
            if (VERBOSE) {
              System.out.println("  close writer and open new writer from non-NRT reader numDocs=" + w.getDocStats().numDocs);
            }
            w.close();
            r.close();
            r = DirectoryReader.open(dir);
            assertEquals(writerNumDocs, r.numDocs());
            nrtReaderNumDocs = writerNumDocs;
            nrtLiveIDs = new HashSet<>(liveIDs);
          } else {
            if (VERBOSE) {
              System.out.println("  rollback writer and open new writer from NRT reader numDocs=" + w.getDocStats().numDocs);
            }
            w.rollback();
          }
          IndexWriterConfig iwc = newIndexWriterConfig();
          iwc.setIndexCommit(r.getIndexCommit());
          w = new IndexWriter(dir, iwc);
          writerNumDocs = nrtReaderNumDocs;
          liveIDs = new HashSet<>(nrtLiveIDs);
          r.close();
          r = DirectoryReader.open(w);
        }
        break;

      case 4:
        if (VERBOSE) {
          System.out.println("    commit");
        }
        w.commit();
        commitAfterNRT = true;
        break;
      }
    }

    IOUtils.close(w, r, dir);
  }

  public void testConsistentFieldNumbers() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    // Empty first commit:
    w.commit();

    Document doc = new Document();
    doc.add(newStringField("f0", "foo", Field.Store.NO));
    w.addDocument(doc);

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(1, r.maxDoc());

    doc = new Document();
    doc.add(newStringField("f1", "foo", Field.Store.NO));
    w.addDocument(doc);

    DirectoryReader r2 = DirectoryReader.openIfChanged(r);
    assertNotNull(r2);
    r.close();
    assertEquals(2, r2.maxDoc());
    w.rollback();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r2.getIndexCommit());

    IndexWriter w2 = new IndexWriter(dir, iwc);
    r2.close();

    doc = new Document();
    doc.add(newStringField("f1", "foo", Field.Store.NO));
    doc.add(newStringField("f0", "foo", Field.Store.NO));
    w2.addDocument(doc);
    w2.close();
    dir.close();
  }

  public void testInvalidOpenMode() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(1, r.maxDoc());
    w.close();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
    iwc.setIndexCommit(r.getIndexCommit());
    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
      new IndexWriter(dir, iwc);
    });
    assertEquals("cannot use IndexWriterConfig.setIndexCommit() with OpenMode.CREATE", expected.getMessage());

    IOUtils.close(r, dir);
  }

  public void testOnClosedReader() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(1, r.maxDoc());
    IndexCommit commit = r.getIndexCommit();
    r.close();
    w.close();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(commit);
    expectThrows(AlreadyClosedException.class, () -> {
      new IndexWriter(dir, iwc);
    });

    IOUtils.close(r, dir);
  }

  public void testStaleNRTReader() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(1, r.maxDoc());
    w.addDocument(new Document());

    DirectoryReader r2 = DirectoryReader.openIfChanged(r);
    assertNotNull(r2);
    r2.close();
    w.rollback();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());
    w = new IndexWriter(dir, iwc);
    assertEquals(1, w.getDocStats().numDocs);

    r.close();
    DirectoryReader r3 = DirectoryReader.open(w);
    assertEquals(1, r3.numDocs());
    
    w.addDocument(new Document());
    DirectoryReader r4 = DirectoryReader.openIfChanged(r3);
    r3.close();
    assertEquals(2, r4.numDocs());
    r4.close();
    w.close();

    IOUtils.close(r, dir);
  }

  public void testAfterRollback() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
    w.addDocument(new Document());
    w.commit();
    w.addDocument(new Document());

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(2, r.maxDoc());
    w.rollback();

    IndexWriterConfig iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());
    w = new IndexWriter(dir, iwc);
    assertEquals(2, w.getDocStats().numDocs);

    r.close();
    w.close();

    DirectoryReader r2 = DirectoryReader.open(dir);
    assertEquals(2, r2.numDocs());
    IOUtils.close(r2, dir);
  }

  // Pull NRT reader after writer has committed and then indexed another doc:
  public void testAfterCommitThenIndexKeepCommits() throws Exception {
    Directory dir = newDirectory();
    IndexWriterConfig iwc = newIndexWriterConfig();

    // Keep all commits:
    iwc.setIndexDeletionPolicy(new IndexDeletionPolicy() {
        @Override
        public void onInit(List<? extends IndexCommit> commits) {
        }

        @Override
        public void onCommit(List<? extends IndexCommit> commits) {
        }
      });

    IndexWriter w = new IndexWriter(dir, iwc);
    w.addDocument(new Document());
    w.commit();
    w.addDocument(new Document());

    DirectoryReader r = DirectoryReader.open(w);
    assertEquals(2, r.maxDoc());
    w.addDocument(new Document());

    DirectoryReader r2 = DirectoryReader.open(w);
    assertEquals(3, r2.maxDoc());
    IOUtils.close(r2, w);

    // r is not stale because, even though we've committed the original writer since it was open, we are keeping all commit points:
    iwc = newIndexWriterConfig();
    iwc.setIndexCommit(r.getIndexCommit());
    IndexWriter w2 = new IndexWriter(dir, iwc);
    assertEquals(2, w2.getDocStats().maxDoc);
    IOUtils.close(r, w2, dir);
  }
}
