// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <cstring>
#include <memory>
#include <vector>

#include "flutter/fml/build_config.h"
#include "flutter/fml/file.h"
#include "flutter/fml/mapping.h"
#include "flutter/fml/paths.h"
#include "flutter/fml/unique_fd.h"
#include "gtest/gtest.h"

static bool WriteStringToFile(const fml::UniqueFD& fd,
                              const std::string& contents) {
  if (!fml::TruncateFile(fd, contents.size())) {
    return false;
  }

  fml::FileMapping mapping(fd, {fml::FileMapping::Protection::kWrite});
  if (mapping.GetSize() != contents.size()) {
    return false;
  }

  if (mapping.GetMutableMapping() == nullptr) {
    return false;
  }

  ::memmove(mapping.GetMutableMapping(), contents.data(), contents.size());
  return true;
}

static std::string ReadStringFromFile(const fml::UniqueFD& fd) {
  fml::FileMapping mapping(fd);

  if (mapping.GetMapping() == nullptr) {
    return {};
  }

  return {reinterpret_cast<const char*>(mapping.GetMapping()),
          mapping.GetSize()};
}

TEST(FileTest, CreateTemporaryAndUnlink) {
  auto dir_name = fml::CreateTemporaryDirectory();
  ASSERT_NE(dir_name, "");
  auto dir =
      fml::OpenDirectory(dir_name.c_str(), false, fml::FilePermission::kRead);
  ASSERT_TRUE(dir.is_valid());
  dir.reset();
  ASSERT_TRUE(fml::UnlinkDirectory(dir_name.c_str()));
}

TEST(FileTest, ScopedTempDirIsValid) {
  fml::ScopedTemporaryDirectory dir;
  ASSERT_TRUE(dir.fd().is_valid());
}

TEST(FileTest, CanOpenFileForWriting) {
  fml::ScopedTemporaryDirectory dir;
  ASSERT_TRUE(dir.fd().is_valid());

  auto fd =
      fml::OpenFile(dir.fd(), "some.txt", true, fml::FilePermission::kWrite);
  ASSERT_TRUE(fd.is_valid());
  fd.reset();
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "some.txt"));
}

TEST(FileTest, CanTruncateAndWrite) {
  fml::ScopedTemporaryDirectory dir;
  ASSERT_TRUE(dir.fd().is_valid());

  std::string contents = "some contents here";

  // On the first iteration, this tests writing and then reading a file that
  // didn't exist yet. On the second iteration it tests truncating, writing,
  // and reading a file that already existed.
  for (int i = 0; i < 2; i++) {
    {
      auto fd = fml::OpenFile(dir.fd(), "some.txt", true,
                              fml::FilePermission::kReadWrite);
      ASSERT_TRUE(fd.is_valid());

      ASSERT_TRUE(fml::TruncateFile(fd, contents.size()));

      fml::FileMapping mapping(fd, {fml::FileMapping::Protection::kWrite});
      ASSERT_EQ(mapping.GetSize(), contents.size());
      ASSERT_NE(mapping.GetMutableMapping(), nullptr);

      ::memcpy(mapping.GetMutableMapping(), contents.data(), contents.size());
    }

    {
      auto fd = fml::OpenFile(dir.fd(), "some.txt", false,
                              fml::FilePermission::kRead);
      ASSERT_TRUE(fd.is_valid());

      fml::FileMapping mapping(fd);
      ASSERT_EQ(mapping.GetSize(), contents.size());

      ASSERT_EQ(
          0, ::memcmp(mapping.GetMapping(), contents.data(), contents.size()));
    }
  }

  fml::UnlinkFile(dir.fd(), "some.txt");
}

TEST(FileTest, CreateDirectoryStructure) {
  fml::ScopedTemporaryDirectory dir;

  std::string contents = "These are my contents";
  {
    auto sub = fml::CreateDirectory(dir.fd(), {"a", "b", "c"},
                                    fml::FilePermission::kReadWrite);
    ASSERT_TRUE(sub.is_valid());
    auto file = fml::OpenFile(sub, "my_contents", true,
                              fml::FilePermission::kReadWrite);
    ASSERT_TRUE(file.is_valid());
    ASSERT_TRUE(WriteStringToFile(file, contents));
  }

  const char* file_path = "a/b/c/my_contents";

  {
    auto contents_file =
        fml::OpenFile(dir.fd(), file_path, false, fml::FilePermission::kRead);
    ASSERT_EQ(ReadStringFromFile(contents_file), contents);
  }

  // Cleanup.
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), file_path));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b/c"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a"));
}

