/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <thread>

#include "SpecialSystemDirectory.h"
#include "gtest/gtest.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/dom/BlobImpl.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/GetFilesHelper.h"
#include "mozilla/dom/UnionTypes.h"
#include "mozilla/gtest/MozAssertions.h"
#include "mozilla/media/MediaUtils.h"
#include "nsIFile.h"

using namespace mozilla;
using namespace mozilla::dom;

nsCOMPtr<nsIFile> MakeFileFromPathSegments(
    const nsTArray<const char*>& aPathSegments) {
  nsCOMPtr<nsIFile> file;
  MOZ_ALWAYS_SUCCEEDS(GetSpecialSystemDirectory(OS_CurrentWorkingDirectory,
                                                getter_AddRefs(file)));
  for (const auto* pathSegment : aPathSegments) {
    MOZ_ALWAYS_SUCCEEDS(
        file->AppendRelativePath(NS_ConvertASCIItoUTF16(pathSegment)));
  }
  return file;
}

nsresult AppendFileOrDirectory(nsTArray<OwningFileOrDirectory>& aDirectories,
                               const nsTArray<const char*>& aPathSegments) {
  bool exists = false;
  int retryCount = 5;
  nsCOMPtr<nsIFile> file = MakeFileFromPathSegments(aPathSegments);
  while (retryCount--) {
    if (NS_SUCCEEDED(file->Exists(&exists))) {
      break;
    }
    // May require retrying (bug 1963029)
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }
  NS_ENSURE_TRUE(exists, NS_ERROR_FILE_NOT_FOUND);
  bool isDir;
  MOZ_ALWAYS_SUCCEEDS(file->IsDirectory(&isDir));
  if (isDir) {
    // We just need to iterate over the directory, so use the junk scope
    RefPtr<Directory> directory =
        Directory::Create(xpc::NativeGlobal(xpc::PrivilegedJunkScope()), file);
    NS_ENSURE_TRUE(directory, NS_ERROR_FAILURE);

    OwningFileOrDirectory* owningDirectory = aDirectories.EmplaceBack();
    owningDirectory->SetAsDirectory() = directory;
  } else {
    OwningFileOrDirectory* owningDirectory = aDirectories.EmplaceBack();
    RefPtr<File> fileObject = File::CreateFromFile(
        xpc::NativeGlobal(xpc::PrivilegedJunkScope()), file);
    owningDirectory->SetAsFile() = fileObject;
  }
  return NS_OK;
}

struct BoolStruct {
  bool mValue = false;
};

class FilesCallback : public GetFilesCallback {
 public:
  FilesCallback(std::atomic<bool>& aGotResponse,
                const nsTArray<nsString>& aExpectedPaths)
      : mGotResponse(aGotResponse), mExpectedPaths(aExpectedPaths.Clone()) {}

  // -------------------
  // GetFilesCallback
  // -------------------
  void Callback(nsresult aStatus,
                const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
    EXPECT_EQ(aBlobImpls.Length(), mExpectedPaths.Length());
    for (const auto& blob : aBlobImpls) {
      nsString path;
      ErrorResult error;
      blob->GetMozFullPathInternal(path, error);
      ASSERT_EQ(error.StealNSResult(), NS_OK);
      ASSERT_TRUE(mExpectedPaths.Contains(path));
    }
    mGotResponse = true;
  }

 private:
  std::atomic<bool>& mGotResponse;
  nsTArray<nsString> mExpectedPaths;
};

nsTArray<nsString> GetExpectedPaths(
    const nsTArray<nsTArray<const char*>>& aPathSegmentsArray) {
  nsTArray<nsString> expectedPaths(aPathSegmentsArray.Length());
  for (const auto& pathSegments : aPathSegmentsArray) {
    auto file = MakeFileFromPathSegments(pathSegments);
    nsString expectedPath;
    MOZ_ALWAYS_SUCCEEDS(file->GetPath(expectedPath));
    expectedPaths.AppendElement(expectedPath);
  }
  return expectedPaths;
}

