/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.quicksetup.util;

import static org.testng.Assert.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

import org.opends.quicksetup.ApplicationException;
import org.opends.quicksetup.Constants;
import org.opends.quicksetup.QuickSetupTestCase;
import org.opends.quicksetup.TestUtilities;
import org.opends.server.util.StaticUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * FileManager Tester.
 */
@SuppressWarnings("javadoc")
@Test(groups = {"slow"}, sequential=true)
public class FileManagerTest extends QuickSetupTestCase {

  private File fmWorkspace;
  private FileManager fileManager;

  @BeforeClass
  public void setUp() throws Exception {
    fileManager = new FileManager();
    fmWorkspace = new File(TestUtilities.getQuickSetupTestWorkspace(),
            "filemanagertests");
    if (fmWorkspace.exists()) {
      fileManager.deleteRecursively(fmWorkspace);
    }
    if (!fmWorkspace.mkdir()) {
      throw new IllegalStateException("cannot create FileManagerTest workspace");
    }
  }

  @AfterMethod
  public void cleanupWorkspace() throws Exception {
    String[] children = fmWorkspace.list();
    if (children != null) {
      for (String child : children) {
        fileManager.deleteRecursively(new File(fmWorkspace, child));
      }
    }
  }

  /**
   * Tests synchronized.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testSynchronize() throws Exception {
    File s = new File(fmWorkspace, "s");
    File t = new File(fmWorkspace, "t");

    String childOfS = "child of s";
    String childOfT = "child of t";

    createSourceFiles(s, childOfS);
    createSourceFiles(t, childOfT);

    File t_d1 = new File(t, "d1");
    File t_f1a = new File(t_d1, "f1a");
    File t_f1b = new File(t_d1, "f1b");

    File t_d2 = new File(t, "d2");
    File t_f2a = new File(t_d2, "f2a");
    File t_d2b = new File(t_d2, "d2b");
    File t_f2b1 = new File(t_d2b, "f2b1");

    assertTrue(t_d1.exists());
    assertTrue(t_f1a.exists());
    assertTrue(t_f1b.exists());

    assertTrue(t_d2.exists());
    assertTrue(t_f2a.exists());
    assertTrue(t_d2b.exists());
    assertTrue(t_f2b1.exists());

    assertTrue(t_f2b1.delete());
    assertTrue(t_d2b.delete());
    assertTrue(t_f1a.delete());

    fileManager.synchronize(s, t);

    // Make sure files we deleted above were copied from the source dir
    assertTrue(t_f2b1.exists());
    assertEquals(childOfS, contentsOf(t_f2b1));

    assertTrue(t_d2b.exists());

    assertTrue(t_f1a.exists());
    assertEquals(childOfS, contentsOf(t_f1a));

    // Make sure files that pre-existed didn't get touched
    assertEquals(childOfT, contentsOf(t_f1b));
    assertEquals(childOfT, contentsOf(t_f2a));
  }

  /** Tests the rename. */
  @Test
  public void testRenameNonExistentTarget() throws Exception {
    File src = File.createTempFile("src", null);
    File target = new File(src.getParentFile(), "target");
    try {
      if (target.exists()) {
        target.delete();
        assertFalse(target.exists());
      }
      fileManager.rename(src, target);
      assertFalse(src.exists());
      assertTrue(target.exists());
    } finally {
      src.delete();
      target.delete();
    }
  }

  /**
   * Tests the {@link StaticUtils#renameFile(java.io.File, java.io.File)}
   * method.
   *
   * @throws Exception If the test failed unexpectedly.
   */
  @Test(enabled = false)
  public void testRenameFileExistentTarget() throws Exception {
    File src = File.createTempFile("src", null);
    File target = File.createTempFile("target", null);
    try {
      StaticUtils.renameFile(src, target);
      assertFalse(src.exists());
      assertTrue(target.exists());
    } finally {
      src.delete();
      target.delete();
    }
  }

