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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.AccessDeniedException;
import java.nio.file.Path;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.Lock;
import org.apache.lucene.store.LockObtainFailedException;
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.Constants;
import org.apache.lucene.util.PrintStreamInfoStream;

/** Base class for per-LockFactory tests. */
public abstract class BaseLockFactoryTestCase extends LuceneTestCase {

  /**
   * Subclass returns the Directory to be tested; if it's an FS-based directory it should point to
   * the specified path, else it can ignore it.
   */
  protected abstract Directory getDirectory(Path path) throws IOException;

  /** Test obtaining and releasing locks, checking validity */
  public void testBasics() throws IOException {
    Path tempPath = createTempDir();
    Directory dir = getDirectory(tempPath);

    Lock l = dir.obtainLock("commit");
    // shouldn't be able to get the lock twice
    expectThrows(
        LockObtainFailedException.class,
        () -> {
          dir.obtainLock("commit");
        });
    l.close();

    // Make sure we can obtain first one again:
    l = dir.obtainLock("commit");
    l.close();

    dir.close();
  }

  /** Test closing locks twice */
  public void testDoubleClose() throws IOException {
    Path tempPath = createTempDir();
    Directory dir = getDirectory(tempPath);

    Lock l = dir.obtainLock("commit");
    l.close();
    l.close(); // close again, should be no exception

    dir.close();
  }

  /** Test ensureValid returns true after acquire */
  public void testValidAfterAcquire() throws IOException {
    Path tempPath = createTempDir();
    Directory dir = getDirectory(tempPath);
    Lock l = dir.obtainLock("commit");
    l.ensureValid(); // no exception
    l.close();
    dir.close();
  }

  /** Test ensureValid throws exception after close */
  public void testInvalidAfterClose() throws IOException {
    Path tempPath = createTempDir();
    Directory dir = getDirectory(tempPath);

    Lock l = dir.obtainLock("commit");
    l.close();

    expectThrows(
        AlreadyClosedException.class,
        () -> {
          l.ensureValid();
        });

    dir.close();
  }

  public void testObtainConcurrently() throws InterruptedException, IOException {
    Path tempPath = createTempDir();
    final Directory directory = getDirectory(tempPath);
    final AtomicBoolean running = new AtomicBoolean(true);
    final AtomicInteger atomicCounter = new AtomicInteger(0);
    final ReentrantLock assertingLock = new ReentrantLock();
    int numThreads = 2 + random().nextInt(10);
    final int runs = atLeast(1000);
    CyclicBarrier barrier = new CyclicBarrier(numThreads);
    Thread[] threads = new Thread[numThreads];
    for (int i = 0; i < threads.length; i++) {
      threads[i] =
          new Thread() {
            @Override
            public void run() {
              try {
                barrier.await();
              } catch (Exception e) {
                throw new RuntimeException(e);
              }
              while (running.get()) {
                try (Lock lock = directory.obtainLock("foo.lock")) {
                  assertFalse(assertingLock.isLocked());
                  if (assertingLock.tryLock()) {
                    assertingLock.unlock();
                  } else {
                    fail();
                  }
                  assert lock != null; // stupid compiler
                } catch (
                    @SuppressWarnings("unused")
                    IOException ex) {
                  //
                }
                if (atomicCounter.incrementAndGet() > runs) {
                  running.set(false);
                }
              }
            }
          };
      threads[i].start();
    }

    for (int i = 0; i < threads.length; i++) {
      threads[i].join();
    }
    directory.close();
  }

  // Verify: do stress test, by opening IndexReaders and
  // IndexWriters over & over in 2 threads and making sure
  // no unexpected exceptions are raised:
  public void testStressLocks() throws Exception {
    Path tempPath = createTempDir();
    assumeFalse("cannot handle buggy Files.delete", TestUtil.hasWindowsFS(tempPath));

    Directory dir = getDirectory(tempPath);

    // First create a 1 doc index:
    IndexWriter w =
        new IndexWriter(
            dir, new IndexWriterConfig(new MockAnalyzer(random())).setOpenMode(OpenMode.CREATE));
    addDoc(w);
    w.close();

    int numIterations = atLeast(20);
    WriterThread writer = new WriterThread(numIterations, dir);
    SearcherThread searcher = new SearcherThread(numIterations, dir);
    writer.start();
    searcher.start();

    writer.join();
    searcher.join();

    assertTrue("IndexWriter hit unexpected exceptions", !writer.hitException);
    assertTrue("IndexSearcher hit unexpected exceptions", !searcher.hitException);

    dir.close();
  }

