/* 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 "FileSystemBackgroundRequestHandler.h"
#include "FileSystemEntryMetadataArray.h"
#include "FileSystemMocks.h"
#include "fs/FileSystemRequestHandler.h"
#include "gtest/gtest.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/FileSystemManager.h"
#include "mozilla/dom/FileSystemManagerChild.h"
#include "mozilla/dom/IPCBlob.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/dom/PFileSystemManager.h"
#include "mozilla/dom/StorageManager.h"
#include "mozilla/ipc/FileDescriptorUtils.h"
#include "mozilla/ipc/IPCCore.h"
#include "mozilla/net/CookieJarSettings.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIFile.h"

using ::testing::_;
using ::testing::ByRef;
using ::testing::Invoke;
using ::testing::Return;

namespace mozilla::dom::fs::test {

class TestFileSystemRequestHandler : public ::testing::Test {
 protected:
  void SetUp() override {
    mMockGlobal = GetMockGlobal();
    mGlobal = mMockGlobal;

    mCookieJarSettings = mozilla::net::CookieJarSettings::Create(
        mozilla::net::CookieJarSettings::CreateMode::eRegular,
        /* Should resist fingerprinting */ true);
    mListener = MakeAndAddRef<ExpectResolveCalled>();

    mChild = FileSystemChildMetadata("parent"_ns, u"ChildName"_ns);
    mEntry = FileSystemEntryMetadata("myid"_ns, u"EntryName"_ns,
                                     /* directory */ false);
    mName = u"testDir"_ns;
    mFileSystemManagerChild = MakeAndAddRef<TestFileSystemManagerChild>();
    mManager = MakeAndAddRef<FileSystemManager>(
        mGlobal, nullptr,
        MakeRefPtr<FileSystemBackgroundRequestHandler>(
            mFileSystemManagerChild));
  }

  void TearDown() override {
    if (!mManager->IsShutdown()) {
      EXPECT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
    }
  }

  already_AddRefed<Promise> GetDefaultPromise() {
    IgnoredErrorResult rv;
    RefPtr<Promise> result = Promise::Create(mGlobal, rv);
    mListener->ClearDone();
    result->AppendNativeHandler(mListener->AsHandler());

    return result.forget();
  }

  already_AddRefed<Promise> GetSimplePromise() {
    IgnoredErrorResult rv;
    RefPtr<Promise> result = Promise::Create(mGlobal, rv);

    return result.forget();
  }

  already_AddRefed<Promise> GetShutdownPromise() {
    RefPtr<Promise> promise = GetDefaultPromise();
    EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
        .WillOnce(Invoke([promise]() { promise->MaybeResolveWithUndefined(); }))
        .WillOnce(Return());
    EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());

    return promise.forget();
  }

  UniquePtr<FileSystemRequestHandler> GetFileSystemRequestHandler() {
    return MakeUnique<FileSystemRequestHandler>();
  }

  void ShutdownFileSystemManager() {
    RefPtr<Promise> promise = GetShutdownPromise();

    mManager->Shutdown();

    SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                       [this]() { return mListener->IsDone(); });
    ASSERT_TRUE(mManager->IsShutdown());
  }

  void AllowStorageAccess() {
    EXPECT_CALL(*mMockGlobal, GetCookieJarSettings())
        .WillOnce(::testing::Return(&*mCookieJarSettings));

    EXPECT_CALL(*mMockGlobal, GetStorageAccess())
        .WillOnce(::testing::Return(mozilla::StorageAccess::eAllow));
  }

  MockGlobalObject* mMockGlobal;
  nsCOMPtr<nsIGlobalObject> mGlobal;

  RefPtr<ExpectResolveCalled> mListener;

  FileSystemChildMetadata mChild;
  FileSystemEntryMetadata mEntry;
  nsString mName;
  RefPtr<TestFileSystemManagerChild> mFileSystemManagerChild;
  RefPtr<FileSystemManager> mManager;
  RefPtr<nsICookieJarSettings> mCookieJarSettings;
};

TEST_F(TestFileSystemRequestHandler, isGetRootHandleSuccessful) {
  AllowStorageAccess();

  auto fakeResponse = [](auto&& aResolve, auto&& /* aReject */) {
    EntryId expected = "expected"_ns;
    FileSystemGetHandleResponse response(expected);
    aResolve(std::move(response));
  };

  EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
  EXPECT_CALL(*mFileSystemManagerChild, SendGetRootHandle(_, _))
      .WillOnce(Invoke(fakeResponse));

  RefPtr<Promise> promise = GetDefaultPromise();
  auto testable = GetFileSystemRequestHandler();
  testable->GetRootHandle(mManager, promise, IgnoredErrorResult());
  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [this]() { return mListener->IsDone(); });
}

TEST_F(TestFileSystemRequestHandler, isGetRootHandleBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetRootHandle(mManager, GetSimplePromise(),
                                               error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isGetDirectoryHandleSuccessful) {
  AllowStorageAccess();

  auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
                         auto&& /* aReject */) {
    EntryId expected = "expected"_ns;
    FileSystemGetHandleResponse response(expected);
    aResolve(std::move(response));
  };

  EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
  EXPECT_CALL(*mFileSystemManagerChild, SendGetDirectoryHandle(_, _, _))
      .WillOnce(Invoke(fakeResponse));

  RefPtr<Promise> promise = GetDefaultPromise();
  auto testable = GetFileSystemRequestHandler();
  testable->GetDirectoryHandle(mManager, mChild,
                               /* create */ true, promise,
                               IgnoredErrorResult());
  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [this]() { return mListener->IsDone(); });
}