  /**
   * Tests the {@link StaticUtils#renameFile(java.io.File, java.io.File)}
   * method.  Renaming locked files is a problem on Windows but not so
   * much on other platforms.
   *
   * @throws Exception If the test failed unexpectedly.
   */
  @Test(enabled = false, groups={"windows"}, expectedExceptions=IOException.class)
  public void testRenameFileLockedTarget() throws Exception {
    File src = File.createTempFile("src", null);
    File target = File.createTempFile("target", null);
    FileChannel c = new RandomAccessFile(target, "rw").getChannel();
    FileLock lock = c.lock();
    try {
      StaticUtils.renameFile(src, target);
    } finally {
      lock.release();
      src.delete();
      target.delete();
    }
  }

  /**
   * Tests basic move.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testMove() throws Exception {

    File fromDir = new File(fmWorkspace, "from");
    fromDir.mkdir();

    File toDir = new File(fmWorkspace, "to");
    toDir.mkdir();

    createSourceFiles(fromDir, "abc");

    fileManager.move(fromDir, toDir);

    assertFalse(fromDir.exists());
    assertTrue(new File(toDir, fromDir.getName()).exists());
  }

  /**
   * Tests basic move with filtering.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testMove2() throws Exception {
    File fromDir = new File(fmWorkspace, "from");
    fromDir.mkdir();

    File toDir = new File(fmWorkspace, "to");
    toDir.mkdir();

    createSourceFiles(fromDir, "abc");

    // Create a filter that should reject the operation
    FileFilter filter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        return false;
      }
    };
    fileManager.move(fromDir, toDir, filter);

    assertTrue(fromDir.exists());
    assertFalse(new File(toDir, fromDir.getName()).exists());
  }

  /**
   * Tests basic delete.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testDelete() throws Exception {
    File dir = new File(fmWorkspace, "dir");
    dir.mkdir();
    assertTrue(dir.exists());
    fileManager.delete(dir);
    assertFalse(dir.exists());

    File file = new File(fmWorkspace, "file");
    writeContents(file, "abc");
    assertTrue(file.exists());
    fileManager.delete(file);
    assertFalse(file.exists());
  }

  /**
   * Tests basic delete with filtering.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testDelete2() throws Exception {

    // Create a filter that should reject the operation
    FileFilter filter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        return false;
      }
    };

    File dir = new File(fmWorkspace, "dir");
    dir.mkdir();
    assertTrue(dir.exists());
    fileManager.delete(dir, filter);
    assertTrue(dir.exists());

    File file = new File(fmWorkspace, "file");
    writeContents(file, "abc");
    assertTrue(file.exists());
    fileManager.delete(file, filter);
    assertTrue(file.exists());
  }

  /**
   * Test recursive delete.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testDeleteRecursively() throws Exception {
    File dir = new File(fmWorkspace, "dir");
    createSourceFiles(dir, "abc");
    assertTrue(dir.exists());
    fileManager.deleteRecursively(dir);
    assertFalse(dir.exists());
  }

  /**
   * Test recursive delete with filtering.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testDeleteRecursively1() throws Exception {
    File dir = new File(fmWorkspace, "dir");
    createSourceFiles(dir, "abc");
    File d2 = new File(dir, "d2");
    File d2b = new File(d2, "d2b");
    final File f2b1 = new File(d2b, "f2b1");

    // Test that a filter can stop a delete altogether
    FileFilter rejectOpFilter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        return false;
      }
    };
    int childCount = countSelfAndAllChildren(dir);
    assertTrue(dir.exists());
    fileManager.deleteRecursively(dir, rejectOpFilter,
            FileManager.DeletionPolicy.DELETE_IMMEDIATELY);
    assertTrue(dir.exists());
    // Make sure we didn't lose any kids
    assertEquals(childCount, countSelfAndAllChildren(dir));

    // Test that using a filter to delete one file works
    FileFilter killChildFileFilter = new FileFilter() {
      @Override
      public boolean accept(File f) {
        return f.equals(f2b1);
      }
    };
    assertTrue(dir.exists());
    assertTrue(f2b1.exists());
    fileManager.deleteRecursively(dir, killChildFileFilter,
            FileManager.DeletionPolicy.DELETE_IMMEDIATELY);
    assertTrue(dir.exists());
    assertEquals(childCount - 1, countSelfAndAllChildren(dir));
    assertFalse(f2b1.exists());
  }

  /**
   * Test basic copy.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testCopy() throws Exception {
    File file = new File(fmWorkspace, "file");
    writeContents(file, "abc");
    File dir = new File(fmWorkspace, "dir");
    String[] children = dir.list();
    assertTrue(children == null || children.length == 0);
    fileManager.copy(file, dir);
    assertTrue(file.exists());
    assertEquals(dir.list().length, 1);
  }

  /**
   * Make sure things fail if target is a file and not a directory.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false, expectedExceptions = ApplicationException.class)
  public void testCopy1() throws Exception {
    File file = new File(fmWorkspace, "file");
    writeContents(file, "abc");
    File file2 = new File(fmWorkspace, "file2");
    writeContents(file2, "abc");
    fileManager.copy(file, file2);
  }

  /**
   * Make sure things fail if target is a file and not a directory.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testCopy2() throws Exception {
    File file = new File(fmWorkspace, "file");
    String NEW_CHILD_CONTENT = "new";
    writeContents(file, NEW_CHILD_CONTENT);
    File dir = new File(fmWorkspace, "dir");
    dir.mkdir();
    File dirChild = new File(dir, "file");
    String ORIGINAL_CHILD_CONTENT = "orinld";
    writeContents(dirChild, ORIGINAL_CHILD_CONTENT);

    // Try a copy without overwriting and make sure the original file didn't get replaced.
    fileManager.copy(file, dir, false);
    assertEquals(contentsOf(dirChild), ORIGINAL_CHILD_CONTENT);

    // New try a copy with overwrite true and make sure the original file got replaced.
    fileManager.copy(file, dir, true);
    assertEquals(contentsOf(dirChild), NEW_CHILD_CONTENT);
  }

  /**
   * Test copy recursively.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testCopyRecursively() throws Exception {
    File source = new File(fmWorkspace, "source");
    createSourceFiles(source, "abc");
    int count = countSelfAndAllChildren(source);

    File dest = new File(fmWorkspace, "dest");
    fileManager.copyRecursively(source, dest);

    File copiedSource = new File(dest, "source");
    assertTrue(copiedSource.exists());
    assertEquals(count, countSelfAndAllChildren(copiedSource));
  }

  /**
   * Tests copy recursively with filtering.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testCopyRecursively1() throws Exception {
    // Test that a filter can stop a delete altogether
    FileFilter rejectOpFilter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        return false;
      }
    };

    File source = new File(fmWorkspace, "source");
    createSourceFiles(source, "abc");
    File d2 = new File(source, "d2");
    File d2b = new File(d2, "d2b");
    final File f2b1 = new File(d2b, "f2b1");
    File dest = new File(fmWorkspace, "dest");

    // Make sure a copy with a rejection filter doesn't do anything
    fileManager.copyRecursively(source, dest, rejectOpFilter);

    File copiedSource = new File(dest, "source");
    assertFalse(copiedSource.exists());
    assertEquals(countSelfAndAllChildren(dest), 1);

    // Test that using a filter to delete one file works
    FileFilter copyChildFileFilter = new FileFilter() {
      @Override
      public boolean accept(File f) {
        return f.equals(f2b1);
      }
    };
    fileManager.copyRecursively(source, dest, copyChildFileFilter);
    File copiedD2 = new File(copiedSource, "d2");
    File copiedD2b = new File(d2, "d2b");
    final File copiedF2b1 = new File(d2b, "f2b1");
    assertTrue(copiedSource.exists());
    assertEquals(countSelfAndAllChildren(copiedSource), 4);
    assertTrue(copiedD2.exists());
    assertEquals(countSelfAndAllChildren(copiedD2), 3);
    assertTrue(copiedD2b.exists());
    assertEquals(countSelfAndAllChildren(copiedD2b), 2);
    assertTrue(copiedF2b1.exists());
  }

  /**
   * Tests copy recursively with filtering and overwrite.
   * @throws Exception if something unexpected
   */
  @Test(enabled = false)
  public void testCopyRecursively2() throws Exception {
    File source = new File(fmWorkspace, "source");
    String FILES_TO_COPY = "to copy";
    createSourceFiles(source, FILES_TO_COPY);
    File d2 = new File(source, "d2");
    File d2b = new File(d2, "d2b");
    final File f2b1 = new File(d2b, "f2b1");
    File dest = new File(fmWorkspace, "dest");

    File copiedSource = new File(dest, "source");
    File copiedD2 = new File(copiedSource, "d2");
    copiedD2.mkdir();
    File copiedD2b = new File(d2, "d2b");
    copiedD2b.mkdir();
    final File copiedF2b1 = new File(d2b, "f2b1");
    String ORIGINAL = "original";
    writeContents(copiedF2b1, ORIGINAL);

    // Test that using a filter to delete one file works
    FileFilter copyChildFileFilter = new FileFilter() {
      @Override
      public boolean accept(File f) {
        return f.equals(f2b1);
      }
    };
    // With overwrite off make sure it doesn't get replaced
    fileManager.copyRecursively(source, dest, copyChildFileFilter, false);
    assertEquals(ORIGINAL, contentsOf(copiedF2b1));

    // Now with overwrite make sure it gets replaced.
    fileManager.copyRecursively(source, dest, copyChildFileFilter, true);
    assertEquals(ORIGINAL, contentsOf(copiedF2b1));
  }

