/* 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/. */

#ifndef DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
#define DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_

#include <memory>  // We don't have a mozilla shared pointer for pod types

#include "TestHelpers.h"
#include "fs/FileSystemChildFactory.h"
#include "fs/FileSystemRequestHandler.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "js/Promise.h"
#include "js/RootingAPI.h"
#include "jsapi.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/DOMExceptionBinding.h"
#include "mozilla/dom/FileSystemHandle.h"
#include "mozilla/dom/FileSystemManagerChild.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "nsIGlobalObject.h"
#include "nsISupports.h"
#include "nsISupportsImpl.h"
#include "nsITimer.h"

class MockGlobalObject;

namespace mozilla::dom::fs {

inline std::ostream& operator<<(std::ostream& aOut,
                                const FileSystemEntryMetadata& aMetadata) {
  return aOut;
}

namespace test {

nsIGlobalObject* GetGlobal();

MockGlobalObject* GetMockGlobal();

nsresult GetAsString(const RefPtr<Promise>& aPromise, nsAString& aString);

mozilla::ipc::PrincipalInfo GetPrincipalInfo();

class MockFileSystemRequestHandler : public FileSystemRequestHandler {
 public:
  MOCK_METHOD(void, GetRootHandle,
              (RefPtr<FileSystemManager> aManager, RefPtr<Promise> aPromise,
               ErrorResult& aError),
              (override));

  MOCK_METHOD(void, GetDirectoryHandle,
              (RefPtr<FileSystemManager> & aManager,
               const FileSystemChildMetadata& aDirectory, bool aCreate,
               RefPtr<Promise> aPromise, ErrorResult& aError),
              (override));

  MOCK_METHOD(void, GetFileHandle,
              (RefPtr<FileSystemManager> & aManager,
               const FileSystemChildMetadata& aFile, bool aCreate,
               RefPtr<Promise> aPromise, ErrorResult& aError),
              (override));

  MOCK_METHOD(void, GetFile,
              (RefPtr<FileSystemManager> & aManager,
               const FileSystemEntryMetadata& aFile, RefPtr<Promise> aPromise,
               ErrorResult& aError),
              (override));

  MOCK_METHOD(void, GetEntries,
              (RefPtr<FileSystemManager> & aManager, const EntryId& aDirectory,
               PageNumber aPage, RefPtr<Promise> aPromise,
               RefPtr<FileSystemEntryMetadataArray>& aSink,
               ErrorResult& aError),
              (override));

  MOCK_METHOD(void, RemoveEntry,
              (RefPtr<FileSystemManager> & aManager,
               const FileSystemChildMetadata& aEntry, bool aRecursive,
               RefPtr<Promise> aPromise, ErrorResult& aError),
              (override));

  MOCK_METHOD(void, MoveEntry,
              (RefPtr<FileSystemManager> & aManager,
               RefPtr<FileSystemHandle> aHandle,
               FileSystemEntryMetadata* const aEntry,
               const FileSystemChildMetadata& aNewEntry,
               RefPtr<Promise> aPromise, ErrorResult& aError),
              (override));

  MOCK_METHOD(void, RenameEntry,
              (RefPtr<FileSystemManager> & aManager,
               RefPtr<FileSystemHandle> aHandle,
               FileSystemEntryMetadata* const aEntry, const Name& aName,
               RefPtr<Promise> aPromise, ErrorResult& aError),
              (override));

  MOCK_METHOD(void, Resolve,
              (RefPtr<FileSystemManager> & aManager,
               const FileSystemEntryPair& aEndpoints, RefPtr<Promise> aPromise,
               ErrorResult& aError),
              (override));
};

class WaitablePromiseListener {
 public:
  NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING

  virtual void ClearDone() = 0;

  virtual bool IsDone() const = 0;

  virtual PromiseNativeHandler* AsHandler() = 0;

