/* 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/BasePrincipal.h"
#include "mozilla/ExpandedPrincipal.h"
#include "mozilla/ExtensionPolicyService.h"
#include "mozilla/GenericFactory.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/SystemPrincipal.h"
#include "mozilla/dom/ProcessIsolation.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/gtest/MozAssertions.h"
#include "mozilla/gtest/MozHelpers.h"
#include "nsComponentManager.h"
#include "nsIEnterprisePolicies.h"

using namespace mozilla;
using namespace mozilla::dom;

static nsCOMPtr<nsIPrincipal> MakeTestPrincipal(const char* aURI) {
  nsCOMPtr<nsIURI> uri;
  MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aURI));
  return BasePrincipal::CreateContentPrincipal(uri, {});
}

namespace {

static bool gJitDisabled = false;

struct RemoteTypes {
  nsCString mIsolated;
  nsCString mUnisolated;
};

struct WorkerExpectation {
  nsCOMPtr<nsIPrincipal> mPrincipal;
  WorkerKind mWorkerKind = WorkerKindShared;
  bool mJitDisabled = false;
  Result<RemoteTypes, nsresult> mExpected = Err(NS_ERROR_FAILURE);
  nsCString mCurrentRemoteType = "fakeRemoteType"_ns;

  void Check(bool aUseRemoteSubframes) {
    nsAutoCString origin;
    ASSERT_NS_SUCCEEDED(mPrincipal->GetOrigin(origin));

    nsPrintfCString describe(
        "origin: %s, workerKind: %s, currentRemoteType: %s, "
        "useRemoteSubframes: %d",
        origin.get(), mWorkerKind == WorkerKindShared ? "shared" : "service",
        mCurrentRemoteType.get(), aUseRemoteSubframes);

    gJitDisabled = mJitDisabled;
    auto result = IsolationOptionsForWorker(
        mPrincipal, mWorkerKind, mCurrentRemoteType, aUseRemoteSubframes);
    ASSERT_EQ(result.isOk(), mExpected.isOk())
        << "Unexpected status (expected " << (mExpected.isOk() ? "ok" : "err")
        << ") for " << describe;
    if (mExpected.isOk()) {
      const nsCString& expected = aUseRemoteSubframes
                                      ? mExpected.inspect().mIsolated
                                      : mExpected.inspect().mUnisolated;
      ASSERT_EQ(result.inspect().mRemoteType, expected)
          << "Unexpected remote type (expected " << expected << ") for "
          << describe;
    }
  }
};

#define MOCK_ENTERPRISE_POLICIES_CID \
  {0xaabbc001, 0xdd00, 0x1234, {0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89}}
NS_DEFINE_NAMED_CID(MOCK_ENTERPRISE_POLICIES_CID);

class MockEnterprisePoliciesService final : public nsIEnterprisePolicies {
  ~MockEnterprisePoliciesService() = default;

 public:
  NS_DECL_ISUPPORTS

  NS_IMETHOD GetStatus(int16_t* aStatus) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD GetIsEnterprise(bool* aIsEnterprise) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD IsAllowed(const nsACString&, bool* aRetVal) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD IsAllowedForURI(const nsACString&, nsIURI*,
                             bool* aRetVal) override {
    *aRetVal = !gJitDisabled;
    return NS_OK;
  }
  NS_IMETHOD GetActivePolicies(JS::MutableHandle<JS::Value>) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD GetSupportMenu(JS::MutableHandle<JS::Value>) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD GetExtensionPolicy(const nsACString&,
                                JS::MutableHandle<JS::Value>) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD GetExtensionSettings(const nsACString&,
                                  JS::MutableHandle<JS::Value>) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD MayInstallAddon(JS::Handle<JS::Value>, bool*) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD IsAddonRequiredByPolicy(const nsACString&, bool*) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD AllowedInstallSource(nsIURI*, bool*) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
  NS_IMETHOD IsExemptExecutableExtension(const nsACString&, const nsACString&,
                                         bool*) override {
    return NS_ERROR_NOT_IMPLEMENTED;
  }
};

NS_IMPL_ISUPPORTS(MockEnterprisePoliciesService, nsIEnterprisePolicies)

static nsresult ConstructMockEnterprisePolicies(const nsIID& aIID,
                                                void** aResult) {
  RefPtr<MockEnterprisePoliciesService> service =
      new MockEnterprisePoliciesService();
  return service->QueryInterface(aIID, aResult);
}

StaticRefPtr<nsIFactory> gMockPolicyFactory;

static void RegisterMockPolicyService() {
  MOZ_ASSERT(!gMockPolicyFactory);
  nsCOMPtr<nsIFactory> existing;
  if (NS_SUCCEEDED(nsComponentManagerImpl::gComponentManager->GetClassObject(
          kMOCK_ENTERPRISE_POLICIES_CID, NS_GET_IID(nsIFactory),
          getter_AddRefs(existing))) &&
      existing) {
    (void)nsComponentManagerImpl::gComponentManager->UnregisterFactory(
        kMOCK_ENTERPRISE_POLICIES_CID, existing);
  }
  gMockPolicyFactory =
      new mozilla::GenericFactory(ConstructMockEnterprisePolicies);
  MOZ_ALWAYS_SUCCEEDS(
      nsComponentManagerImpl::gComponentManager->RegisterFactory(
          kMOCK_ENTERPRISE_POLICIES_CID, "MockEnterprisePolicies",
          "@mozilla.org/enterprisepolicies;1", gMockPolicyFactory));
}

static void UnregisterMockPolicyService() {
  MOZ_ASSERT(gMockPolicyFactory);
  MOZ_ALWAYS_SUCCEEDS(
      nsComponentManagerImpl::gComponentManager->UnregisterFactory(
          kMOCK_ENTERPRISE_POLICIES_CID, gMockPolicyFactory));
  gMockPolicyFactory = nullptr;
}

}  // namespace

static nsCString WebIsolatedRemoteType(nsIPrincipal* aPrincipal,
                                       bool aJitDisabled = false) {
  nsAutoCString origin;
  MOZ_ALWAYS_SUCCEEDS(aPrincipal->GetSiteOrigin(origin));
  if (aJitDisabled) {
    return FISSION_WEB_REMOTE_TYPE + "="_ns + origin + "^disableJit=1"_ns;
  }
  return FISSION_WEB_REMOTE_TYPE + "="_ns + origin;
}

static nsCString CoopCoepRemoteType(nsIPrincipal* aPrincipal) {
  nsAutoCString origin;
  MOZ_ALWAYS_SUCCEEDS(aPrincipal->GetSiteOrigin(origin));
  return WITH_COOP_COEP_REMOTE_TYPE + "="_ns + origin;
}

static nsCString ServiceWorkerIsolatedRemoteType(nsIPrincipal* aPrincipal,
                                                 bool aJitDisabled = false) {
  nsAutoCString origin;
  MOZ_ALWAYS_SUCCEEDS(aPrincipal->GetSiteOrigin(origin));
  if (aJitDisabled) {
    return SERVICEWORKER_REMOTE_TYPE + "="_ns + origin + "^disableJit=1"_ns;
  }
  return SERVICEWORKER_REMOTE_TYPE + "="_ns + origin;
}

TEST(ProcessIsolationTest, WorkerOptions)
{
  // Forcibly enable the privileged mozilla content process for the duration of
  // the test.
  MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(
      "browser.tabs.remote.separatedMozillaDomains", "addons.mozilla.org"));
  MOZ_ALWAYS_SUCCEEDS(Preferences::SetBool(
      "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true));
  auto cleanup = MakeScopeExit([&] {
    MOZ_ALWAYS_SUCCEEDS(
        Preferences::ClearUser("browser.tabs.remote.separatedMozillaDomains"));
    MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(
        "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess"));
  });

  OriginAttributes containerOA;
  containerOA.mUserContextId = 1;

  nsCOMPtr<nsIPrincipal> systemPrincipal = SystemPrincipal::Get();
  nsCOMPtr<nsIPrincipal> nullPrincipal =
      NullPrincipal::CreateWithoutOriginAttributes();
  nsCOMPtr<nsIPrincipal> nullContainerPrincipal =
      NullPrincipal::Create(containerOA);
  nsCOMPtr<nsIPrincipal> secureComPrincipal =
      MakeTestPrincipal("https://example.com");
  nsCOMPtr<nsIPrincipal> secureOrgPrincipal =
      MakeTestPrincipal("https://example.org");
  nsCOMPtr<nsIPrincipal> insecureOrgPrincipal =
      MakeTestPrincipal("http://example.org");
  nsCOMPtr<nsIPrincipal> filePrincipal =
      MakeTestPrincipal("file:///path/to/dir");
  nsCOMPtr<nsIPrincipal> extensionPrincipal =
      MakeTestPrincipal("moz-extension://fake-uuid");
  nsCOMPtr<nsIPrincipal> privilegedMozillaPrincipal =
      MakeTestPrincipal("https://addons.mozilla.org");
  nsCOMPtr<nsIPrincipal> expandedPrincipal = ExpandedPrincipal::Create(
      nsTArray{secureComPrincipal, extensionPrincipal}, {});
  nsCOMPtr<nsIPrincipal> nullSecureComPrecursorPrincipal =
      NullPrincipal::CreateWithInheritedAttributes(secureComPrincipal);

  nsCString extensionRemoteType =
      ExtensionPolicyService::GetSingleton().UseRemoteExtensions()
          ? EXTENSION_REMOTE_TYPE
          : NOT_REMOTE_TYPE;
  nsCString fileRemoteType =
      StaticPrefs::browser_tabs_remote_separateFileUriProcess()
          ? FILE_REMOTE_TYPE
          : WEB_REMOTE_TYPE;

  WorkerExpectation expectations[] = {
      // Neither service not shared workers can have expanded principals
      {.mPrincipal = expandedPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},
      {.mPrincipal = expandedPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},

      // Service workers cannot have system or null principals
      {.mPrincipal = systemPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},
      {.mPrincipal = nullPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},
      {.mPrincipal = nullContainerPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},
      {.mPrincipal = nullSecureComPrecursorPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},

      // Service workers with various content principals
      {.mPrincipal = secureComPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected =
           RemoteTypes{ServiceWorkerIsolatedRemoteType(secureComPrincipal),
                       WEB_REMOTE_TYPE}},
      {.mPrincipal = secureOrgPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected =
           RemoteTypes{ServiceWorkerIsolatedRemoteType(secureOrgPrincipal),
                       WEB_REMOTE_TYPE}},
      {.mPrincipal = extensionPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = RemoteTypes{extensionRemoteType, extensionRemoteType}},
      {.mPrincipal = privilegedMozillaPrincipal,
       .mWorkerKind = WorkerKindService,
       .mExpected = RemoteTypes{PRIVILEGEDMOZILLA_REMOTE_TYPE,
                                PRIVILEGEDMOZILLA_REMOTE_TYPE}},

      // Shared Worker loaded from within a webCOOP+COEP remote type process,
      // should load elsewhere.
      {.mPrincipal = secureComPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WebIsolatedRemoteType(secureComPrincipal),
                                WEB_REMOTE_TYPE},
       .mCurrentRemoteType = CoopCoepRemoteType(secureComPrincipal)},

      // Even precursorless null principal should load elsewhere.
      {.mPrincipal = nullPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WEB_REMOTE_TYPE, WEB_REMOTE_TYPE},
       .mCurrentRemoteType = CoopCoepRemoteType(secureComPrincipal)},
      {.mPrincipal = nullContainerPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WEB_REMOTE_TYPE "=^userContextId=1"_ns,
                                WEB_REMOTE_TYPE "=^userContextId=1"_ns},
       .mCurrentRemoteType = CoopCoepRemoteType(secureComPrincipal)},

      // System principal shared workers can only load in the parent process.
      {.mPrincipal = systemPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{NOT_REMOTE_TYPE, NOT_REMOTE_TYPE},
       .mCurrentRemoteType = NOT_REMOTE_TYPE},
      {.mPrincipal = systemPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = Err(NS_ERROR_UNEXPECTED),
       .mCurrentRemoteType = PRIVILEGEDABOUT_REMOTE_TYPE},
      {.mPrincipal = systemPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},
      {.mPrincipal = systemPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = Err(NS_ERROR_UNEXPECTED)},

      // Content principals should load in the appropriate remote types,
      // ignoring the current remote type.
      {.mPrincipal = secureComPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WebIsolatedRemoteType(secureComPrincipal),
                                WEB_REMOTE_TYPE}},
      {.mPrincipal = secureOrgPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WebIsolatedRemoteType(secureOrgPrincipal),
                                WEB_REMOTE_TYPE}},
      {.mPrincipal = insecureOrgPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WebIsolatedRemoteType(insecureOrgPrincipal),
                                WEB_REMOTE_TYPE}},
      {.mPrincipal = filePrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{fileRemoteType, fileRemoteType}},
      {.mPrincipal = extensionPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{extensionRemoteType, extensionRemoteType}},
      {.mPrincipal = privilegedMozillaPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{PRIVILEGEDMOZILLA_REMOTE_TYPE,
                                PRIVILEGEDMOZILLA_REMOTE_TYPE}},
      {.mPrincipal = nullSecureComPrecursorPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mExpected = RemoteTypes{WebIsolatedRemoteType(secureComPrincipal),
                                WEB_REMOTE_TYPE}},

      // When the policy service calls for the JIT to be disabled the remote
      // type should reflect that.
      {.mPrincipal = secureComPrincipal,
       .mWorkerKind = WorkerKindShared,
       .mJitDisabled = true,
       .mExpected = RemoteTypes{WebIsolatedRemoteType(secureComPrincipal, true),
                                SharedWebRemoteType(OriginAttributes{}, true)}},
      {.mPrincipal = secureComPrincipal,
       .mWorkerKind = WorkerKindService,
       .mJitDisabled = true,
       .mExpected = RemoteTypes{ServiceWorkerIsolatedRemoteType(
                                    secureComPrincipal, true),
                                SharedWebRemoteType(OriginAttributes{}, true)}},
  };

  RegisterMockPolicyService();
  for (auto& expectation : expectations) {
    expectation.Check(true);
    expectation.Check(false);
  }
  UnregisterMockPolicyService();
}
