// Copyright (c) 2016 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

#include "filesystem.h"
#include "test.h"

namespace kj {
namespace {

KJ_TEST("Path") {
  KJ_EXPECT(Path(nullptr).toString() == ".");
  KJ_EXPECT(Path(nullptr).toString(true) == "/");
  KJ_EXPECT(Path("foo").toString() == "foo");
  KJ_EXPECT(Path("foo").toString(true) == "/foo");

  KJ_EXPECT(Path({"foo", "bar"}).toString() == "foo/bar");
  KJ_EXPECT(Path({"foo", "bar"}).toString(true) == "/foo/bar");

  KJ_EXPECT(Path::parse("foo/bar").toString() == "foo/bar");
  KJ_EXPECT(Path::parse("foo//bar").toString() == "foo/bar");
  KJ_EXPECT(Path::parse("foo/./bar").toString() == "foo/bar");
  KJ_EXPECT(Path::parse("foo/../bar").toString() == "bar");
  KJ_EXPECT(Path::parse("foo/bar/..").toString() == "foo");
  KJ_EXPECT(Path::parse("foo/bar/../..").toString() == ".");

  KJ_EXPECT(Path({"foo", "bar"}).eval("baz").toString() == "foo/bar/baz");
  KJ_EXPECT(Path({"foo", "bar"}).eval("./baz").toString() == "foo/bar/baz");
  KJ_EXPECT(Path({"foo", "bar"}).eval("baz/qux").toString() == "foo/bar/baz/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("baz//qux").toString() == "foo/bar/baz/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("baz/./qux").toString() == "foo/bar/baz/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("baz/../qux").toString() == "foo/bar/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("baz/qux/..").toString() == "foo/bar/baz");
  KJ_EXPECT(Path({"foo", "bar"}).eval("../baz").toString() == "foo/baz");
  KJ_EXPECT(Path({"foo", "bar"}).eval("baz/../../qux/").toString() == "foo/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("/baz/qux").toString() == "baz/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("//baz/qux").toString() == "baz/qux");
  KJ_EXPECT(Path({"foo", "bar"}).eval("/baz/../qux").toString() == "qux");

  KJ_EXPECT(Path({"foo", "bar"}).basename()[0] == "bar");
  KJ_EXPECT(Path({"foo", "bar", "baz"}).parent().toString() == "foo/bar");

  KJ_EXPECT(Path({"foo", "bar"}).append("baz").toString() == "foo/bar/baz");
  KJ_EXPECT(Path({"foo", "bar"}).append(Path({"baz", "qux"})).toString() == "foo/bar/baz/qux");

  {
    // Test methods which are overloaded for && on a non-rvalue path.
    Path path({"foo", "bar"});
    KJ_EXPECT(path.eval("baz").toString() == "foo/bar/baz");
    KJ_EXPECT(path.eval("./baz").toString() == "foo/bar/baz");
    KJ_EXPECT(path.eval("baz/qux").toString() == "foo/bar/baz/qux");
    KJ_EXPECT(path.eval("baz//qux").toString() == "foo/bar/baz/qux");
    KJ_EXPECT(path.eval("baz/./qux").toString() == "foo/bar/baz/qux");
    KJ_EXPECT(path.eval("baz/../qux").toString() == "foo/bar/qux");
    KJ_EXPECT(path.eval("baz/qux/..").toString() == "foo/bar/baz");
    KJ_EXPECT(path.eval("../baz").toString() == "foo/baz");
    KJ_EXPECT(path.eval("baz/../../qux/").toString() == "foo/qux");
    KJ_EXPECT(path.eval("/baz/qux").toString() == "baz/qux");
    KJ_EXPECT(path.eval("/baz/../qux").toString() == "qux");

    KJ_EXPECT(path.basename()[0] == "bar");
    KJ_EXPECT(path.parent().toString() == "foo");

    KJ_EXPECT(path.append("baz").toString() == "foo/bar/baz");
    KJ_EXPECT(path.append(Path({"baz", "qux"})).toString() == "foo/bar/baz/qux");
  }

  KJ_EXPECT(kj::str(Path({"foo", "bar"})) == "foo/bar");
}

KJ_TEST("Path exceptions") {
  KJ_EXPECT_THROW_MESSAGE("invalid path component", Path(""));
  KJ_EXPECT_THROW_MESSAGE("invalid path component", Path("."));
  KJ_EXPECT_THROW_MESSAGE("invalid path component", Path(".."));
  KJ_EXPECT_THROW_MESSAGE("NUL character", Path(StringPtr("foo\0bar", 7)));

  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path::parse(".."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path::parse("../foo"));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path::parse("foo/../.."));
  KJ_EXPECT_THROW_MESSAGE("expected a relative path", Path::parse("/foo"));

  KJ_EXPECT_THROW_MESSAGE("NUL character", Path::parse(kj::StringPtr("foo\0bar", 7)));

  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("../../.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("../baz/../../.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("baz/../../../.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("/.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).eval("/baz/../.."));

  KJ_EXPECT_THROW_MESSAGE("root path has no basename", Path(nullptr).basename());
  KJ_EXPECT_THROW_MESSAGE("root path has no parent", Path(nullptr).parent());
}

KJ_TEST("Win32 Path") {
  KJ_EXPECT(Path({"foo", "bar"}).toWin32String() == "foo\\bar");
  KJ_EXPECT(Path({"foo", "bar"}).toWin32String(true) == "\\\\foo\\bar");
  KJ_EXPECT(Path({"c:", "foo", "bar"}).toWin32String(true) == "c:\\foo\\bar");
  KJ_EXPECT(Path({"A:", "foo", "bar"}).toWin32String(true) == "A:\\foo\\bar");

  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz").toWin32String() == "foo\\bar\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("./baz").toWin32String() == "foo\\bar\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/qux").toWin32String() == "foo\\bar\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz//qux").toWin32String() == "foo\\bar\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/./qux").toWin32String() == "foo\\bar\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/../qux").toWin32String() == "foo\\bar\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/qux/..").toWin32String() == "foo\\bar\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("../baz").toWin32String() == "foo\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz/../../qux/").toWin32String() == "foo\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32(".\\baz").toWin32String() == "foo\\bar\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\qux").toWin32String() == "foo\\bar\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\\\qux").toWin32String() == "foo\\bar\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\.\\qux").toWin32String() == "foo\\bar\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\..\\qux").toWin32String() == "foo\\bar\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\qux\\..").toWin32String() == "foo\\bar\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("..\\baz").toWin32String() == "foo\\baz");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\..\\..\\qux\\").toWin32String() == "foo\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("baz\\../..\\qux/").toWin32String() == "foo\\qux");

  KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("/baz/qux")
      .toWin32String(true) == "c:\\baz\\qux");
  KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("\\baz\\qux")
      .toWin32String(true) == "c:\\baz\\qux");
  KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("d:\\baz\\qux")
      .toWin32String(true) == "d:\\baz\\qux");
  KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("d:\\baz\\..\\qux")
      .toWin32String(true) == "d:\\qux");
  KJ_EXPECT(Path({"c:", "foo", "bar"}).evalWin32("\\\\baz\\qux")
      .toWin32String(true) == "\\\\baz\\qux");
  KJ_EXPECT(Path({"foo", "bar"}).evalWin32("d:\\baz\\..\\qux")
      .toWin32String(true) == "d:\\qux");
  KJ_EXPECT(Path({"foo", "bar", "baz"}).evalWin32("\\qux")
      .toWin32String(true) == "\\\\foo\\bar\\qux");
}