 protected:
  virtual ~WaitablePromiseListener() = default;
};

template <class SuccessHandler, class ErrorHandler,
          uint32_t MilliSeconds = 2000u>
class TestPromiseListener : public PromiseNativeHandler,
                            public WaitablePromiseListener {
 public:
  TestPromiseListener()
      : mIsDone(std::make_shared<bool>(false)), mOnSuccess(), mOnError() {
    ClearDone();
  }

  // nsISupports implementation

  NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override {
    nsresult rv = NS_ERROR_UNEXPECTED;
    NS_INTERFACE_TABLE0(TestPromiseListener)

    return rv;
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TestPromiseListener, override)

  // PromiseNativeHandler implementation

  void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
                        ErrorResult& aError) override {
    mozilla::ScopeExit flagAsDone([isDone = mIsDone, timer = mTimer] {
      timer->Cancel();
      *isDone = true;
    });

    mOnSuccess();
  }

  void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
                        ErrorResult& aError) override {
    mozilla::ScopeExit flagAsDone([isDone = mIsDone, timer = mTimer] {
      timer->Cancel();
      *isDone = true;
    });

    if (aValue.isInt32()) {
      mOnError(static_cast<nsresult>(aValue.toInt32()));
      return;
    }

    ASSERT_TRUE(aValue.isObject());
    JS::Rooted<JSObject*> exceptionObject(aCx, &aValue.toObject());

    RefPtr<Exception> exception;
    UNWRAP_OBJECT(Exception, exceptionObject, exception);
    if (exception) {
      mOnError(static_cast<nsresult>(exception->Result()));
      return;
    }
  }

  // WaitablePromiseListener implementation

  void ClearDone() override {
    *mIsDone = false;
    if (mTimer) {
      mTimer->Cancel();
    }
    auto timerCallback = [isDone = mIsDone](nsITimer* aTimer) {
      *isDone = true;
      FAIL() << "Timed out!";
    };
    auto res = NS_NewTimerWithCallback(timerCallback, MilliSeconds,
                                       nsITimer::TYPE_ONE_SHOT,
                                       "fs::TestPromiseListener::ClearDone"_ns);
    if (res.isOk()) {
      mTimer = res.unwrap();
    }
  }

  bool IsDone() const override { return *mIsDone; }

  PromiseNativeHandler* AsHandler() override { return this; }

  SuccessHandler& GetSuccessHandler() { return mOnSuccess; }

  SuccessHandler& GetErrorHandler() { return mOnError; }

 protected:
  virtual ~TestPromiseListener() = default;

  std::shared_ptr<bool> mIsDone;  // We pass this to a callback

  nsCOMPtr<nsITimer> mTimer;

  SuccessHandler mOnSuccess;

  ErrorHandler mOnError;
};

class TestFileSystemManagerChild : public FileSystemManagerChild {
 public:
  MOCK_METHOD(void, SendGetRootHandle,
              (mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse> &&
                   aResolve,
               mozilla::ipc::RejectCallback&& aReject),
              (override));