void ExpectGetFilesHelperResponse(
    RefPtr<GetFilesHelper> aHelper,
    const nsTArray<nsTArray<const char*>>& aPathSegmentsArray) {
  nsTArray<nsString> expectedPaths = GetExpectedPaths(aPathSegmentsArray);

  std::atomic<bool> gotCallbackResponse = false;
  std::atomic<bool> gotMozPromiseResponse = false;
  RefPtr<FilesCallback> callback =
      MakeRefPtr<FilesCallback>(gotCallbackResponse, expectedPaths);
  aHelper->AddCallback(callback);
  auto mozPromise = MakeRefPtr<GetFilesHelper::MozPromiseType>(__func__);
  aHelper->AddMozPromise(mozPromise,
                         xpc::NativeGlobal(xpc::PrivilegedJunkScope()));
  mozPromise->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [&gotMozPromiseResponse,
       &expectedPaths](const nsTArray<RefPtr<mozilla::dom::File>>& aFiles) {
        EXPECT_EQ(aFiles.Length(), expectedPaths.Length());
        for (const auto& file : aFiles) {
          nsString path;
          ErrorResult error;
          file->GetMozFullPathInternal(path, error);
          ASSERT_EQ(error.StealNSResult(), NS_OK);
          ASSERT_TRUE(expectedPaths.Contains(path));
        }
        gotMozPromiseResponse = true;
      },
      []() { FAIL() << "MozPromise got rejected!"; });
  // Make timedOut a RefPtr so if we get a response after this function
  // has finished we can safely check that (and don't start accessing stack
  // values that don't exist anymore)
  RefPtr timedOut = MakeRefPtr<media::Refcountable<BoolStruct>>();

  RefPtr<CancelableRunnable> timer =
      NS_NewCancelableRunnableFunction("GetFilesHelper timeout", [&] {
        if (!gotCallbackResponse.load() || !gotMozPromiseResponse.load()) {
          timedOut->mValue = true;
        }
      });
  constexpr uint32_t kTimeout = 10000;
  NS_DelayedDispatchToCurrentThread(do_AddRef(timer), kTimeout);
  mozilla::SpinEventLoopUntil(
      "Waiting for GetFilesHelper result"_ns, [&, timedOut]() {
        return (gotCallbackResponse.load() && gotMozPromiseResponse.load()) ||
               timedOut->mValue;
      });
  timer->Cancel();
  EXPECT_TRUE(gotCallbackResponse);
  EXPECT_TRUE(gotMozPromiseResponse);
  EXPECT_FALSE(timedOut->mValue);
}

TEST(GetFilesHelper, TestSingleDirectory)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(
      AppendFileOrDirectory(directories, {"getfiles", "inner2"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, true, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner2", "fileinner2.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}

TEST(GetFilesHelper, TestSingleNestedDirectory)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(
      AppendFileOrDirectory(directories, {"getfiles", "inner1"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, true, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "fileinner1.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "inner", "fileinnerinner1.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}

TEST(GetFilesHelper, TestSingleNestedDirectoryNoRecursion)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(
      AppendFileOrDirectory(directories, {"getfiles", "inner1"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, false, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "fileinner1.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}

TEST(GetFilesHelper, TestSingleDirectoryWithMultipleNestedChildren)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(AppendFileOrDirectory(directories, {"getfiles"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, true, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(nsTArray{"getfiles", "file1.txt"});
  pathSegmentsArray.AppendElement(nsTArray{"getfiles", "file2.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "fileinner1.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "inner", "fileinnerinner1.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner2", "fileinner2.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}

TEST(GetFilesHelper, TestSingleFile)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(AppendFileOrDirectory(
      directories, {"getfiles", "inner1", "fileinner1.txt"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, true, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "fileinner1.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}

TEST(GetFilesHelper, TestMultipleFiles)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(AppendFileOrDirectory(
      directories, {"getfiles", "inner1", "fileinner1.txt"}));
  ASSERT_NS_SUCCEEDED(AppendFileOrDirectory(
      directories, {"getfiles", "inner2", "fileinner2.txt"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, true, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "fileinner1.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner2", "fileinner2.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}

// Content Analysis can use GetFilesHelper with multiple directories,
// so make sure that works.
TEST(GetFilesHelper, TestMultipleDirectories)
{
  nsTArray<OwningFileOrDirectory> directories;
  ASSERT_NS_SUCCEEDED(
      AppendFileOrDirectory(directories, {"getfiles", "inner1"}));
  ASSERT_NS_SUCCEEDED(
      AppendFileOrDirectory(directories, {"getfiles", "inner2"}));

  ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directories, true, error);
  ASSERT_EQ(error.StealNSResult(), NS_OK);

  nsTArray<nsTArray<const char*>> pathSegmentsArray;
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "fileinner1.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner1", "inner", "fileinnerinner1.txt"});
  pathSegmentsArray.AppendElement(
      nsTArray{"getfiles", "inner2", "fileinner2.txt"});
  ExpectGetFilesHelperResponse(helper, pathSegmentsArray);
}
