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

#include "gtest/gtest.h"

#include "mozilla/_ipdltest/IPDLUnitTest.h"
#include "mozilla/ipc/IOThreadChild.h"
#include "mozilla/ipc/NodeController.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/ipc/ProcessChild.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "nsDebugImpl.h"
#include "nsThreadManager.h"

#include <string>

#ifdef MOZ_WIDGET_ANDROID
#  include "nsIAppShell.h"
#  include "nsServiceManagerUtils.h"
#  include "nsWidgetsCID.h"
#endif

namespace mozilla::_ipdltest {

static std::unordered_map<std::string_view, ipc::IToplevelProtocol* (*)()>
    sAllocChildActorRegistry;

const char* RegisterAllocChildActor(const char* aName,
                                    ipc::IToplevelProtocol* (*aFunc)()) {
  sAllocChildActorRegistry[aName] = aFunc;
  return aName;
}

already_AddRefed<IPDLUnitTestParent> IPDLUnitTestParent::CreateCrossProcess() {
#ifdef MOZ_WIDGET_ANDROID
  // Force-initialize the appshell on android, as android child process
  // launching depends on widget component initialization.
  nsCOMPtr<nsIAppShell> _appShell = do_GetService(NS_APPSHELL_CID);
#endif

  RefPtr<IPDLUnitTestParent> parent = new IPDLUnitTestParent();
  parent->mSubprocess =
      new ipc::GeckoChildProcessHost(GeckoProcessType_IPDLUnitTest);

  std::vector<std::string> extraArgs;

  auto prefSerializer = MakeUnique<ipc::SharedPreferenceSerializer>();
  if (!prefSerializer->SerializeToSharedMemory(GeckoProcessType_IPDLUnitTest,
                                               /* remoteType */ ""_ns)) {
    ADD_FAILURE()
        << "SharedPreferenceSerializer::SerializeToSharedMemory failed";
    return nullptr;
  }
  prefSerializer->AddSharedPrefCmdLineArgs(*parent->mSubprocess, extraArgs);

  if (!parent->mSubprocess->SyncLaunch(extraArgs)) {
    ADD_FAILURE() << "Subprocess launch failed";
    return nullptr;
  }

  if (!parent->mSubprocess->TakeInitialEndpoint().Bind(parent.get())) {
    ADD_FAILURE() << "Opening the parent actor failed";
    return nullptr;
  }

  EXPECT_TRUE(parent->CanSend());
  return parent.forget();
}

already_AddRefed<IPDLUnitTestParent> IPDLUnitTestParent::CreateCrossThread() {
  RefPtr<IPDLUnitTestParent> parent = new IPDLUnitTestParent();
  RefPtr<IPDLUnitTestChild> child = new IPDLUnitTestChild();

  nsresult rv =
      NS_NewNamedThread("IPDL UnitTest", getter_AddRefs(parent->mOtherThread));
  if (NS_FAILED(rv)) {
    ADD_FAILURE() << "Failed to create IPDLUnitTest thread";
    return nullptr;
  }
  if (!parent->Open(child, parent->mOtherThread)) {
    ADD_FAILURE() << "Opening the actor failed";
    return nullptr;
  }

  EXPECT_TRUE(parent->CanSend());
  return parent.forget();
}

IPDLUnitTestParent::~IPDLUnitTestParent() {
  if (mSubprocess) {
    mSubprocess->Destroy();
    mSubprocess = nullptr;
  }
  if (mOtherThread) {
    mOtherThread->Shutdown();
  }
}

bool IPDLUnitTestParent::Start(const char* aName,
                               ipc::IToplevelProtocol* aActor) {
  nsID channelId = nsID::GenerateUUID();
  auto [parentPort, childPort] =
      ipc::NodeController::GetSingleton()->CreatePortPair();
  if (!SendStart(nsDependentCString(aName), std::move(childPort), channelId)) {
    ADD_FAILURE() << "IPDLUnitTestParent::SendStart failed";
    return false;
  }
  if (!aActor->Open(std::move(parentPort), channelId, OtherPid())) {
    ADD_FAILURE() << "Unable to open parent actor";
    return false;
  }
  return true;
}

ipc::IPCResult IPDLUnitTestParent::RecvReport(const TestPartResult& aReport) {
  if (!aReport.failed()) {
    return IPC_OK();
  }

  // Report the failure
  ADD_FAILURE_AT(aReport.filename().get(), aReport.lineNumber())
      << "[child " << OtherPid() << "] " << aReport.summary();

  // If the failure was fatal, kill the child process to avoid hangs.
  if (aReport.fatal()) {
    KillHard();
  }
  return IPC_OK();
}

ipc::IPCResult IPDLUnitTestParent::RecvComplete() {
  mComplete = true;
  Close();
  return IPC_OK();
}

void IPDLUnitTestParent::KillHard() {
  if (mCalledKillHard) {
    return;
  }
  mCalledKillHard = true;

  // We can't effectively kill a same-process situation, but we can trigger
  // shutdown early to avoid hanging.
  if (mOtherThread) {
    Close();
    nsCOMPtr<nsIThread> otherThread = mOtherThread.forget();
    otherThread->Shutdown();
  }

  if (mSubprocess) {
    ProcessHandle handle = mSubprocess->GetChildProcessHandle();
    if (!base::KillProcess(handle, base::PROCESS_END_KILLED_BY_USER)) {
      NS_WARNING("failed to kill subprocess!");
    }
    mSubprocess->SetAlreadyDead();
  }
}

ipc::IPCResult IPDLUnitTestChild::RecvStart(const nsCString& aName,
                                            ipc::ScopedPort aPort,
                                            const nsID& aMessageChannelId) {
  auto* allocChildActor =
      sAllocChildActorRegistry[std::string_view{aName.get()}];
  if (!allocChildActor) {
    ADD_FAILURE() << "No AllocChildActor for name " << aName.get()
                  << " registered!";
    return IPC_FAIL(this, "No AllocChildActor registered!");
  }

  // Store references to the node & port to watch for test completion.
  RefPtr<ipc::NodeController> controller = aPort.Controller();
  mojo::core::ports::PortRef port = aPort.Port();

  auto* child = allocChildActor();
  if (!child->Open(std::move(aPort), aMessageChannelId, OtherPid())) {
    ADD_FAILURE() << "Unable to open child actor";
    return IPC_FAIL(this, "Unable to open child actor");
  }

  // Wait for the port which was created for this actor to be fully torn down.
  SpinEventLoopUntil("IPDLUnitTestChild::RecvStart"_ns,
                     [&] { return controller->GetStatus(port).isNothing(); });

  // Tear down the test actor to end the test.
  SendComplete();
  return IPC_OK();
}

void IPDLUnitTestChild::ActorDestroy(ActorDestroyReason aWhy) {
  if (!XRE_IsParentProcess()) {
    XRE_ShutdownChildProcess();
  }
}

void IPDLTestHelper::TestWrapper(bool aCrossProcess) {
  // Create the host and start the test actor with it.
  RefPtr<IPDLUnitTestParent> host =
      aCrossProcess ? IPDLUnitTestParent::CreateCrossProcess()
                    : IPDLUnitTestParent::CreateCrossThread();
  ASSERT_TRUE(host);
  if (!host->Start(GetName(), GetActor())) {
    FAIL();
  }

  // XXX: Consider adding a test timeout?

  // Run the test body. This will send the initial messages to our actor, which
  // will eventually clean itself up.
  TestBody();

  // Spin the event loop until the test wrapper host has fully shut down.
  SpinEventLoopUntil("IPDLTestHelper::TestWrapper"_ns,
                     [&] { return !host->CanSend(); });

  EXPECT_TRUE(host->ReportedComplete())
      << "child process exited without signalling completion";
}

// Listener registered within the IPDLUnitTest process used to relay GTest
// failures to the parent process, so that the are marked as failing the overall
// gtest.
class IPDLChildProcessTestListener : public testing::EmptyTestEventListener {
 public:
  explicit IPDLChildProcessTestListener(IPDLUnitTestChild* aActor)
      : mActor(aActor) {}

