/*
 * 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 static java.nio.charset.StandardCharsets.UTF_8;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Random;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.codecs.Codec;
import org.apache.lucene.document.BinaryDocValuesField;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.analysis.CrankyTokenFilter;
import org.apache.lucene.tests.analysis.MockTokenizer;
import org.apache.lucene.tests.analysis.MockVariableLengthPayloadFilter;
import org.apache.lucene.tests.codecs.asserting.AssertingCodec;
import org.apache.lucene.tests.codecs.cranky.CrankyCodec;
import org.apache.lucene.tests.store.MockDirectoryWrapper;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressCodecs;
import org.apache.lucene.tests.util.Rethrow;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.IOUtils;

/**
 * Causes a bunch of non-aborting and aborting exceptions and checks that no index corruption is
 * ever created
 */
@SuppressCodecs("SimpleText")
public class TestIndexWriterExceptions2 extends LuceneTestCase {

  // just one thread, serial merge policy, hopefully debuggable
  public void testBasics() throws Exception {
    // disable slow things: we don't rely upon sleeps here.
    Directory dir = newDirectory();
    if (dir instanceof MockDirectoryWrapper) {
      ((MockDirectoryWrapper) dir).setThrottling(MockDirectoryWrapper.Throttling.NEVER);
      ((MockDirectoryWrapper) dir).setUseSlowOpenClosers(false);
    }

    // log all exceptions we hit, in case we fail (for debugging)
    ByteArrayOutputStream exceptionLog = new ByteArrayOutputStream();
    PrintStream exceptionStream = new PrintStream(exceptionLog, true, UTF_8);
    // PrintStream exceptionStream = System.out;

    // create lots of non-aborting exceptions with a broken analyzer
    final long analyzerSeed = random().nextLong();
    Analyzer analyzer =
        new Analyzer() {
          @Override
          protected TokenStreamComponents createComponents(String fieldName) {
            MockTokenizer tokenizer = new MockTokenizer(MockTokenizer.SIMPLE, false);
            tokenizer.setEnableChecks(
                false); // TODO: can we turn this on? our filter is probably too evil
            TokenStream stream = tokenizer;
            // emit some payloads
            if (fieldName.contains("payloads")) {
              stream = new MockVariableLengthPayloadFilter(new Random(analyzerSeed), stream);
            }
            stream = new CrankyTokenFilter(stream, new Random(analyzerSeed));
            return new TokenStreamComponents(tokenizer, stream);
          }
        };

    // create lots of aborting exceptions with a broken codec
    // we don't need a random codec, as we aren't trying to find bugs in the codec here.
    Codec inner = RANDOM_MULTIPLIER > 1 ? Codec.getDefault() : new AssertingCodec();
    Codec codec = new CrankyCodec(inner, new Random(random().nextLong()));

    IndexWriterConfig conf = newIndexWriterConfig(analyzer);
    // just for now, try to keep this test reproducible
    conf.setMergeScheduler(new SerialMergeScheduler());
    conf.setCodec(codec);

    int numDocs = atLeast(100);

    IndexWriter iw = new IndexWriter(dir, conf);
    try {
      boolean allowAlreadyClosed = false;
      for (int i = 0; i < numDocs; i++) {
        // TODO: add crankyDocValuesFields, etc
        Document doc = new Document();
        doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
        doc.add(new NumericDocValuesField("dv", i));
        doc.add(new BinaryDocValuesField("dv2", new BytesRef(Integer.toString(i))));
        doc.add(new SortedDocValuesField("dv3", new BytesRef(Integer.toString(i))));
        doc.add(new SortedSetDocValuesField("dv4", new BytesRef(Integer.toString(i))));
        doc.add(new SortedSetDocValuesField("dv4", new BytesRef(Integer.toString(i - 1))));
        doc.add(new SortedNumericDocValuesField("dv5", i));
        doc.add(new SortedNumericDocValuesField("dv5", i - 1));
        doc.add(
            newTextField(
                "text1", TestUtil.randomAnalysisString(random(), 20, true), Field.Store.NO));
        // ensure we store something
        doc.add(new StoredField("stored1", "foo"));
        doc.add(new StoredField("stored1", "bar"));
        // ensure we get some payloads
        doc.add(
            newTextField(
                "text_payloads", TestUtil.randomAnalysisString(random(), 6, true), Field.Store.NO));
        // ensure we get some vectors
        FieldType ft = new FieldType(TextField.TYPE_NOT_STORED);
        ft.setStoreTermVectors(true);
        doc.add(newField("text_vectors", TestUtil.randomAnalysisString(random(), 6, true), ft));
        doc.add(new IntPoint("point", random().nextInt()));
        doc.add(new IntPoint("point2d", random().nextInt(), random().nextInt()));

        if (random().nextInt(10) > 0) {
          // single doc
          try {
            iw.addDocument(doc);
            // we made it, sometimes delete our doc, or update a dv
            int thingToDo = random().nextInt(4);
            if (thingToDo == 0) {
              iw.deleteDocuments(new Term("id", Integer.toString(i)));
            } else if (thingToDo == 1) {
              iw.updateNumericDocValue(new Term("id", Integer.toString(i)), "dv", i + 1L);
            } else if (thingToDo == 2) {
              iw.updateBinaryDocValue(
                  new Term("id", Integer.toString(i)),
                  "dv2",
                  new BytesRef(Integer.toString(i + 1)));
            }
          } catch (
              @SuppressWarnings("unused")
              AlreadyClosedException ace) {
            // OK: writer was closed by abort; we just reopen now:
            assertTrue(iw.isDeleterClosed());
            assertTrue(allowAlreadyClosed);
            allowAlreadyClosed = false;
            conf = newIndexWriterConfig(analyzer);
            // just for now, try to keep this test reproducible
            conf.setMergeScheduler(new SerialMergeScheduler());
            conf.setCodec(codec);
            iw = new IndexWriter(dir, conf);
          } catch (Exception e) {
            if (e.getMessage() != null && e.getMessage().startsWith("Fake IOException")) {
              exceptionStream.println("\nTEST: got expected fake exc:" + e.getMessage());
              e.printStackTrace(exceptionStream);
              allowAlreadyClosed = true;
            } else {
              Rethrow.rethrow(e);
            }
          }
        } else {
          // block docs
          Document doc2 = new Document();
          doc2.add(newStringField("id", Integer.toString(-i), Field.Store.NO));
          doc2.add(
              newTextField(
                  "text1", TestUtil.randomAnalysisString(random(), 20, true), Field.Store.NO));
          doc2.add(new StoredField("stored1", "foo"));
          doc2.add(new StoredField("stored1", "bar"));
          doc2.add(newField("text_vectors", TestUtil.randomAnalysisString(random(), 6, true), ft));

          try {
            iw.addDocuments(Arrays.asList(doc, doc2));
            // we made it, sometimes delete our docs
            if (random().nextBoolean()) {
              iw.deleteDocuments(
                  new Term("id", Integer.toString(i)), new Term("id", Integer.toString(-i)));
            }
          } catch (
              @SuppressWarnings("unused")
              AlreadyClosedException ace) {
            // OK: writer was closed by abort; we just reopen now:
            assertTrue(iw.isDeleterClosed());
            assertTrue(allowAlreadyClosed);
            allowAlreadyClosed = false;
            conf = newIndexWriterConfig(analyzer);
            // just for now, try to keep this test reproducible
            conf.setMergeScheduler(new SerialMergeScheduler());
            conf.setCodec(codec);
            iw = new IndexWriter(dir, conf);
          } catch (Exception e) {
            if (e.getMessage() != null && e.getMessage().startsWith("Fake IOException")) {
              exceptionStream.println("\nTEST: got expected fake exc:" + e.getMessage());
              e.printStackTrace(exceptionStream);
              allowAlreadyClosed = true;
            } else {
              Rethrow.rethrow(e);
            }
          }
        }

        if (random().nextInt(10) == 0) {
          // trigger flush:
          try {
            if (random().nextBoolean()) {
              DirectoryReader ir = null;
              try {
                ir = DirectoryReader.open(iw, random().nextBoolean(), false);
                TestUtil.checkReader(ir);
              } finally {
                IOUtils.closeWhileHandlingException(ir);
              }
            } else {
              iw.commit();
            }
            if (DirectoryReader.indexExists(dir)) {
              TestUtil.checkIndex(dir);
            }
          } catch (
              @SuppressWarnings("unused")
              AlreadyClosedException ace) {
            // OK: writer was closed by abort; we just reopen now:
            assertTrue(iw.isDeleterClosed());
            assertTrue(allowAlreadyClosed);
            allowAlreadyClosed = false;
            conf = newIndexWriterConfig(analyzer);
            // just for now, try to keep this test reproducible
            conf.setMergeScheduler(new SerialMergeScheduler());
            conf.setCodec(codec);
            iw = new IndexWriter(dir, conf);
          } catch (Exception e) {
            if (e.getMessage() != null && e.getMessage().startsWith("Fake IOException")) {
              exceptionStream.println("\nTEST: got expected fake exc:" + e.getMessage());
              e.printStackTrace(exceptionStream);
              allowAlreadyClosed = true;
            } else {
              Rethrow.rethrow(e);
            }
          }
        }
      }

      try {
        iw.close();
      } catch (Exception e) {
        if (e.getMessage() != null && e.getMessage().startsWith("Fake IOException")) {
          exceptionStream.println("\nTEST: got expected fake exc:" + e.getMessage());
          e.printStackTrace(exceptionStream);
          try {
            iw.rollback();
          } catch (
              @SuppressWarnings("unused")
              Throwable t) {
          }
        } else {
          Rethrow.rethrow(e);
        }
      }
      dir.close();
    } catch (Throwable t) {
      System.out.println("Unexpected exception: dumping fake-exception-log:...");
      exceptionStream.flush();
      System.out.println(exceptionLog.toString(UTF_8));
      System.out.flush();
      Rethrow.rethrow(t);
    }

    if (VERBOSE) {
      System.out.println("TEST PASSED: dumping fake-exception-log:...");
      System.out.println(exceptionLog.toString(UTF_8));
    }
  }
}