TEST(FileTest, VisitFilesCanBeCalledTwice) {
  fml::ScopedTemporaryDirectory dir;

  {
    auto file = fml::OpenFile(dir.fd(), "my_contents", true,
                              fml::FilePermission::kReadWrite);
    ASSERT_TRUE(file.is_valid());
  }

  int count;
  fml::FileVisitor count_visitor = [&count](const fml::UniqueFD& directory,
                                            const std::string& filename) {
    count += 1;
    return true;
  };
  count = 0;
  fml::VisitFiles(dir.fd(), count_visitor);
  ASSERT_EQ(count, 1);

  // Without `rewinddir` in `VisitFiles`, the following check would fail.
  count = 0;
  fml::VisitFiles(dir.fd(), count_visitor);
  ASSERT_EQ(count, 1);

  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "my_contents"));
}

TEST(FileTest, CanListFilesRecursively) {
  fml::ScopedTemporaryDirectory dir;

  {
    auto c = fml::CreateDirectory(dir.fd(), {"a", "b", "c"},
                                  fml::FilePermission::kReadWrite);
    ASSERT_TRUE(c.is_valid());
    auto file1 =
        fml::OpenFile(c, "file1", true, fml::FilePermission::kReadWrite);
    auto file2 =
        fml::OpenFile(c, "file2", true, fml::FilePermission::kReadWrite);
    auto d = fml::CreateDirectory(c, {"d"}, fml::FilePermission::kReadWrite);
    ASSERT_TRUE(d.is_valid());
    auto file3 =
        fml::OpenFile(d, "file3", true, fml::FilePermission::kReadWrite);
    ASSERT_TRUE(file1.is_valid());
    ASSERT_TRUE(file2.is_valid());
    ASSERT_TRUE(file3.is_valid());
  }

  std::set<std::string> names;
  fml::FileVisitor visitor = [&names](const fml::UniqueFD& directory,
                                      const std::string& filename) {
    names.insert(filename);
    return true;
  };

  fml::VisitFilesRecursively(dir.fd(), visitor);
  ASSERT_EQ(names, std::set<std::string>(
                       {"a", "b", "c", "d", "file1", "file2", "file3"}));

  // Cleanup.
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "a/b/c/d/file3"));
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "a/b/c/file1"));
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "a/b/c/file2"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b/c/d"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b/c"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a"));
}

TEST(FileTest, CanStopVisitEarly) {
  fml::ScopedTemporaryDirectory dir;

  {
    auto d = fml::CreateDirectory(dir.fd(), {"a", "b", "c", "d"},
                                  fml::FilePermission::kReadWrite);
    ASSERT_TRUE(d.is_valid());
  }

  std::set<std::string> names;
  fml::FileVisitor visitor = [&names](const fml::UniqueFD& directory,
                                      const std::string& filename) {
    names.insert(filename);
    return filename == "c" ? false : true;  // stop if c is found
  };

  // Check the d is not visited as we stop at c.
  ASSERT_FALSE(fml::VisitFilesRecursively(dir.fd(), visitor));
  ASSERT_EQ(names, std::set<std::string>({"a", "b", "c"}));

  // Cleanup.
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b/c/d"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b/c"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a/b"));
  ASSERT_TRUE(fml::UnlinkDirectory(dir.fd(), "a"));
}

TEST(FileTest, AtomicWriteTest) {
  fml::ScopedTemporaryDirectory dir;

  const std::string contents = "These are my contents.";

  auto data = std::make_unique<fml::DataMapping>(
      std::vector<uint8_t>{contents.begin(), contents.end()});

  // Write.
  ASSERT_TRUE(fml::WriteAtomically(dir.fd(), "precious_data", *data));

  // Read and verify.
  ASSERT_EQ(contents,
            ReadStringFromFile(fml::OpenFile(dir.fd(), "precious_data", false,
                                             fml::FilePermission::kRead)));

  // Cleanup.
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "precious_data"));
}