KJ_TEST("Win32 Path exceptions") {
  KJ_EXPECT_THROW_MESSAGE("colons are prohibited", Path({"c:", "foo", "bar"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("colons are prohibited", Path({"c:", "foo:bar"}).toWin32String(true));
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"con"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"CON", "bar"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"foo", "cOn"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"prn"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"aux"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"NUL"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"nul.txt"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"com3"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"lpt9"}).toWin32String());
  KJ_EXPECT_THROW_MESSAGE("DOS reserved name", Path({"com1.hello"}).toWin32String());

  KJ_EXPECT_THROW_MESSAGE("drive letter or netbios", Path({"?", "foo"}).toWin32String(true));

  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("../../.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("../baz/../../.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("baz/../../../.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting", Path({"foo", "bar"}).evalWin32("c:\\..\\.."));
  KJ_EXPECT_THROW_MESSAGE("break out of starting",
      Path({"c:", "foo", "bar"}).evalWin32("/baz/../../.."));
  KJ_EXPECT_THROW_MESSAGE("must specify drive letter", Path({"foo"}).evalWin32("\\baz\\qux"));
}

KJ_TEST("WriteMode operators") {
  WriteMode createOrModify = WriteMode::CREATE | WriteMode::MODIFY;

  KJ_EXPECT(has(createOrModify, WriteMode::MODIFY));
  KJ_EXPECT(has(createOrModify, WriteMode::CREATE));
  KJ_EXPECT(!has(createOrModify, WriteMode::CREATE_PARENT));
  KJ_EXPECT(has(createOrModify, createOrModify));
  KJ_EXPECT(!has(createOrModify, createOrModify | WriteMode::CREATE_PARENT));
  KJ_EXPECT(!has(createOrModify, WriteMode::CREATE | WriteMode::CREATE_PARENT));
  KJ_EXPECT(!has(WriteMode::CREATE, createOrModify));

  KJ_EXPECT(createOrModify != WriteMode::MODIFY);
  KJ_EXPECT(createOrModify != WriteMode::CREATE);

  KJ_EXPECT(createOrModify - WriteMode::CREATE == WriteMode::MODIFY);
  KJ_EXPECT(WriteMode::CREATE + WriteMode::MODIFY == createOrModify);

  // Adding existing bit / subtracting non-existing bit are no-ops.
  KJ_EXPECT(createOrModify + WriteMode::MODIFY == createOrModify);
  KJ_EXPECT(createOrModify - WriteMode::CREATE_PARENT == createOrModify);
}

// ======================================================================================

class TestClock final: public Clock {
public:
  void tick() {
    time += 1 * SECONDS;
  }

  Date now() override { return time; }

  void expectChanged(FsNode& file) {
    KJ_EXPECT(file.stat().lastModified == time);
    time += 1 * SECONDS;
  }
  void expectUnchanged(FsNode& file) {
    KJ_EXPECT(file.stat().lastModified != time);
  }

private:
  Date time = UNIX_EPOCH + 1 * SECONDS;
};

KJ_TEST("InMemoryFile") {
  TestClock clock;

  auto file = newInMemoryFile(clock);
  clock.expectChanged(*file);

  KJ_EXPECT(file->readAllText() == "");
  clock.expectUnchanged(*file);

  file->writeAll("foo");
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == "foo");

  file->write(3, StringPtr("bar").asBytes());
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == "foobar");

  file->write(3, StringPtr("baz").asBytes());
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == "foobaz");

  file->write(9, StringPtr("qux").asBytes());
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == kj::StringPtr("foobaz\0\0\0qux", 12));

  file->truncate(6);
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == "foobaz");

  file->truncate(18);
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == kj::StringPtr("foobaz\0\0\0\0\0\0\0\0\0\0\0\0", 18));

  {
    auto mapping = file->mmap(0, 18);
    auto privateMapping = file->mmapPrivate(0, 18);
    auto writableMapping = file->mmapWritable(0, 18);
    clock.expectUnchanged(*file);

    KJ_EXPECT(mapping.size() == 18);
    KJ_EXPECT(privateMapping.size() == 18);
    KJ_EXPECT(writableMapping->get().size() == 18);
    clock.expectUnchanged(*file);

    KJ_EXPECT(writableMapping->get().begin() == mapping.begin());
    KJ_EXPECT(privateMapping.begin() != mapping.begin());

    KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "foobaz");
    KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "foobaz");
    clock.expectUnchanged(*file);

    file->write(0, StringPtr("qux").asBytes());
    clock.expectChanged(*file);
    KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "quxbaz");
    KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "foobaz");

    file->write(12, StringPtr("corge").asBytes());
    KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == "corge");

    // Can shrink.
    file->truncate(6);
    KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == kj::StringPtr("\0\0\0\0\0", 5));

    // Can regrow.
    file->truncate(18);
    KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == kj::StringPtr("\0\0\0\0\0", 5));

    // Can't grow past previoous capacity.
    KJ_EXPECT_THROW_MESSAGE("cannot resize the file backing store", file->truncate(100));

    clock.expectChanged(*file);
    writableMapping->changed(writableMapping->get().slice(0, 3));
    clock.expectChanged(*file);
    writableMapping->sync(writableMapping->get().slice(0, 3));
    clock.expectChanged(*file);
  }

  // But now we can since the mapping is gone.
  file->truncate(100);

  file->truncate(6);
  clock.expectChanged(*file);

  KJ_EXPECT(file->readAllText() == "quxbaz");
  file->zero(3, 3);
  clock.expectChanged(*file);
  KJ_EXPECT(file->readAllText() == StringPtr("qux\0\0\0", 6));
}

