/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/UniquePtr.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/DOMExceptionBinding.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"

namespace mozilla::dom::fs {

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

namespace test {

nsIGlobalObject* GetGlobal();

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, FileSystemHandle* aHandle,
               FileSystemEntryMetadata* const aEntry,
               const FileSystemChildMetadata& aNewEntry,
               RefPtr<Promise> aPromise, ErrorResult& aError),
              (override));

  MOCK_METHOD(void, RenameEntry,
              (RefPtr<FileSystemManager> & aManager, 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)),
        mTimer(),
        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!";
    };
    const char* timerName = "fs::TestPromiseListener::ClearDone";
    auto res = NS_NewTimerWithCallback(timerCallback, MilliSeconds,
                                       nsITimer::TYPE_ONE_SHOT, timerName);
    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);

#endif  // DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