  @DataProvider(name = "differTestData")
  public Object[][] differTestData() {
    return new Object[][] {
      new Object[] { "abc", "abc" },
      new Object[] { "abc", "xyz" },
      new Object[] { "abc", "abc\n" },
      new Object[] { "abc\n", "abc\n" },
      new Object[] { "abc\nabc", "abc\nabc" },
      new Object[] { "abc\nabc\nabc", "abc\nabc\nabc" }
    };
  }

  @Test(dataProvider = "differTestData")
  public void testFilesDiffer(String contents1, String contents2)
          throws Exception
  {
    File f1 = new File(fmWorkspace, "f1");
    File f2 = new File(fmWorkspace, "f2");
    writeContents(f1, contents1);
    writeContents(f2, contents2);
    if (!contents1.equals(contents2)) {
      assertTrue(fileManager.filesDiffer(f1, f2),
              "File contents '" + contents1 + "' and '" + contents2 + "' are " +
                      "not equal despite what FileManager claims");
    } else {
      assertFalse(fileManager.filesDiffer(f1, f2),
              "File contents '" + contents1 + "' and '" + contents2 + "' are " +
                      "equal despite what FileManager claims");
    }
    f1.delete();
    f2.delete();
  }

  /**
   * Creates a set of file for testing.
   * @param parent of the files.
   * @param childContent content of non-directory files.
   * @throws IOException if something unexpected
   */
  private void createSourceFiles(File parent, String childContent)
          throws IOException {
    if (!parent.exists()) {
      parent.mkdir();
    }
    File d1 = new File(parent, "d1");
    File f1a = new File(d1, "f1a");
    File f1b = new File(d1, "f1b");

    File d2 = new File(parent, "d2");
    File f2a = new File(d2, "f2a");
    File d2b = new File(d2, "d2b");
    File f2b1 = new File(d2b, "f2b1");

    d1.mkdir();
    d2.mkdir();
    d2b.mkdir();

    writeContents(f1a, childContent);
    writeContents(f1b, childContent);
    writeContents(f2a, childContent);
    writeContents(f2b1, childContent);
  }

  private void writeContents(File f, String content) throws IOException {
    if (!f.exists())
    {
      f.createNewFile();
    }
    FileWriter fw = new FileWriter(f);
    fw.write(content);
    fw.flush();
    fw.close();
  }

  private String contentsOf(File f) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(f));
    StringBuilder sb = new StringBuilder();
    String s;
    s = br.readLine();
    while (s != null) {
      sb.append(s);
      s = br.readLine();
      if (s != null) {
        sb.append(Constants.LINE_SEPARATOR);
      }
    }
    br.close();
    return sb.toString();
  }

  private int countSelfAndAllChildren(File f) {
    int count = 0;
    if (f != null) {
      count = 1;
      if (f.isDirectory()) {
        File[] children = f.listFiles();
        if (children != null) {
          for (File child : children) {
            count += countSelfAndAllChildren(child);
          }
        }
      }
    }
    return count;
  }
}