KJ_TEST("InMemoryFile::copy()") {
  TestClock clock;

  auto source = newInMemoryFile(clock);
  source->writeAll("foobarbaz");

  auto dest = newInMemoryFile(clock);
  dest->writeAll("quxcorge");
  clock.expectChanged(*dest);

  KJ_EXPECT(dest->copy(3, *source, 6, kj::maxValue) == 3);
  clock.expectChanged(*dest);
  KJ_EXPECT(dest->readAllText() == "quxbazge");

  KJ_EXPECT(dest->copy(0, *source, 3, 4) == 4);
  clock.expectChanged(*dest);
  KJ_EXPECT(dest->readAllText() == "barbazge");

  KJ_EXPECT(dest->copy(0, *source, 128, kj::maxValue) == 0);
  clock.expectUnchanged(*dest);

  KJ_EXPECT(dest->copy(4, *source, 3, 0) == 0);
  clock.expectUnchanged(*dest);

  String bigString = strArray(repeat("foobar", 10000), "");
  source->truncate(bigString.size() + 1000);
  source->write(123, bigString.asBytes());

  dest->copy(321, *source, 123, bigString.size());
  KJ_EXPECT(dest->readAllText().slice(321) == bigString);
}

KJ_TEST("File::copy()") {
  TestClock clock;

  auto source = newInMemoryFile(clock);
  source->writeAll("foobarbaz");

  auto dest = newInMemoryFile(clock);
  dest->writeAll("quxcorge");
  clock.expectChanged(*dest);

  KJ_EXPECT(dest->File::copy(3, *source, 6, kj::maxValue) == 3);
  clock.expectChanged(*dest);
  KJ_EXPECT(dest->readAllText() == "quxbazge");

  KJ_EXPECT(dest->File::copy(0, *source, 3, 4) == 4);
  clock.expectChanged(*dest);
  KJ_EXPECT(dest->readAllText() == "barbazge");

  KJ_EXPECT(dest->File::copy(0, *source, 128, kj::maxValue) == 0);
  clock.expectUnchanged(*dest);

  KJ_EXPECT(dest->File::copy(4, *source, 3, 0) == 0);
  clock.expectUnchanged(*dest);

  String bigString = strArray(repeat("foobar", 10000), "");
  source->truncate(bigString.size() + 1000);
  source->write(123, bigString.asBytes());

  dest->File::copy(321, *source, 123, bigString.size());
  KJ_EXPECT(dest->readAllText().slice(321) == bigString);
}