TEST(FileTest, IgnoreBaseDirWhenPathIsAbsolute) {
  fml::ScopedTemporaryDirectory dir;

  // Make an absolute path.
  std::string filename = "filename.txt";
  std::string full_path =
      fml::paths::AbsolutePath(fml::paths::JoinPaths({dir.path(), filename}));

  const std::string contents = "These are my contents.";
  auto data = std::make_unique<fml::DataMapping>(
      std::vector<uint8_t>{contents.begin(), contents.end()});

  // Write.
  ASSERT_TRUE(fml::WriteAtomically(dir.fd(), full_path.c_str(), *data));

  // Test existence.
  ASSERT_TRUE(fml::FileExists(dir.fd(), full_path.c_str()));

  // Read and verify.
  ASSERT_EQ(contents,
            ReadStringFromFile(fml::OpenFile(dir.fd(), full_path.c_str(), false,
                                             fml::FilePermission::kRead)));

  // Cleanup.
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), full_path.c_str()));
}

TEST(FileTest, EmptyMappingTest) {
  fml::ScopedTemporaryDirectory dir;

  {
    auto file = fml::OpenFile(dir.fd(), "my_contents", true,
                              fml::FilePermission::kReadWrite);

    fml::FileMapping mapping(file);
    ASSERT_TRUE(mapping.IsValid());
    ASSERT_EQ(mapping.GetSize(), 0ul);
    ASSERT_EQ(mapping.GetMapping(), nullptr);
  }

  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "my_contents"));
}

TEST(FileTest, MappingDontNeedSafeTest) {
  fml::ScopedTemporaryDirectory dir;

  {
    auto file = fml::OpenFile(dir.fd(), "my_contents", true,
                              fml::FilePermission::kReadWrite);
    WriteStringToFile(file, "some content");
  }

  {
    auto file = fml::OpenFile(dir.fd(), "my_contents", false,
                              fml::FilePermission::kRead);
    fml::FileMapping mapping(file);
    ASSERT_TRUE(mapping.IsValid());
    ASSERT_EQ(mapping.GetMutableMapping(), nullptr);
    ASSERT_TRUE(mapping.IsDontNeedSafe());
  }

  {
    auto file = fml::OpenFile(dir.fd(), "my_contents", false,
                              fml::FilePermission::kReadWrite);
    fml::FileMapping mapping(file, {fml::FileMapping::Protection::kRead,
                                    fml::FileMapping::Protection::kWrite});
    ASSERT_TRUE(mapping.IsValid());
    ASSERT_NE(mapping.GetMutableMapping(), nullptr);
    ASSERT_FALSE(mapping.IsDontNeedSafe());
  }
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), "my_contents"));
}

TEST(FileTest, FileTestsWork) {
  fml::ScopedTemporaryDirectory dir;
  ASSERT_TRUE(dir.fd().is_valid());
  const char* filename = "some.txt";
  auto fd =
      fml::OpenFile(dir.fd(), filename, true, fml::FilePermission::kWrite);
  ASSERT_TRUE(fd.is_valid());
  fd.reset();
  ASSERT_TRUE(fml::FileExists(dir.fd(), filename));
  ASSERT_TRUE(
      fml::IsFile(fml::paths::JoinPaths({dir.path(), filename}).c_str()));
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), filename));
}

TEST(FileTest, FileTestsSupportsUnicode) {
  fml::ScopedTemporaryDirectory dir;
  ASSERT_TRUE(dir.fd().is_valid());
  const auto* filename = reinterpret_cast<const char*>(u8"äëïöüテスト☃");
  auto fd =
      fml::OpenFile(dir.fd(), filename, true, fml::FilePermission::kWrite);
  ASSERT_TRUE(fd.is_valid());
  fd.reset();
  ASSERT_TRUE(fml::FileExists(dir.fd(), filename));
  ASSERT_TRUE(
      fml::IsFile(fml::paths::JoinPaths({dir.path(), filename}).c_str()));
  ASSERT_TRUE(fml::UnlinkFile(dir.fd(), filename));
}
