/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* 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/. */

/*
 * Test harness for XPCOM objects, providing a scoped XPCOM initializer,
 * nsCOMPtr, nsRefPtr, do_CreateInstance, do_GetService, ns(Auto|C|)String,
 * and stdio.h/stdlib.h.
 */

#ifndef LibFuzzerTestHarness_h__
#define LibFuzzerTestHarness_h__

#include "mozilla/ArrayUtils.h"
#include "mozilla/Attributes.h"

#include "prenv.h"
#include "nsComponentManagerUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsCOMPtr.h"
#include "nsAutoPtr.h"
#include "nsStringGlue.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIDirectoryService.h"
#include "nsIFile.h"
#include "nsIProperties.h"
#include "nsIObserverService.h"
#include "nsXULAppAPI.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

namespace {

static uint32_t gFailCount = 0;

/**
 * Prints the given failure message and arguments using printf, prepending
 * "TEST-UNEXPECTED-FAIL " for the benefit of the test harness and
 * appending "\n" to eliminate having to type it at each call site.
 */
MOZ_FORMAT_PRINTF(1, 2) void fail(const char* msg, ...)
{
  va_list ap;

  printf("TEST-UNEXPECTED-FAIL | ");

  va_start(ap, msg);
  vprintf(msg, ap);
  va_end(ap);

  putchar('\n');
  ++gFailCount;
}

//-----------------------------------------------------------------------------

class ScopedXPCOM : public nsIDirectoryServiceProvider2
{
  public:
    NS_DECL_ISUPPORTS

    explicit ScopedXPCOM(const char* testName,
                         nsIDirectoryServiceProvider *dirSvcProvider = nullptr)
    : mDirSvcProvider(dirSvcProvider)
    {
      mTestName = testName;
      printf("Running %s tests...\n", mTestName);

      nsresult rv = NS_InitXPCOM2(&mServMgr, nullptr, this);
      if (NS_FAILED(rv))
      {
        fail("NS_InitXPCOM2 returned failure code 0x%x", rv);
        mServMgr = nullptr;
        return;
      }
    }

    ~ScopedXPCOM()
    {
      // If we created a profile directory, we need to remove it.
      if (mProfD) {
        nsCOMPtr<nsIObserverService> os =
          do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
        MOZ_ASSERT(os);
        if (os) {
          MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-change-net-teardown", nullptr));
          MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-change-teardown", nullptr));
          MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-before-change", nullptr));
          MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-before-change-qm", nullptr));
          MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr, "profile-before-change-telemetry", nullptr));
        }

        if (NS_FAILED(mProfD->Remove(true))) {
          NS_WARNING("Problem removing profile directory");
        }

        mProfD = nullptr;
      }

      if (mServMgr)
      {
        NS_RELEASE(mServMgr);
        nsresult rv = NS_ShutdownXPCOM(nullptr);
        if (NS_FAILED(rv))
        {
          fail("XPCOM shutdown failed with code 0x%x", rv);
          exit(1);
        }
      }