KJ_TEST("InMemoryDirectory") {
  TestClock clock;

  auto dir = newInMemoryDirectory(clock);
  clock.expectChanged(*dir);

  KJ_EXPECT(dir->listNames() == nullptr);
  KJ_EXPECT(dir->listEntries() == nullptr);
  KJ_EXPECT(!dir->exists(Path("foo")));
  KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
  KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr);
  clock.expectUnchanged(*dir);

  {
    auto file = dir->openFile(Path("foo"), WriteMode::CREATE);
    clock.expectChanged(*dir);
    file->writeAll("foobar");
    clock.expectUnchanged(*dir);
  }
  clock.expectUnchanged(*dir);

  KJ_EXPECT(dir->exists(Path("foo")));
  clock.expectUnchanged(*dir);

  {
    auto stats = dir->lstat(Path("foo"));
    clock.expectUnchanged(*dir);
    KJ_EXPECT(stats.type == FsNode::Type::FILE);
    KJ_EXPECT(stats.size == 6);
  }

  {
    auto list = dir->listNames();
    clock.expectUnchanged(*dir);
    KJ_ASSERT(list.size() == 1);
    KJ_EXPECT(list[0] == "foo");
  }

  {
    auto list = dir->listEntries();
    clock.expectUnchanged(*dir);
    KJ_ASSERT(list.size() == 1);
    KJ_EXPECT(list[0].name == "foo");
    KJ_EXPECT(list[0].type == FsNode::Type::FILE);
  }

  KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar");
  clock.expectUnchanged(*dir);

  KJ_EXPECT(dir->tryOpenFile(Path({"foo", "bar"}), WriteMode::MODIFY) == nullptr);
  KJ_EXPECT(dir->tryOpenFile(Path({"bar", "baz"}), WriteMode::MODIFY) == nullptr);
  KJ_EXPECT_THROW_MESSAGE("parent is not a directory",
      dir->tryOpenFile(Path({"bar", "baz"}), WriteMode::CREATE));
  clock.expectUnchanged(*dir);

  {
    auto file = dir->openFile(Path({"bar", "baz"}), WriteMode::CREATE | WriteMode::CREATE_PARENT);
    clock.expectChanged(*dir);
    file->writeAll("bazqux");
    clock.expectUnchanged(*dir);
  }
  clock.expectUnchanged(*dir);

  KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "bazqux");
  clock.expectUnchanged(*dir);

  {
    auto stats = dir->lstat(Path("bar"));
    clock.expectUnchanged(*dir);
    KJ_EXPECT(stats.type == FsNode::Type::DIRECTORY);
  }

  {
    auto list = dir->listNames();
    clock.expectUnchanged(*dir);
    KJ_ASSERT(list.size() == 2);
    KJ_EXPECT(list[0] == "bar");
    KJ_EXPECT(list[1] == "foo");
  }

  {
    auto list = dir->listEntries();
    clock.expectUnchanged(*dir);
    KJ_ASSERT(list.size() == 2);
    KJ_EXPECT(list[0].name == "bar");
    KJ_EXPECT(list[0].type == FsNode::Type::DIRECTORY);
    KJ_EXPECT(list[1].name == "foo");
    KJ_EXPECT(list[1].type == FsNode::Type::FILE);
  }

  {
    auto subdir = dir->openSubdir(Path("bar"));
    clock.expectUnchanged(*dir);
    clock.expectUnchanged(*subdir);

    KJ_EXPECT(subdir->openFile(Path("baz"))->readAllText() == "bazqux");
    clock.expectUnchanged(*subdir);
  }

  auto subdir = dir->openSubdir(Path("corge"), WriteMode::CREATE);
  clock.expectChanged(*dir);

  subdir->openFile(Path("grault"), WriteMode::CREATE)->writeAll("garply");
  clock.expectUnchanged(*dir);
  clock.expectChanged(*subdir);

  KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "garply");

  dir->openFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY)
     ->write(0, StringPtr("rag").asBytes());
  KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragply");
  clock.expectUnchanged(*dir);

  {
    auto replacer =
        dir->replaceFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY);
    clock.expectUnchanged(*subdir);
    replacer->get().writeAll("rag");
    clock.expectUnchanged(*subdir);
    // Don't commit.
  }
  clock.expectUnchanged(*subdir);
  KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragply");

  {
    auto replacer =
        dir->replaceFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY);
    clock.expectUnchanged(*subdir);
    replacer->get().writeAll("rag");
    clock.expectUnchanged(*subdir);
    replacer->commit();
    clock.expectChanged(*subdir);
    KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "rag");
  }

  KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "rag");

  {
    auto appender = dir->appendFile(Path({"corge", "grault"}), WriteMode::MODIFY);
    appender->write("waldo", 5);
    appender->write("fred", 4);
  }

  KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragwaldofred");

  KJ_EXPECT(dir->exists(Path("foo")));
  clock.expectUnchanged(*dir);
  dir->remove(Path("foo"));
  clock.expectChanged(*dir);
  KJ_EXPECT(!dir->exists(Path("foo")));
  KJ_EXPECT(!dir->tryRemove(Path("foo")));
  clock.expectUnchanged(*dir);

  KJ_EXPECT(dir->exists(Path({"bar", "baz"})));
  clock.expectUnchanged(*dir);
  dir->remove(Path({"bar", "baz"}));
  clock.expectUnchanged(*dir);
  KJ_EXPECT(!dir->exists(Path({"bar", "baz"})));
  KJ_EXPECT(dir->exists(Path("bar")));
  KJ_EXPECT(!dir->tryRemove(Path({"bar", "baz"})));
  clock.expectUnchanged(*dir);

  KJ_EXPECT(dir->exists(Path("corge")));
  KJ_EXPECT(dir->exists(Path({"corge", "grault"})));
  clock.expectUnchanged(*dir);
  dir->remove(Path("corge"));
  clock.expectChanged(*dir);
  KJ_EXPECT(!dir->exists(Path("corge")));
  KJ_EXPECT(!dir->exists(Path({"corge", "grault"})));
  KJ_EXPECT(!dir->tryRemove(Path("corge")));
  clock.expectUnchanged(*dir);
}

