/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 */

#include <folly/File.h>

#include <fstream>
#include <random>

#include <fmt/core.h>
#include <glog/logging.h>

#include <folly/String.h>
#include <folly/portability/Fcntl.h>
#include <folly/portability/Filesystem.h>
#include <folly/portability/GTest.h>

using namespace folly;

namespace {

void expectWouldBlock(ssize_t r) {
  int savedErrno = errno;
  EXPECT_EQ(-1, r);
  EXPECT_EQ(EAGAIN, savedErrno) << errnoStr(savedErrno);
}
void expectOK(ssize_t r) {
  int savedErrno = errno;
  EXPECT_LE(0, r) << ": errno=" << errnoStr(savedErrno);
}

class TempDir {
 public:
  static fs::path make_path() {
    std::random_device rng;
    std::uniform_int_distribution<uint64_t> dist;
    auto basename = "folly-unit-test-" + fmt::format("{:016x}", dist(rng));
    return fs::canonical(fs::temp_directory_path()) / basename;
  }
  fs::path const path{make_path()};
  TempDir() { fs::create_directory(path); }
  ~TempDir() { fs::remove_all(path); }
};

} // namespace

TEST(File, Simple) {
  TempDir tmpd;
  auto tmpf = tmpd.path / "foobar.txt";
  std::ofstream{tmpf.native()} << "hello world" << std::endl;

  // Open a file, ensure it's indeed open for reading
  char buf = 'x';
  {
    File f(tmpf.string());
    EXPECT_NE(-1, f.fd());
    EXPECT_EQ(1, ::read(f.fd(), &buf, 1));
    f.close();
    EXPECT_EQ(-1, f.fd());
  }
}

TEST(File, SimpleStringPiece) {
  TempDir tmpd;
  auto tmpf = tmpd.path / "foobar.txt";
  std::ofstream{tmpf.native()} << "hello world" << std::endl;

  char buf = 'x';
  File f(StringPiece(tmpf.string()));
  EXPECT_NE(-1, f.fd());
  EXPECT_EQ(1, ::read(f.fd(), &buf, 1));
  f.close();
  EXPECT_EQ(-1, f.fd());
}

TEST(File, OwnsFd) {
  // Wrap a file descriptor, make sure that ownsFd works
  // We'll test that the file descriptor is closed by closing the writing
  // end of a pipe and making sure that a non-blocking read from the reading
  // end returns 0.

  char buf = 'x';
  int p[2];
  expectOK(::pipe(p));
  int flags = ::fcntl(p[0], F_GETFL);
  expectOK(flags);
  expectOK(::fcntl(p[0], F_SETFL, flags | O_NONBLOCK));
  expectWouldBlock(::read(p[0], &buf, 1));
  {
    File f(p[1]);
    EXPECT_EQ(p[1], f.fd());
  }
  // Ensure that moving the file doesn't close it
  {
    File f(p[1]);
    EXPECT_EQ(p[1], f.fd());
    File f1(std::move(f));
    EXPECT_EQ(-1, f.fd());
    EXPECT_EQ(p[1], f1.fd());
  }
  expectWouldBlock(::read(p[0], &buf, 1)); // not closed
  {
    File f(p[1], true);
    EXPECT_EQ(p[1], f.fd());
  }
  ssize_t r = ::read(p[0], &buf, 1); // eof
  expectOK(r);
  EXPECT_EQ(0, r);
  ::close(p[0]);
}

TEST(File, Release) {
  File in(STDOUT_FILENO, false);
  CHECK_EQ(STDOUT_FILENO, in.release());
  CHECK_EQ(-1, in.release());
}

#define EXPECT_CONTAINS(haystack, needle)                                     \
  EXPECT_NE(::std::string::npos, ::folly::StringPiece(haystack).find(needle)) \
      << "Haystack: '" << haystack << "'\nNeedle: '" << needle << "'";

TEST(File, UsefulError) {
  try {
    File("does_not_exist.txt", 0, 0666);
  } catch (const std::runtime_error& e) {
    EXPECT_CONTAINS(e.what(), "does_not_exist.txt");
    EXPECT_CONTAINS(e.what(), "0666");
  }
}

TEST(File, Truthy) {
  File temp = File::temporary();

  EXPECT_TRUE(bool(temp));

  if (temp) {
    ;
  } else {
    ADD_FAILURE();
  }

  if (File file = File::temporary()) {
    ;
  } else {
    ADD_FAILURE();
  }

  EXPECT_FALSE(bool(File()));
  if (File()) {
    ADD_FAILURE();
  }
  if (File notOpened = File()) {
    ADD_FAILURE();
  }
}

TEST(File, Dup) {
  auto f = File::temporary();

  auto d = f.dup();
#ifndef _WIN32
  EXPECT_EQ(::fcntl(d.fd(), F_GETFD, 0) & FD_CLOEXEC, 0);
#endif
  (void)d;
}

TEST(File, DupCloseOnExec) {
  auto f = File::temporary();

  auto d = f.dupCloseOnExec();
#ifndef _WIN32
  EXPECT_EQ(::fcntl(d.fd(), F_GETFD, 0) & FD_CLOEXEC, FD_CLOEXEC);
#endif
  (void)d;
}

TEST(File, HelperCtor) {
  TempDir tmpd;
  auto tmpf = tmpd.path / "foobar.txt";

  File::makeFile(StringPiece(tmpf.string())).then([](File&& f) {
    char buf = 'x';
    EXPECT_NE(-1, f.fd());
    EXPECT_EQ(1, ::read(f.fd(), &buf, 1));
    f.close();
    EXPECT_EQ(-1, f.fd());
  });
}