  MOCK_METHOD(
      void, SendGetDirectoryHandle,
      (const FileSystemGetHandleRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse>&& aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendGetFileHandle,
      (const FileSystemGetHandleRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse>&& aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendGetAccessHandle,
      (const FileSystemGetAccessHandleRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemGetAccessHandleResponse>&&
           aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendGetWritable,
      (const FileSystemGetWritableRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemGetWritableFileStreamResponse>&&
           aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendGetFile,
      (const FileSystemGetFileRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemGetFileResponse>&& aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendResolve,
      (const FileSystemResolveRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemResolveResponse>&& aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendGetEntries,
      (const FileSystemGetEntriesRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemGetEntriesResponse>&& aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(
      void, SendRemoveEntry,
      (const FileSystemRemoveEntryRequest& request,
       mozilla::ipc::ResolveCallback<FileSystemRemoveEntryResponse>&& aResolve,
       mozilla::ipc::RejectCallback&& aReject),
      (override));

  MOCK_METHOD(void, Shutdown, (), (override));

 protected:
  virtual ~TestFileSystemManagerChild() = default;
};

class TestFileSystemChildFactory final : public FileSystemChildFactory {
 public:
  explicit TestFileSystemChildFactory(TestFileSystemManagerChild* aChild)
      : mChild(aChild) {}

  already_AddRefed<FileSystemManagerChild> Create() const override {
    return RefPtr<TestFileSystemManagerChild>(mChild).forget();
  }

  ~TestFileSystemChildFactory() = default;

 private:
  TestFileSystemManagerChild* mChild;
};

struct MockExpectMe {
  MOCK_METHOD0(InvokeMe, void());

  template <class... Args>
  void operator()(Args...) {
    InvokeMe();
  }
};

template <nsresult Expected>
struct NSErrorMatcher {
  void operator()(nsresult aErr) { ASSERT_NSEQ(Expected, aErr); }
};

struct FailOnCall {
  template <class... Args>
  void operator()(Args...) {
    FAIL();
  }
};

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

#define MOCK_PROMISE_LISTENER(name, ...) \
  using name = mozilla::dom::fs::test::TestPromiseListener<__VA_ARGS__>;

MOCK_PROMISE_LISTENER(
    ExpectNotImplemented, mozilla::dom::fs::test::FailOnCall,
    mozilla::dom::fs::test::NSErrorMatcher<NS_ERROR_NOT_IMPLEMENTED>);

MOCK_PROMISE_LISTENER(ExpectResolveCalled, mozilla::dom::fs::test::MockExpectMe,
                      mozilla::dom::fs::test::FailOnCall);

#define MOCKGLOBALOBJECT_IID                  \
  {/* c0a93d91-9c15-49e1-abed-890b3679ac6e */ \
   0xc0a93d91,                                \
   0x9c15,                                    \
   0x49e1,                                    \
   {0xab, 0xed, 0x89, 0x0b, 0x36, 0x79, 0xac, 0x6e}}

class MockGlobalObject : public nsIGlobalObject, public nsWrapperCache {
 public:
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MockGlobalObject)

  NS_INLINE_DECL_STATIC_IID(MOCKGLOBALOBJECT_IID)

  explicit MockGlobalObject(nsCOMPtr<nsIGlobalObject>&& aGlobal)
      : mGlobal(std::move(aGlobal)) {}

  JSObject* GetGlobalJSObject() override {
    return mGlobal->GetGlobalJSObject();
  }

  JSObject* GetGlobalJSObjectPreserveColor() const override {
    return mGlobal->GetGlobalJSObjectPreserveColor();
  }

  MOCK_METHOD(JSObject*, WrapObject,
              (JSContext * aCx, JS::Handle<JSObject*> aGivenProto), (override));

  MOCK_METHOD(nsICookieJarSettings*, GetCookieJarSettings, (), (override));

  MOCK_METHOD(nsIPrincipal*, PrincipalOrNull, (), (const));

  MOCK_METHOD(mozilla::StorageAccess, GetStorageAccess, (), (override));

  JS::loader::ModuleLoaderBase* GetModuleLoader(JSContext* aCx) override {
    return mGlobal->GetModuleLoader(aCx);
  }

  bool ShouldResistFingerprinting(RFPTarget aTarget) const override {
    return mGlobal->ShouldResistFingerprinting(std::move(aTarget));
  }

  nsISerialEventTarget* SerialEventTarget() const override {
    return mGlobal->SerialEventTarget();
  }

  nsresult Dispatch(already_AddRefed<nsIRunnable> aRunnable) const override {
    return mGlobal->Dispatch(std::move(aRunnable));
  }

  mozilla::OriginTrials Trials() const override { return mGlobal->Trials(); }

 protected:
  ~MockGlobalObject() = default;

  nsCOMPtr<nsIGlobalObject> mGlobal;
};

#endif  // DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
