// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.vfs;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.Lists;
import com.google.common.testing.EqualsTester;
import com.google.common.testing.GcFinalization;
import com.google.devtools.build.lib.util.BlazeClock;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;

/**
 * A test for {@link Path}.
 */
@RunWith(JUnit4.class)
public class PathTest {
  private FileSystem filesystem;
  private Path root;

  @Before
  public final void initializeFileSystem() throws Exception  {
    filesystem = new InMemoryFileSystem(BlazeClock.instance());
    root = filesystem.getRootDirectory();
    Path first = root.getChild("first");
    first.createDirectory();
  }

  @Test
  public void testStartsWithWorksForSelf() {
    assertStartsWithReturns(true, "/first/child", "/first/child");
  }

  @Test
  public void testStartsWithWorksForChild() {
    assertStartsWithReturns(true,
        "/first/child", "/first/child/grandchild");
  }

  @Test
  public void testStartsWithWorksForDeepDescendant() {
    assertStartsWithReturns(true,
        "/first/child", "/first/child/grandchild/x/y/z");
  }

  @Test
  public void testStartsWithFailsForParent() {
    assertStartsWithReturns(false, "/first/child", "/first");
  }

  @Test
  public void testStartsWithFailsForSibling() {
    assertStartsWithReturns(false, "/first/child", "/first/child2");
  }

  @Test
  public void testStartsWithFailsForLinkToDescendant()
      throws Exception {
    Path linkTarget = filesystem.getPath("/first/linked_to");
    FileSystemUtils.createEmptyFile(linkTarget);
    Path second = filesystem.getPath("/second/");
    second.createDirectory();
    second.getChild("child_link").createSymbolicLink(linkTarget);
    assertStartsWithReturns(false, "/first", "/second/child_link");
  }

  @Test
  public void testStartsWithFailsForNullPrefix() {
    try {
      filesystem.getPath("/first").startsWith(null);
      fail();
    } catch (Exception e) {
    }
  }

  private void assertStartsWithReturns(boolean expected,
                                       String ancestor,
                                       String descendant) {
    Path parent = filesystem.getPath(ancestor);
    Path child = filesystem.getPath(descendant);
    assertEquals(expected, child.startsWith(parent));
  }

  @Test
  public void testGetChildWorks() {
    assertGetChildWorks("second");
    assertGetChildWorks("...");
    assertGetChildWorks("....");
  }

  private void assertGetChildWorks(String childName) {
    assertEquals(filesystem.getPath("/first/" + childName),
        filesystem.getPath("/first").getChild(childName));
  }

  @Test
  public void testGetChildFailsForChildWithSlashes() {
    assertGetChildFails("second/third");
    assertGetChildFails("./third");
    assertGetChildFails("../third");
    assertGetChildFails("second/..");
    assertGetChildFails("second/.");
    assertGetChildFails("/third");
    assertGetChildFails("third/");
  }

  private void assertGetChildFails(String childName) {
    try {
      filesystem.getPath("/first").getChild(childName);
      fail();
    } catch (IllegalArgumentException e) {
      // Expected.
    }
  }

  @Test
  public void testGetChildFailsForDotAndDotDot() {
    assertGetChildFails(".");
    assertGetChildFails("..");
  }

  @Test
  public void testGetChildFailsForEmptyString() {
    assertGetChildFails("");
  }

  @Test
  public void testRelativeToWorks() {
    assertRelativeToWorks("apple", "/fruit/apple", "/fruit");
    assertRelativeToWorks("apple/jonagold", "/fruit/apple/jonagold", "/fruit");
  }

  @Test
  public void testGetRelativeWithStringWorks() {
    assertGetRelativeWorks("/first/x/y", "y");
    assertGetRelativeWorks("/y", "/y");
    assertGetRelativeWorks("/first/x/x", "./x");
    assertGetRelativeWorks("/first/y", "../y");
    assertGetRelativeWorks("/", "../../../../..");
  }

  @Test
  public void testAsFragmentWorks() {
    assertAsFragmentWorks("/");
    assertAsFragmentWorks("//");
    assertAsFragmentWorks("/first");
    assertAsFragmentWorks("/first/x/y");
    assertAsFragmentWorks("/first/x/y.foo");
  }

  @Test
  public void testGetRelativeWithFragmentWorks() {
    Path dir = filesystem.getPath("/first/x");
    assertEquals("/first/x/y",
                 dir.getRelative(new PathFragment("y")).toString());
    assertEquals("/first/x/x",
                 dir.getRelative(new PathFragment("./x")).toString());
    assertEquals("/first/y",
                 dir.getRelative(new PathFragment("../y")).toString());

  }

