/*
 * 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.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import org.apache.lucene.document.Document;
import org.apache.lucene.store.ByteArrayDataInput;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FilterDirectory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.tests.index.RandomIndexWriter;

// See: https://issues.apache.org/jira/browse/SOLR-12028 Tests cannot remove files on Windows
// machines occasionally
public class TestMockDirectoryWrapper extends BaseDirectoryTestCase {

  @Override
  protected Directory getDirectory(Path path) throws IOException {
    final MockDirectoryWrapper dir;
    if (random().nextBoolean()) {
      dir = newMockDirectory();
    } else {
      dir = newMockFSDirectory(path);
    }
    return dir;
  }

  // we wrap the directory in slow stuff, so only run nightly
  @Override
  @Nightly
  public void testThreadSafetyInListAll() throws Exception {
    super.testThreadSafetyInListAll();
  }

  public void testDiskFull() throws IOException {
    // test writeBytes
    MockDirectoryWrapper dir = newMockDirectory();
    dir.setMaxSizeInBytes(3);
    final byte[] bytes = new byte[] {1, 2};
    IndexOutput out = dir.createOutput("foo", IOContext.DEFAULT);
    out.writeBytes(bytes, bytes.length); // first write should succeed
    // close() to ensure the written bytes are not buffered and counted
    // against the directory size
    out.close();

    IndexOutput out2 = dir.createOutput("bar", IOContext.DEFAULT);
    expectThrows(IOException.class, () -> out2.writeBytes(bytes, bytes.length));
    out2.close();
    dir.close();

    // test copyBytes
    dir = newMockDirectory();
    dir.setMaxSizeInBytes(3);
    out = dir.createOutput("foo", IOContext.DEFAULT);
    out.copyBytes(new ByteArrayDataInput(bytes), bytes.length); // first copy should succeed
    // close() to ensure the written bytes are not buffered and counted
    // against the directory size
    out.close();

    IndexOutput out3 = dir.createOutput("bar", IOContext.DEFAULT);
    expectThrows(
        IOException.class, () -> out3.copyBytes(new ByteArrayDataInput(bytes), bytes.length));
    out3.close();
    dir.close();
  }

  public void testMDWinsideOfMDW() throws Exception {
    // add MDW inside another MDW
    Directory dir = new MockDirectoryWrapper(random(), newMockDirectory());
    RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
    for (int i = 0; i < 20; i++) {
      iw.addDocument(new Document());
    }
    iw.commit();
    iw.close();
    dir.close();
  }

  // just shields the wrapped directory from being closed
  private static class PreventCloseDirectoryWrapper extends FilterDirectory {
    public PreventCloseDirectoryWrapper(Directory in) {
      super(in);
    }

    @Override
    public void close() {}
  }

  public void testCorruptOnCloseIsWorkingFSDir() throws Exception {
    Path path = createTempDir();
    try (Directory dir = newFSDirectory(path)) {
      testCorruptOnCloseIsWorking(dir);
    }
  }

  public void testCorruptOnCloseIsWorkingOnByteBuffersDirectory() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      testCorruptOnCloseIsWorking(dir);
    }
  }

  private void testCorruptOnCloseIsWorking(Directory dir) throws Exception {

    dir = new PreventCloseDirectoryWrapper(dir);

    try (MockDirectoryWrapper wrapped = new MockDirectoryWrapper(random(), dir)) {

      // otherwise MDW sometimes randomly leaves the file intact and we'll see false test failures:
      wrapped.alwaysCorrupt = true;

      // MDW will only try to corrupt things if it sees an index:
      RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
      iw.addDocument(new Document());
      iw.close();

      // not sync'd!
      try (IndexOutput out = wrapped.createOutput("foo", IOContext.DEFAULT)) {
        for (int i = 0; i < 100; i++) {
          out.writeInt(i);
        }
      }

      // MDW.close now corrupts our unsync'd file (foo):
    }

    boolean changed = false;
    IndexInput in = null;
    try {
      in = dir.openInput("foo", IOContext.DEFAULT);
    } catch (@SuppressWarnings("unused") NoSuchFileException | FileNotFoundException fnfe) {
      // ok
      changed = true;
    }
    if (in != null) {
      for (int i = 0; i < 100; i++) {
        int x;
        try {
          x = in.readInt();
        } catch (
            @SuppressWarnings("unused")
            EOFException eofe) {
          changed = true;
          break;
        }
        if (x != i) {
          changed = true;
          break;
        }
      }

      in.close();
    }

    assertTrue(
        "MockDirectoryWrapper on dir=" + dir + " failed to corrupt an unsync'd file", changed);
  }

  public void testAbuseClosedIndexInput() throws Exception {
    MockDirectoryWrapper dir = newMockDirectory();
    IndexOutput out = dir.createOutput("foo", IOContext.DEFAULT);
    out.writeByte((byte) 42);
    out.close();
    final IndexInput in = dir.openInput("foo", IOContext.DEFAULT);
    in.close();
    expectThrows(RuntimeException.class, in::readByte);
    dir.close();
  }

  public void testAbuseCloneAfterParentClosed() throws Exception {
    MockDirectoryWrapper dir = newMockDirectory();
    IndexOutput out = dir.createOutput("foo", IOContext.DEFAULT);
    out.writeByte((byte) 42);
    out.close();
    IndexInput in = dir.openInput("foo", IOContext.DEFAULT);
    final IndexInput clone = in.clone();
    in.close();
    expectThrows(RuntimeException.class, clone::readByte);
    dir.close();
  }

  public void testAbuseCloneOfCloneAfterParentClosed() throws Exception {
    MockDirectoryWrapper dir = newMockDirectory();
    IndexOutput out = dir.createOutput("foo", IOContext.DEFAULT);
    out.writeByte((byte) 42);
    out.close();
    IndexInput in = dir.openInput("foo", IOContext.DEFAULT);
    IndexInput clone1 = in.clone();
    IndexInput clone2 = clone1.clone();
    in.close();
    expectThrows(RuntimeException.class, clone2::readByte);
    dir.close();
  }
}