KJ_TEST("InMemoryDirectory symlinks") {
  TestClock clock;

  auto dir = newInMemoryDirectory(clock);
  clock.expectChanged(*dir);

  dir->symlink(Path("foo"), "bar/qux/../baz", WriteMode::CREATE);
  clock.expectChanged(*dir);

  KJ_EXPECT(!dir->trySymlink(Path("foo"), "bar/qux/../baz", WriteMode::CREATE));
  clock.expectUnchanged(*dir);

  {
    auto stats = dir->lstat(Path("foo"));
    clock.expectUnchanged(*dir);
    KJ_EXPECT(stats.type == FsNode::Type::SYMLINK);
  }

  KJ_EXPECT(dir->readlink(Path("foo")) == "bar/qux/../baz");

  // Broken link into non-existing directory cannot be opened in any mode.
  KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
  KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::CREATE) == nullptr);
  KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr);
  KJ_EXPECT_THROW_MESSAGE("parent is not a directory",
      dir->tryOpenFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY));
  KJ_EXPECT_THROW_MESSAGE("parent is not a directory",
      dir->tryOpenFile(Path("foo"),
          WriteMode::CREATE | WriteMode::MODIFY | WriteMode::CREATE_PARENT));

  // Create the directory.
  auto subdir = dir->openSubdir(Path("bar"), WriteMode::CREATE);
  clock.expectChanged(*dir);

  // Link still points to non-existing file so cannot be open in most modes.
  KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
  KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::CREATE) == nullptr);
  KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr);
  clock.expectUnchanged(*dir);

  // But... CREATE | MODIFY works.
  dir->openFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY)
     ->writeAll("foobar");
  clock.expectUnchanged(*dir);  // Change is only to subdir!

  KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "foobar");
  KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar");
  KJ_EXPECT(dir->openFile(Path("foo"), WriteMode::MODIFY)->readAllText() == "foobar");

  // operations that modify the symlink
  dir->symlink(Path("foo"), "corge", WriteMode::MODIFY);
  KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "foobar");
  KJ_EXPECT(dir->readlink(Path("foo")) == "corge");
  KJ_EXPECT(!dir->exists(Path("foo")));
  KJ_EXPECT(dir->lstat(Path("foo")).type == FsNode::Type::SYMLINK);
  KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);

  dir->remove(Path("foo"));
  KJ_EXPECT(!dir->exists(Path("foo")));
  KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
}