      printf("Finished running %s tests.\n", mTestName);
    }

    already_AddRefed<nsIFile> GetProfileDirectory()
    {
      if (mProfD) {
        nsCOMPtr<nsIFile> copy = mProfD;
        return copy.forget();
      }

      // Create a unique temporary folder to use for this test.
      // Note that runcppunittests.py will run tests with a temp
      // directory as the cwd, so just put something under that.
      nsCOMPtr<nsIFile> profD;
      nsresult rv = NS_GetSpecialDirectory(NS_OS_CURRENT_PROCESS_DIR,
                                           getter_AddRefs(profD));
      NS_ENSURE_SUCCESS(rv, nullptr);

      rv = profD->Append(NS_LITERAL_STRING("cpp-unit-profd"));
      NS_ENSURE_SUCCESS(rv, nullptr);

      rv = profD->CreateUnique(nsIFile::DIRECTORY_TYPE, 0755);
      NS_ENSURE_SUCCESS(rv, nullptr);

      mProfD = profD;
      return profD.forget();
    }

    already_AddRefed<nsIFile> GetGREDirectory()
    {
      if (mGRED) {
        nsCOMPtr<nsIFile> copy = mGRED;
        return copy.forget();
      }

      char* env = PR_GetEnv("MOZ_XRE_DIR");
      nsCOMPtr<nsIFile> greD;
      if (env) {
        NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false,
                        getter_AddRefs(greD));
      }

      mGRED = greD;
      return greD.forget();
    }

    already_AddRefed<nsIFile> GetGREBinDirectory()
    {
      if (mGREBinD) {
        nsCOMPtr<nsIFile> copy = mGREBinD;
        return copy.forget();
      }

      nsCOMPtr<nsIFile> greD = GetGREDirectory();
      if (!greD) {
        return greD.forget();
      }
      greD->Clone(getter_AddRefs(mGREBinD));

#ifdef XP_MACOSX
      nsAutoCString leafName;
      mGREBinD->GetNativeLeafName(leafName);
      if (leafName.EqualsLiteral("Resources")) {
        mGREBinD->SetNativeLeafName(NS_LITERAL_CSTRING("MacOS"));
      }
#endif

      nsCOMPtr<nsIFile> copy = mGREBinD;
      return copy.forget();
    }

    ////////////////////////////////////////////////////////////////////////////
    //// nsIDirectoryServiceProvider

    NS_IMETHODIMP GetFile(const char *aProperty, bool *_persistent,
                          nsIFile **_result) override
    {
      // If we were supplied a directory service provider, ask it first.
      if (mDirSvcProvider &&
          NS_SUCCEEDED(mDirSvcProvider->GetFile(aProperty, _persistent,
                                                _result))) {
        return NS_OK;
      }

      // Otherwise, the test harness provides some directories automatically.
      if (0 == strcmp(aProperty, NS_APP_USER_PROFILE_50_DIR) ||
          0 == strcmp(aProperty, NS_APP_USER_PROFILE_LOCAL_50_DIR) ||
          0 == strcmp(aProperty, NS_APP_PROFILE_LOCAL_DIR_STARTUP)) {
        nsCOMPtr<nsIFile> profD = GetProfileDirectory();
        NS_ENSURE_TRUE(profD, NS_ERROR_FAILURE);

        nsCOMPtr<nsIFile> clone;
        nsresult rv = profD->Clone(getter_AddRefs(clone));
        NS_ENSURE_SUCCESS(rv, rv);

        *_persistent = true;
        clone.forget(_result);
        return NS_OK;
      } else if (0 == strcmp(aProperty, NS_GRE_DIR)) {
        nsCOMPtr<nsIFile> greD = GetGREDirectory();
        NS_ENSURE_TRUE(greD, NS_ERROR_FAILURE);

        *_persistent = true;
        greD.forget(_result);
        return NS_OK;
      } else if (0 == strcmp(aProperty, NS_GRE_BIN_DIR)) {
        nsCOMPtr<nsIFile> greBinD = GetGREBinDirectory();
        NS_ENSURE_TRUE(greBinD, NS_ERROR_FAILURE);

        *_persistent = true;
        greBinD.forget(_result);
        return NS_OK;
      }

      return NS_ERROR_FAILURE;
    }

    ////////////////////////////////////////////////////////////////////////////
    //// nsIDirectoryServiceProvider2

    NS_IMETHODIMP GetFiles(const char *aProperty, nsISimpleEnumerator **_enum) override
    {
      // If we were supplied a directory service provider, ask it first.
      nsCOMPtr<nsIDirectoryServiceProvider2> provider =
        do_QueryInterface(mDirSvcProvider);
      if (provider && NS_SUCCEEDED(provider->GetFiles(aProperty, _enum))) {
        return NS_OK;
      }

     return NS_ERROR_FAILURE;
   }

  private:
    const char* mTestName;
    nsIServiceManager* mServMgr;
    nsCOMPtr<nsIDirectoryServiceProvider> mDirSvcProvider;
    nsCOMPtr<nsIFile> mProfD;
    nsCOMPtr<nsIFile> mGRED;
    nsCOMPtr<nsIFile> mGREBinD;
};

NS_IMPL_QUERY_INTERFACE(
  ScopedXPCOM,
  nsIDirectoryServiceProvider,
  nsIDirectoryServiceProvider2
)

NS_IMETHODIMP_(MozExternalRefCountType)
ScopedXPCOM::AddRef()
{
  return 2;
}

NS_IMETHODIMP_(MozExternalRefCountType)
ScopedXPCOM::Release()
{
  return 1;
}

} // namespace

#endif  // LibFuzzerTestHarness_h__
