/*
 * Copyright 2013 Google Inc.
 *
 * Licensed 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 com.google.common.jimfs;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.common.testing.EqualsTester;
import com.google.common.testing.NullPointerTester;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for {@link JimfsPath}.
 *
 * @author Colin Decker
 */
@RunWith(JUnit4.class)
public class JimfsPathTest {

  private final PathService pathService = PathServiceTest.fakeUnixPathService();

  @Test
  public void testPathParsing() {
    assertPathEquals("/", "/");
    assertPathEquals("/foo", "/foo");
    assertPathEquals("/foo", "/", "foo");
    assertPathEquals("/foo/bar", "/foo/bar");
    assertPathEquals("/foo/bar", "/", "foo", "bar");
    assertPathEquals("/foo/bar", "/foo", "bar");
    assertPathEquals("/foo/bar", "/", "foo/bar");
    assertPathEquals("foo/bar/baz", "foo/bar/baz");
    assertPathEquals("foo/bar/baz", "foo", "bar", "baz");
    assertPathEquals("foo/bar/baz", "foo/bar", "baz");
    assertPathEquals("foo/bar/baz", "foo", "bar/baz");
  }

  @Test
  public void testPathParsing_withExtraSeparators() {
    assertPathEquals("/foo/bar", "///foo/bar");
    assertPathEquals("/foo/bar", "/foo///bar//");
    assertPathEquals("/foo/bar/baz", "/foo", "/bar", "baz/");
    // assertPathEquals("/foo/bar/baz", "/foo\\/bar//\\\\/baz\\/");
  }

  @Test
  public void testPathParsing_windowsStylePaths() throws IOException {
    PathService windowsPathService = PathServiceTest.fakeWindowsPathService();
    assertEquals("C:\\", pathService.parsePath("C:\\").toString());
    assertEquals("C:\\foo", windowsPathService.parsePath("C:\\foo").toString());
    assertEquals("C:\\foo", windowsPathService.parsePath("C:\\", "foo").toString());
    assertEquals("C:\\foo", windowsPathService.parsePath("C:", "\\foo").toString());
    assertEquals("C:\\foo", windowsPathService.parsePath("C:", "foo").toString());
    assertEquals("C:\\foo\\bar", windowsPathService.parsePath("C:", "foo/bar").toString());
  }

  @Test
  public void testParsing_windowsStylePaths_invalidPaths() {
    PathService windowsPathService = PathServiceTest.fakeWindowsPathService();

    try {
      // The actual windows implementation seems to allow "C:" but treat it as a *name*, not a root
      // despite the fact that a : is illegal except in a root... a : at any position other than
      // index 1 in the string will produce an exception.
      // Here, I choose to be more strict
      windowsPathService.parsePath("C:");
      fail();
    } catch (InvalidPathException expected) {
    }

    try {
      // "1:\" isn't a root because 1 isn't a letter
      windowsPathService.parsePath("1:\\foo");
      fail();
    } catch (InvalidPathException expected) {
    }

    try {
      // < and > are reserved characters
      windowsPathService.parsePath("foo<bar>");
      fail();
    } catch (InvalidPathException expected) {
    }
  }

  @Test
  public void testPathParsing_withAlternateSeparator() {
    // windows recognizes / as an alternate separator
    PathService windowsPathService = PathServiceTest.fakeWindowsPathService();
    assertEquals(
        windowsPathService.parsePath("foo\\bar\\baz"), windowsPathService.parsePath("foo/bar/baz"));
    assertEquals(
        windowsPathService.parsePath("C:\\foo\\bar"), windowsPathService.parsePath("C:\\foo/bar"));
    assertEquals(
        windowsPathService.parsePath("c:\\foo\\bar\\baz"),
        windowsPathService.parsePath("c:", "foo/", "bar/baz"));
  }

  @Test
  public void testRootPath() {
    new PathTester(pathService, "/").root("/").test("/");
  }

  @Test
  public void testRelativePath_singleName() {
    new PathTester(pathService, "test").names("test").test("test");

    Path path = pathService.parsePath("test");
    assertEquals(path, path.getFileName());
  }

  @Test
  public void testRelativePath_twoNames() {
    PathTester tester = new PathTester(pathService, "foo/bar").names("foo", "bar");

    tester.test("foo/bar");
  }

  @Test
  public void testRelativePath_fourNames() {
    new PathTester(pathService, "foo/bar/baz/test")
        .names("foo", "bar", "baz", "test")
        .test("foo/bar/baz/test");
  }

  @Test
  public void testAbsolutePath_singleName() {
    new PathTester(pathService, "/foo").root("/").names("foo").test("/foo");
  }

  @Test
  public void testAbsolutePath_twoNames() {
    new PathTester(pathService, "/foo/bar").root("/").names("foo", "bar").test("/foo/bar");
  }