KJ_TEST("InMemoryDirectory link") {
  TestClock clock;

  auto src = newInMemoryDirectory(clock);
  auto dst = newInMemoryDirectory(clock);

  src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
     ->writeAll("foobar");
  src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
     ->writeAll("bazqux");
  clock.expectChanged(*src);
  clock.expectUnchanged(*dst);

  dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::LINK);
  clock.expectUnchanged(*src);
  clock.expectChanged(*dst);

  KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
  KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux");

  KJ_EXPECT(dst->exists(Path({"link", "bar"})));
  src->remove(Path({"foo", "bar"}));
  KJ_EXPECT(!dst->exists(Path({"link", "bar"})));
}

KJ_TEST("InMemoryDirectory copy") {
  TestClock clock;

  auto src = newInMemoryDirectory(clock);
  auto dst = newInMemoryDirectory(clock);

  src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
     ->writeAll("foobar");
  src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
     ->writeAll("bazqux");
  clock.expectChanged(*src);
  clock.expectUnchanged(*dst);

  dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::COPY);
  clock.expectUnchanged(*src);
  clock.expectChanged(*dst);

  KJ_EXPECT(src->openFile(Path({"foo", "bar"}))->readAllText() == "foobar");
  KJ_EXPECT(src->openFile(Path({"foo", "baz", "qux"}))->readAllText() == "bazqux");
  KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
  KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux");

  KJ_EXPECT(dst->exists(Path({"link", "bar"})));
  src->remove(Path({"foo", "bar"}));
  KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
}

KJ_TEST("InMemoryDirectory move") {
  TestClock clock;

  auto src = newInMemoryDirectory(clock);
  auto dst = newInMemoryDirectory(clock);

  src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
     ->writeAll("foobar");
  src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
     ->writeAll("bazqux");
  clock.expectChanged(*src);
  clock.expectUnchanged(*dst);

  dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::MOVE);
  clock.expectChanged(*src);

  KJ_EXPECT(!src->exists(Path({"foo"})));
  KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
  KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux");
}

KJ_TEST("InMemoryDirectory createTemporary") {
  TestClock clock;

  auto dir = newInMemoryDirectory(clock);
  auto file = dir->createTemporary();
  file->writeAll("foobar");
  KJ_EXPECT(file->readAllText() == "foobar");
  KJ_EXPECT(dir->listNames() == nullptr);
}

}  // namespace
}  // namespace kj