  virtual void OnTestPartResult(
      const testing::TestPartResult& aTestPartResult) override {
    mActor->SendReport(TestPartResult(
        aTestPartResult.failed(), aTestPartResult.fatally_failed(),
        nsDependentCString(aTestPartResult.file_name()),
        aTestPartResult.line_number(),
        nsDependentCString(aTestPartResult.summary()),
        nsDependentCString(aTestPartResult.message())));
  }

  RefPtr<IPDLUnitTestChild> mActor;
};

// ProcessChild instance used to run the IPDLUnitTest process.
class IPDLUnitTestProcessChild : public ipc::ProcessChild {
 public:
  using ipc::ProcessChild::ProcessChild;
  bool Init(int aArgc, char* aArgv[]) override {
    nsDebugImpl::SetMultiprocessMode("IPDLUnitTest");

    if (!ProcessChild::InitPrefs(aArgc, aArgv)) {
      MOZ_CRASH("InitPrefs failed");
      return false;
    }

    if (NS_WARN_IF(NS_FAILED(nsThreadManager::get().Init()))) {
      MOZ_CRASH("nsThreadManager initialization failed");
      return false;
    }

    RefPtr<IPDLUnitTestChild> child = new IPDLUnitTestChild();
    if (!TakeInitialEndpoint().Bind(child.get())) {
      MOZ_CRASH("Bind of IPDLUnitTestChild failed");
      return false;
    }

    // Register a listener to forward test results from the child process to the
    // parent process to be handled there.
    mListener = new IPDLChildProcessTestListener(child);
    testing::UnitTest::GetInstance()->listeners().Append(mListener);

    if (NS_FAILED(NS_InitMinimalXPCOM())) {
      MOZ_CRASH("NS_InitMinimalXPCOM failed");
      return false;
    }

    ipc::SetThisProcessName("IPDLUnitTest");
    return true;
  }

  void CleanUp() override {
    // Clean up the test listener we registered to get a clean shutdown.
    if (mListener) {
      testing::UnitTest::GetInstance()->listeners().Release(mListener);
      delete mListener;
    }

    NS_ShutdownXPCOM(nullptr);
  }

  IPDLChildProcessTestListener* mListener = nullptr;
};

// Defined in nsEmbedFunctions.cpp
extern UniquePtr<ipc::ProcessChild> (*gMakeIPDLUnitTestProcessChild)(
    base::ProcessId, const nsID&);

// Initialize gMakeIPDLUnitTestProcessChild in a static constructor.
int _childProcessEntryPointStaticConstructor = ([] {
  gMakeIPDLUnitTestProcessChild =
      [](base::ProcessId aParentPid,
         const nsID& aMessageChannelId) -> UniquePtr<ipc::ProcessChild> {
    return MakeUnique<IPDLUnitTestProcessChild>(aParentPid, aMessageChannelId);
  };
  return 0;
})();

}  // namespace mozilla::_ipdltest