  @Test
  public void testAbsoluteMultiNamePath_fourNames() {
    new PathTester(pathService, "/foo/bar/baz/test")
        .root("/")
        .names("foo", "bar", "baz", "test")
        .test("/foo/bar/baz/test");
  }

  @Test
  public void testResolve_fromRoot() {
    Path root = pathService.parsePath("/");

    assertResolvedPathEquals("/foo", root, "foo");
    assertResolvedPathEquals("/foo/bar", root, "foo/bar");
    assertResolvedPathEquals("/foo/bar", root, "foo", "bar");
    assertResolvedPathEquals("/foo/bar/baz/test", root, "foo/bar/baz/test");
    assertResolvedPathEquals("/foo/bar/baz/test", root, "foo", "bar/baz", "test");
  }

  @Test
  public void testResolve_fromAbsolute() {
    Path path = pathService.parsePath("/foo");

    assertResolvedPathEquals("/foo/bar", path, "bar");
    assertResolvedPathEquals("/foo/bar/baz/test", path, "bar/baz/test");
    assertResolvedPathEquals("/foo/bar/baz/test", path, "bar/baz", "test");
    assertResolvedPathEquals("/foo/bar/baz/test", path, "bar", "baz", "test");
  }

  @Test
  public void testResolve_fromRelative() {
    Path path = pathService.parsePath("foo");

    assertResolvedPathEquals("foo/bar", path, "bar");
    assertResolvedPathEquals("foo/bar/baz/test", path, "bar/baz/test");
    assertResolvedPathEquals("foo/bar/baz/test", path, "bar", "baz", "test");
    assertResolvedPathEquals("foo/bar/baz/test", path, "bar/baz", "test");
  }

  @Test
  public void testResolve_withThisAndParentDirNames() {
    Path path = pathService.parsePath("/foo");

    assertResolvedPathEquals("/foo/bar/../baz", path, "bar/../baz");
    assertResolvedPathEquals("/foo/bar/../baz", path, "bar", "..", "baz");
    assertResolvedPathEquals("/foo/./bar/baz", path, "./bar/baz");
    assertResolvedPathEquals("/foo/./bar/baz", path, ".", "bar/baz");
  }

  @Test
  public void testResolve_givenAbsolutePath() {
    assertResolvedPathEquals("/test", pathService.parsePath("/foo"), "/test");
    assertResolvedPathEquals("/test", pathService.parsePath("foo"), "/test");
  }

  @Test
  public void testResolve_givenEmptyPath() {
    assertResolvedPathEquals("/foo", pathService.parsePath("/foo"), "");
    assertResolvedPathEquals("foo", pathService.parsePath("foo"), "");
  }

  @Test
  public void testResolve_againstEmptyPath() {
    assertResolvedPathEquals("foo/bar", pathService.emptyPath(), "foo/bar");
  }

  @Test
  public void testResolveName_againstEmptyPath() {
    // resolve(Name) is only used in the DirectoryStream implementation, so it's only used to
    // resolve the names of real existing files against some base directory's path. The base
    // directory path could be the working directory path (i.e. just an empty string), in which case
    // we need to be sure to return a path that is just the name of the file as opposed a path with
    // two names, one being the empty string and the other the file name).
    // See https://github.com/google/jimfs/issues/105
    assertPathEquals("foo", pathService.emptyPath().resolve(Name.simple("foo")));
  }

  @Test
  public void testResolveSibling_givenEmptyPath() {
    Path path = pathService.parsePath("foo/bar");
    Path resolved = path.resolveSibling("");
    assertPathEquals("foo", resolved);

    path = pathService.parsePath("foo");
    resolved = path.resolveSibling("");
    assertPathEquals("", resolved);
  }

  @Test
  public void testResolveSibling_againstEmptyPath() {
    Path path = pathService.parsePath("");
    Path resolved = path.resolveSibling("foo");
    assertPathEquals("foo", resolved);

    path = pathService.parsePath("");
    resolved = path.resolveSibling("");
    assertPathEquals("", resolved);
  }

  @Test
  public void testRelativize_bothAbsolute() {
    // TODO(cgdecker): When the paths have different roots, how should this work?
    // Should it work at all?
    assertRelativizedPathEquals("b/c", pathService.parsePath("/a"), "/a/b/c");
    assertRelativizedPathEquals("c/d", pathService.parsePath("/a/b"), "/a/b/c/d");
  }

  @Test
  public void testRelativize_bothRelative() {
    assertRelativizedPathEquals("b/c", pathService.parsePath("a"), "a/b/c");
    assertRelativizedPathEquals("d", pathService.parsePath("a/b/c"), "a/b/c/d");
  }

  @Test
  public void testRelativize_againstEmptyPath() {
    assertRelativizedPathEquals("foo/bar", pathService.emptyPath(), "foo/bar");
  }

