/*
 * 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.io.IOException;
import java.nio.file.Path;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.FilterDirectory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestCrashCausesCorruptIndex extends LuceneTestCase {

  Path path;

  /** LUCENE-3627: This test fails. */
  public void testCrashCorruptsIndexing() throws Exception {
    path = createTempDir("testCrashCorruptsIndexing");

    indexAndCrashOnCreateOutputSegments2();

    searchForFleas(2);

    indexAfterRestart();

    searchForFleas(3);
  }

  /**
   * index 1 document and commit. prepare for crashing. index 1 more document, and upon commit,
   * creation of segments_2 will crash.
   */
  private void indexAndCrashOnCreateOutputSegments2() throws IOException {
    Directory realDirectory = FSDirectory.open(path);
    CrashAfterCreateOutput crashAfterCreateOutput = new CrashAfterCreateOutput(realDirectory);

    // NOTE: cannot use RandomIndexWriter because it
    // sometimes commits:
    IndexWriter indexWriter =
        new IndexWriter(crashAfterCreateOutput, newIndexWriterConfig(new MockAnalyzer(random())));

    indexWriter.addDocument(getDocument());
    // writes segments_1:
    indexWriter.commit();

    crashAfterCreateOutput.setCrashAfterCreateOutput("pending_segments_2");
    indexWriter.addDocument(getDocument());
    // tries to write segments_2 but hits fake exc:
    expectThrows(
        CrashingException.class,
        () -> {
          indexWriter.commit();
        });

    // writes segments_3
    indexWriter.close();
    assertFalse(slowFileExists(realDirectory, "segments_2"));
    crashAfterCreateOutput.close();
  }

  /** Attempts to index another 1 document. */
  private void indexAfterRestart() throws IOException {
    Directory realDirectory = newFSDirectory(path);

    // LUCENE-3627 (before the fix): this line fails because
    // it doesn't know what to do with the created but empty
    // segments_2 file
    IndexWriter indexWriter =
        new IndexWriter(realDirectory, newIndexWriterConfig(new MockAnalyzer(random())));

    // currently the test fails above.
    // however, to test the fix, the following lines should pass as well.
    indexWriter.addDocument(getDocument());
    indexWriter.close();
    assertFalse(slowFileExists(realDirectory, "segments_2"));
    realDirectory.close();
  }

  /** Run an example search. */
  private void searchForFleas(final int expectedTotalHits) throws IOException {
    Directory realDirectory = newFSDirectory(path);
    IndexReader indexReader = DirectoryReader.open(realDirectory);
    IndexSearcher indexSearcher = newSearcher(indexReader);
    TopDocs topDocs = indexSearcher.search(new TermQuery(new Term(TEXT_FIELD, "fleas")), 10);
    assertNotNull(topDocs);
    assertEquals(expectedTotalHits, topDocs.totalHits.value);
    indexReader.close();
    realDirectory.close();
  }

  private static final String TEXT_FIELD = "text";

  /** Gets a document with content "my dog has fleas". */
  private Document getDocument() {
    Document document = new Document();
    document.add(newTextField(TEXT_FIELD, "my dog has fleas", Field.Store.NO));
    return document;
  }

  /** The marker RuntimeException that we use in lieu of an actual machine crash. */
  private static class CrashingException extends RuntimeException {
    public CrashingException(String msg) {
      super(msg);
    }
  }

  /**
   * This test class provides direct access to "simulating" a crash right after
   * realDirectory.createOutput(..) has been called on a certain specified name.
   */
  private static class CrashAfterCreateOutput extends FilterDirectory {

    private String crashAfterCreateOutput;

    public CrashAfterCreateOutput(Directory realDirectory) throws IOException {
      super(realDirectory);
    }

    public void setCrashAfterCreateOutput(String name) {
      this.crashAfterCreateOutput = name;
    }

    @Override
    public IndexOutput createOutput(String name, IOContext cxt) throws IOException {
      IndexOutput indexOutput = in.createOutput(name, cxt);
      if (null != crashAfterCreateOutput && name.equals(crashAfterCreateOutput)) {
        // CRASH!
        indexOutput.close();
        if (VERBOSE) {
          System.out.println("TEST: now crash");
          new Throwable().printStackTrace(System.out);
        }
        throw new CrashingException("crashAfterCreateOutput " + crashAfterCreateOutput);
      }
      return indexOutput;
    }
  }
}
