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

#include "base/test/test_file_util.h"

#include <vector>

#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/test/test_timeouts.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread_restrictions.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace base {

namespace {

constexpr FilePath::CharType kDirPrefix[] =
    FILE_PATH_LITERAL("test_scoped_temp_dir");

// Deletes all registered file paths upon test completion. There can only be
// one instance at a time.
class PathDeleterOnTestEnd : public testing::EmptyTestEventListener {
 public:
  PathDeleterOnTestEnd() {
    DCHECK(!instance_);
    instance_ = this;
  }

  ~PathDeleterOnTestEnd() override {
    DCHECK_EQ(instance_, this);
    instance_ = nullptr;
  }

  PathDeleterOnTestEnd(const PathDeleterOnTestEnd&) = delete;
  PathDeleterOnTestEnd& operator=(const PathDeleterOnTestEnd&) = delete;

  static PathDeleterOnTestEnd* GetInstance() { return instance_; }

  void DeletePathRecursivelyUponTestEnd(const FilePath& path) {
    file_paths_to_delete_.push_back(path);
  }

  // EmptyTestEventListener overrides.
  void OnTestEnd(const testing::TestInfo& test_info) override {
    if (file_paths_to_delete_.empty()) {
      // Nothing to delete since the last test ended.
      return;
    }

    ScopedAllowBlockingForTesting allow_blocking;
    for (const FilePath& file_path : file_paths_to_delete_) {
      if (!DieFileDie(file_path, /*recurse=*/true)) {
        base::FileEnumerator enumerator(
            file_path, true,
            base::FileEnumerator::FILES | FileEnumerator::DIRECTORIES);
        int num_non_ignored_files = 0;
        for (base::FilePath failed_to_delete = enumerator.Next();
             !failed_to_delete.empty(); failed_to_delete = enumerator.Next()) {
#if BUILDFLAG(IS_WIN)
          // Ignore failure to delete a directory. Most likely, it's because
          // we failed to delete one or more files or subdirectories in the
          // directory. Otherwise, the paths allowed to leak would all have to
          // be top-level sub-directories of `file_path`. This could be worked
          // around, but it's not worth complicating the code.
          if (enumerator.GetInfo().IsDirectory()) {
            continue;
          }
          // Ignore this file if its path contains one of the sub-path names
          // known to leak on Windows. If not, the test will fail.
          if (!std::ranges::any_of(
                  GetPathsAllowedToLeak(),
                  [&failed_to_delete =
                       failed_to_delete.value()](const auto& allowed_to_leak) {
                    return failed_to_delete.contains(allowed_to_leak);
                  })) {
            ++num_non_ignored_files;
          }
#endif  // BUILDFLAG(IS_WIN)
          LOG(WARNING) << "failed to delete " << failed_to_delete;
        }
        EXPECT_EQ(num_non_ignored_files, 0)
            << "Failed to delete temporary directory for testing: "
            << file_path;
      }
    }
    file_paths_to_delete_.clear();
  }

 private:
  static PathDeleterOnTestEnd* instance_;
  std::vector<FilePath> file_paths_to_delete_;
};

// static
PathDeleterOnTestEnd* PathDeleterOnTestEnd::instance_ = nullptr;

FilePath RegisterPathDeleter(const FilePath& path) {
  if (!PathDeleterOnTestEnd::GetInstance()) {
    // Append() transfers ownership of the listener. This means
    // PathDeleterOnTestEnd::GetInstance() will return non-null until all tests
    // are run and the test suite destroyed.
    testing::UnitTest::GetInstance()->listeners().Append(
        new PathDeleterOnTestEnd());
    DCHECK(PathDeleterOnTestEnd::GetInstance());
  }

  PathDeleterOnTestEnd::GetInstance()->DeletePathRecursivelyUponTestEnd(path);

  return path;
}

}  // namespace

bool EvictFileFromSystemCacheWithRetry(const FilePath& path) {
  const int kCycles = 10;
  const TimeDelta kDelay = TestTimeouts::action_timeout() / kCycles;
  for (int i = 0; i < kCycles; i++) {
    if (EvictFileFromSystemCache(path)) {
      return true;
    }
    PlatformThread::Sleep(kDelay);
  }
  return false;
}

FilePath GetTempDirForTesting() {
  FilePath path;
  CHECK(GetTempDir(&path));
  return path;
}

#if BUILDFLAG(IS_WIN)
std::vector<std::wstring>& GetPathsAllowedToLeak() {
  static base::NoDestructor<std::vector<std::wstring>> paths_allowed_to_leak;
  return *paths_allowed_to_leak;
}
#endif  // BUILDFLAG(IS_WIN)

FilePath CreateUniqueTempDirectoryScopedToTest() {
  ScopedAllowBlockingForTesting allow_blocking;
  FilePath path;
  if (!CreateNewTempDirectory(kDirPrefix, &path)) {
    ADD_FAILURE() << "Failed to create unique temporary directory for testing.";
    return FilePath();
  }
  return RegisterPathDeleter(path);
}

FilePath CreateUniqueTempDirectoryScopedToTestInDir(const base::FilePath& dir) {
  ScopedAllowBlockingForTesting allow_blocking;
  FilePath path;
  if (!CreateTemporaryDirInDir(dir, kDirPrefix, &path)) {
    ADD_FAILURE() << "Failed to create unique temporary directory for testing.";
    return FilePath();
  }
  return RegisterPathDeleter(path);
}

}  // namespace base