  private void addDoc(IndexWriter writer) throws IOException {
    Document doc = new Document();
    doc.add(newTextField("content", "aaa", Field.Store.NO));
    writer.addDocument(doc);
  }

  private class WriterThread extends Thread {
    private Directory dir;
    private int numIteration;
    public boolean hitException = false;

    public WriterThread(int numIteration, Directory dir) {
      this.numIteration = numIteration;
      this.dir = dir;
    }

    private String toString(ByteArrayOutputStream baos) {
      try {
        return baos.toString("UTF8");
      } catch (UnsupportedEncodingException uee) {
        // shouldn't happen
        throw new RuntimeException(uee);
      }
    }

    @Override
    public void run() {
      IndexWriter writer = null;
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      for (int i = 0; i < this.numIteration; i++) {
        if (VERBOSE) {
          System.out.println("TEST: WriterThread iter=" + i);
        }

        IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));

        // We only print the IW infoStream output on exc, below:
        PrintStream printStream;
        try {
          printStream = new PrintStream(baos, true, "UTF8");
        } catch (UnsupportedEncodingException uee) {
          // shouldn't happen
          throw new RuntimeException(uee);
        }

        iwc.setInfoStream(new PrintStreamInfoStream(printStream));

        printStream.println("\nTEST: WriterThread iter=" + i);
        iwc.setOpenMode(OpenMode.APPEND);
        try {
          writer = new IndexWriter(dir, iwc);

        } catch (Throwable t) {
          if (Constants.WINDOWS && t instanceof AccessDeniedException) {
            // LUCENE-6684: suppress this: on Windows, a file in the curious "pending delete" state
            // can
            // cause this exc on IW init, where one thread/process deleted an old
            // segments_N, but the delete hasn't finished yet because other threads/processes
            // still have it open
            printStream.println("TEST: AccessDeniedException on init writer");
            t.printStackTrace(printStream);
          } else {
            hitException = true;
            System.out.println(
                "Stress Test Index Writer: creation hit unexpected exception: " + t.toString());
            t.printStackTrace(System.out);
            System.out.println(toString(baos));
          }
          break;
        }
        try {
          addDoc(writer);
        } catch (Throwable t) {
          hitException = true;
          System.out.println(
              "Stress Test Index Writer: addDoc hit unexpected exception: " + t.toString());
          t.printStackTrace(System.out);
          System.out.println(toString(baos));
          break;
        }
        try {
          writer.close();
        } catch (Throwable t) {
          hitException = true;
          System.out.println(
              "Stress Test Index Writer: close hit unexpected exception: " + t.toString());
          t.printStackTrace(System.out);
          System.out.println(toString(baos));
          break;
        }
      }
    }
  }

  private static class SearcherThread extends Thread {
    private Directory dir;
    private int numIteration;
    public boolean hitException = false;

    public SearcherThread(int numIteration, Directory dir) {
      this.numIteration = numIteration;
      this.dir = dir;
    }

    @Override
    public void run() {
      IndexReader reader = null;
      IndexSearcher searcher = null;
      Query query = new TermQuery(new Term("content", "aaa"));
      for (int i = 0; i < this.numIteration; i++) {
        try {
          reader = DirectoryReader.open(dir);
          searcher = newSearcher(reader);
        } catch (Exception e) {
          hitException = true;
          System.out.println(
              "Stress Test Index Searcher: create hit unexpected exception: " + e.toString());
          e.printStackTrace(System.out);
          break;
        }
        try {
          searcher.search(query, 1000);
        } catch (IOException e) {
          hitException = true;
          System.out.println(
              "Stress Test Index Searcher: search hit unexpected exception: " + e.toString());
          e.printStackTrace(System.out);
          break;
        }
        // System.out.println(hits.length() + " total results");
        try {
          reader.close();
        } catch (IOException e) {
          hitException = true;
          System.out.println(
              "Stress Test Index Searcher: close hit unexpected exception: " + e.toString());
          e.printStackTrace(System.out);
          break;
        }
      }
    }
  }
}
