/*
 * 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.analysis.MockAnalyzer;
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.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;
    }

  }
  
}