  @Test
  public void testGetRelativeWithAbsoluteFragmentWorks() {
    Path root = filesystem.getPath("/first/x");
    assertEquals("/x/y",
                 root.getRelative(new PathFragment("/x/y")).toString());
  }

  @Test
  public void testGetRelativeWithAbsoluteStringWorks() {
    Path root = filesystem.getPath("/first/x");
    assertEquals("/x/y", root.getRelative("/x/y").toString());
  }

  @Test
  public void testComparableSortOrder() {
    Path zzz = filesystem.getPath("/zzz");
    Path ZZZ = filesystem.getPath("/ZZZ");
    Path abc = filesystem.getPath("/abc");
    Path aBc = filesystem.getPath("/aBc");
    Path AbC = filesystem.getPath("/AbC");
    Path ABC = filesystem.getPath("/ABC");
    List<Path> list = Lists.newArrayList(zzz, ZZZ, ABC, aBc, AbC, abc);
    Collections.sort(list);
    assertThat(list).containsExactly(ABC, AbC, ZZZ, aBc, abc, zzz).inOrder();
  }

  @Test
  public void testParentOfRootIsRoot() {
    assertSame(root, root.getRelative(".."));

    assertSame(root.getRelative("dots"),
               root.getRelative("broken/../../dots"));
  }

  @Test
  public void testSingleSegmentEquivalence() {
    assertSame(
        root.getRelative("aSingleSegment"),
        root.getRelative("aSingleSegment"));
  }

  @Test
  public void testSiblingNonEquivalenceString() {
    assertNotSame(
        root.getRelative("aSingleSegment"),
        root.getRelative("aDifferentSegment"));
  }

  @Test
  public void testSiblingNonEquivalenceFragment() {
    assertNotSame(
        root.getRelative(new PathFragment("aSingleSegment")),
        root.getRelative(new PathFragment("aDifferentSegment")));
  }

  @Test
  public void testHashCodeStableAcrossGarbageCollections() {
    Path parent = filesystem.getPath("/a");
    PathFragment childFragment = new PathFragment("b");
    Path child = parent.getRelative(childFragment);
    WeakReference<Path> childRef = new WeakReference<>(child);
    int childHashCode1 = childRef.get().hashCode();
    assertEquals(childHashCode1, parent.getRelative(childFragment).hashCode());
    child = null;
    GcFinalization.awaitClear(childRef);
    int childHashCode2 = parent.getRelative(childFragment).hashCode();
    assertEquals(childHashCode1, childHashCode2);
  }

  @Test
  public void testSerialization() throws Exception {
    FileSystem oldFileSystem = Path.getFileSystemForSerialization();
    try {
      Path.setFileSystemForSerialization(filesystem);
      Path root = filesystem.getPath("/");
      Path p1 = filesystem.getPath("/foo");
      Path p2 = filesystem.getPath("/foo/bar");

      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(bos);

      oos.writeObject(root);
      oos.writeObject(p1);
      oos.writeObject(p2);

      ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
      ObjectInputStream ois = new ObjectInputStream(bis);

      Path dsRoot = (Path) ois.readObject();
      Path dsP1 = (Path) ois.readObject();
      Path dsP2 = (Path) ois.readObject();

      new EqualsTester()
          .addEqualityGroup(root, dsRoot)
          .addEqualityGroup(p1, dsP1)
          .addEqualityGroup(p2, dsP2)
          .testEquals();

      assertTrue(p2.startsWith(p1));
      assertTrue(p2.startsWith(dsP1));
      assertTrue(dsP2.startsWith(p1));
      assertTrue(dsP2.startsWith(dsP1));

      // Regression test for a very specific bug in compareTo involving our incorrect usage of
      // reference equality rather than logical equality.
      String relativePathStringA = "child/grandchildA";
      String relativePathStringB = "child/grandchildB";
      assertEquals(
          p1.getRelative(relativePathStringA).compareTo(p1.getRelative(relativePathStringB)),
          p1.getRelative(relativePathStringA).compareTo(dsP1.getRelative(relativePathStringB)));
    } finally {
      Path.setFileSystemForSerialization(oldFileSystem);
    }
  }

  private void assertAsFragmentWorks(String expected) {
    assertEquals(new PathFragment(expected), filesystem.getPath(expected).asFragment());
  }

  private void assertGetRelativeWorks(String expected, String relative) {
    assertEquals(filesystem.getPath(expected),
        filesystem.getPath("/first/x").getRelative(relative));
  }

  private void assertRelativeToWorks(String expected, String relative, String original) {
    assertEquals(new PathFragment(expected),
                 filesystem.getPath(relative).relativeTo(filesystem.getPath(original)));
  }
}