  @Test
  public void testRelativize_oneAbsoluteOneRelative() {
    try {
      pathService.parsePath("/foo/bar").relativize(pathService.parsePath("foo"));
      fail();
    } catch (IllegalArgumentException expected) {
    }

    try {
      pathService.parsePath("foo").relativize(pathService.parsePath("/foo/bar"));
      fail();
    } catch (IllegalArgumentException expected) {
    }
  }

  @Test
  public void testNormalize_withParentDirName() {
    assertNormalizedPathEquals("/foo/baz", "/foo/bar/../baz");
    assertNormalizedPathEquals("/foo/baz", "/foo", "bar", "..", "baz");
  }

  @Test
  public void testNormalize_withThisDirName() {
    assertNormalizedPathEquals("/foo/bar/baz", "/foo/bar/./baz");
    assertNormalizedPathEquals("/foo/bar/baz", "/foo", "bar", ".", "baz");
  }

  @Test
  public void testNormalize_withThisAndParentDirNames() {
    assertNormalizedPathEquals("foo/test", "foo/./bar/../././baz/../test");
  }

  @Test
  public void testNormalize_withLeadingParentDirNames() {
    assertNormalizedPathEquals("../../foo/baz", "../../foo/bar/../baz");
  }

  @Test
  public void testNormalize_withLeadingThisAndParentDirNames() {
    assertNormalizedPathEquals("../../foo/baz", "./.././.././foo/bar/../baz");
  }

  @Test
  public void testNormalize_withExtraParentDirNamesAtRoot() {
    assertNormalizedPathEquals("/", "/..");
    assertNormalizedPathEquals("/", "/../../..");
    assertNormalizedPathEquals("/", "/foo/../../..");
    assertNormalizedPathEquals("/", "/../foo/../../bar/baz/../../../..");
  }

  @Test
  public void testPathWithExtraSlashes() {
    assertPathEquals("/foo/bar/baz", pathService.parsePath("/foo/bar/baz/"));
    assertPathEquals("/foo/bar/baz", pathService.parsePath("/foo//bar///baz"));
    assertPathEquals("/foo/bar/baz", pathService.parsePath("///foo/bar/baz"));
  }

  @Test
  public void testEqualityBasedOnStringNotName() {
    Name a1 = Name.create("a", "a");
    Name a2 = Name.create("A", "a");
    Name a3 = Name.create("a", "A");

    Path path1 = pathService.createFileName(a1);
    Path path2 = pathService.createFileName(a2);
    Path path3 = pathService.createFileName(a3);

    new EqualsTester().addEqualityGroup(path1, path3).addEqualityGroup(path2).testEquals();
  }

  @Test
  public void testNullPointerExceptions() throws NoSuchMethodException {
    NullPointerTester tester =
        new NullPointerTester().ignore(JimfsPath.class.getMethod("toRealPath", LinkOption[].class));
    // ignore toRealPath because the pathService creates fake paths that do not have a
    // JimfsFileSystem instance, causing it to fail since it needs to access the file system

    tester.testAllPublicInstanceMethods(pathService.parsePath("/"));
    tester.testAllPublicInstanceMethods(pathService.parsePath(""));
    tester.testAllPublicInstanceMethods(pathService.parsePath("/foo"));
    tester.testAllPublicInstanceMethods(pathService.parsePath("/foo/bar/baz"));
    tester.testAllPublicInstanceMethods(pathService.parsePath("foo"));
    tester.testAllPublicInstanceMethods(pathService.parsePath("foo/bar"));
    tester.testAllPublicInstanceMethods(pathService.parsePath("foo/bar/baz"));
    tester.testAllPublicInstanceMethods(pathService.parsePath("."));
    tester.testAllPublicInstanceMethods(pathService.parsePath(".."));
  }

  private void assertResolvedPathEquals(
      String expected, Path path, String firstResolvePath, String... moreResolvePaths) {
    Path resolved = path.resolve(firstResolvePath);
    for (String additionalPath : moreResolvePaths) {
      resolved = resolved.resolve(additionalPath);
    }
    assertPathEquals(expected, resolved);

    Path relative = pathService.parsePath(firstResolvePath, moreResolvePaths);
    resolved = path.resolve(relative);
    assertPathEquals(expected, resolved);

    // assert the invariant that p.relativize(p.resolve(q)).equals(q) when q does not have a root
    // p = path, q = relative, p.resolve(q) = resolved
    if (relative.getRoot() == null) {
      assertEquals(relative, path.relativize(resolved));
    }
  }

  private void assertRelativizedPathEquals(String expected, Path path, String relativizePath) {
    Path relativized = path.relativize(pathService.parsePath(relativizePath));
    assertPathEquals(expected, relativized);
  }

  private void assertNormalizedPathEquals(String expected, String first, String... more) {
    assertPathEquals(expected, pathService.parsePath(first, more).normalize());
  }

  private void assertPathEquals(String expected, String first, String... more) {
    assertPathEquals(expected, pathService.parsePath(first, more));
  }

  private void assertPathEquals(String expected, Path path) {
    assertEquals(pathService.parsePath(expected), path);
  }
}