TEST_F(TestFileSystemRequestHandler, isGetDirectoryHandleBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetDirectoryHandle(
      mManager, mChild, /* aCreate */ true, GetSimplePromise(), error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isGetFileHandleSuccessful) {
  AllowStorageAccess();

  auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
                         auto&& /* aReject */) {
    EntryId expected = "expected"_ns;
    FileSystemGetHandleResponse response(expected);
    aResolve(std::move(response));
  };

  EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
  EXPECT_CALL(*mFileSystemManagerChild, SendGetFileHandle(_, _, _))
      .WillOnce(Invoke(fakeResponse));

  RefPtr<Promise> promise = GetDefaultPromise();
  auto testable = GetFileSystemRequestHandler();
  testable->GetFileHandle(mManager, mChild, /* create */ true, promise,
                          IgnoredErrorResult());
  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [this]() { return mListener->IsDone(); });
}

TEST_F(TestFileSystemRequestHandler, isGetFileHandleBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetFileHandle(
      mManager, mChild, /* aCreate */ true, GetSimplePromise(), error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isGetFileSuccessful) {
  AllowStorageAccess();

  auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
                         auto&& /* aReject */) {
    // We have to create a temporary file
    nsCOMPtr<nsIFile> tmpfile;
    nsresult rv =
        NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpfile));
    ASSERT_EQ(NS_SUCCEEDED(rv), true);

    rv = tmpfile->AppendNative("GetFileTestBlob"_ns);
    ASSERT_EQ(NS_SUCCEEDED(rv), true);

    rv = tmpfile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666);
    ASSERT_EQ(NS_SUCCEEDED(rv), true);

    auto blob = MakeRefPtr<FileBlobImpl>(tmpfile);

    TimeStamp last_modified_ms = 0;
    ContentType type = "txt"_ns;
    IPCBlob file;
    IPCBlobUtils::Serialize(blob, file);

    nsTArray<Name> path;
    path.AppendElement(u"root"_ns);
    path.AppendElement(u"Trash"_ns);

    FileSystemFileProperties properties(last_modified_ms, file, type, path);
    FileSystemGetFileResponse response(properties);
    aResolve(std::move(response));
  };

  EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
  EXPECT_CALL(*mFileSystemManagerChild, SendGetFile(_, _, _))
      .WillOnce(Invoke(fakeResponse));

  RefPtr<Promise> promise = GetDefaultPromise();
  auto testable = GetFileSystemRequestHandler();
  testable->GetFile(mManager, mEntry, promise, IgnoredErrorResult());
  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [this]() { return mListener->IsDone(); });
}

TEST_F(TestFileSystemRequestHandler, isGetFileBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetFile(mManager, mEntry, GetSimplePromise(),
                                         error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isGetAccessHandleBlockedAfterShutdown) {
  RefPtr<Promise> promise = GetShutdownPromise();

  mManager->Shutdown();

  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [this]() { return mListener->IsDone(); });
  ASSERT_TRUE(mManager->IsShutdown());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetAccessHandle(mManager, mEntry,
                                                 GetSimplePromise(), error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isGetWritableBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetWritable(
      mManager, mEntry, /* aKeepData */ false, GetSimplePromise(), error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isGetEntriesSuccessful) {
  AllowStorageAccess();

  auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
                         auto&& /* aReject */) {
    nsTArray<FileSystemEntryMetadata> files;
    nsTArray<FileSystemEntryMetadata> directories;
    FileSystemDirectoryListing listing(files, directories);
    FileSystemGetEntriesResponse response(listing);
    aResolve(std::move(response));
  };

  RefPtr<ExpectResolveCalled> listener = MakeAndAddRef<ExpectResolveCalled>();
  IgnoredErrorResult rv;
  listener->ClearDone();
  EXPECT_CALL(listener->GetSuccessHandler(), InvokeMe());

  RefPtr<Promise> promise = Promise::Create(mGlobal, rv);
  promise->AppendNativeHandler(listener);

  EXPECT_CALL(*mFileSystemManagerChild, SendGetEntries(_, _, _))
      .WillOnce(Invoke(fakeResponse));

  auto testable = GetFileSystemRequestHandler();
  RefPtr<FileSystemEntryMetadataArray> sink;

  testable->GetEntries(mManager, mEntry.entryId(), /* page */ 0, promise, sink,
                       IgnoredErrorResult());
  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [listener]() { return listener->IsDone(); });
}

TEST_F(TestFileSystemRequestHandler, isGetEntriesBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  RefPtr<FileSystemEntryMetadataArray> sink;

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->GetEntries(mManager, mEntry.entryId(),
                                            /* aPage */ 0, GetSimplePromise(),
                                            sink, error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

TEST_F(TestFileSystemRequestHandler, isRemoveEntrySuccessful) {
  AllowStorageAccess();

  auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
                         auto&& /* aReject */) {
    FileSystemRemoveEntryResponse response(mozilla::void_t{});
    aResolve(std::move(response));
  };

  EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
  EXPECT_CALL(*mFileSystemManagerChild, SendRemoveEntry(_, _, _))
      .WillOnce(Invoke(fakeResponse));

  auto testable = GetFileSystemRequestHandler();
  RefPtr<Promise> promise = GetDefaultPromise();
  testable->RemoveEntry(mManager, mChild, /* recursive */ true, promise,
                        IgnoredErrorResult());
  SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
                     [this]() { return mListener->IsDone(); });
}

TEST_F(TestFileSystemRequestHandler, isRemoveEntryBlockedAfterShutdown) {
  ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());

  IgnoredErrorResult error;
  GetFileSystemRequestHandler()->RemoveEntry(
      mManager, mChild, /* aRecursive */ true, GetSimplePromise(), error);

  ASSERT_TRUE(error.Failed());
  ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
}

}  // namespace mozilla::dom::fs::test
