/* vim:set ts=2 sw=2 sts=2 et: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

#include "gtest/gtest.h"

#include "js/Conversions.h"
#include "mozilla/Unused.h"
#include "nsJSUtils.h" // nsAutoJSString
#include "nsITelemetry.h"
#include "nsThreadUtils.h"
#include "Telemetry.h"
#include "TelemetryFixture.h"

using namespace mozilla;

#define EXPECTED_STRING "Nice, expected and creative string."

namespace {

void
CheckUintScalar(const char* aName, JSContext* aCx, JS::HandleValue aSnapshot, uint32_t expectedValue)
{
  // Validate the value of the test scalar.
  JS::RootedValue value(aCx);
  JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
  ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value)) << "The test scalar must be reported.";
  ASSERT_TRUE(value.isInt32()) << "The scalar value must be of the correct type.";
  ASSERT_TRUE(value.toInt32() >= 0) << "The uint scalar type must contain a value >= 0.";
  ASSERT_EQ(static_cast<uint32_t>(value.toInt32()), expectedValue) << "The scalar value must match the expected value.";
}

void
CheckBoolScalar(const char* aName, JSContext* aCx, JS::HandleValue aSnapshot, bool expectedValue)
{
  // Validate the value of the test scalar.
  JS::RootedValue value(aCx);
  JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
  ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value)) << "The test scalar must be reported.";
  ASSERT_TRUE(value.isBoolean()) << "The scalar value must be of the correct type.";
  ASSERT_EQ(static_cast<bool>(value.toBoolean()), expectedValue) << "The scalar value must match the expected value.";
}

void
CheckStringScalar(const char* aName, JSContext* aCx, JS::HandleValue aSnapshot, const char* expectedValue)
{
  // Validate the value of the test scalar.
  JS::RootedValue value(aCx);
  JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
  ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value)) << "The test scalar must be reported.";
  ASSERT_TRUE(value.isString()) << "The scalar value must be of the correct type.";

  bool sameString;
  ASSERT_TRUE(JS_StringEqualsAscii(aCx, value.toString(), expectedValue, &sameString)) << "JS String comparison failed";
  ASSERT_TRUE(sameString) << "The scalar value must match the expected string";
}

void
CheckKeyedUintScalar(const char* aName, const char* aKey, JSContext* aCx, JS::HandleValue aSnapshot,
                     uint32_t expectedValue)
{
  JS::RootedValue keyedScalar(aCx);
  JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
  // Get the aName keyed scalar object from the scalars snapshot.
  ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
    << "The keyed scalar must be reported.";

  CheckUintScalar(aKey, aCx, keyedScalar, expectedValue);
}

void
CheckKeyedBoolScalar(const char* aName, const char* aKey, JSContext* aCx, JS::HandleValue aSnapshot,
                     bool expectedValue)
{
  JS::RootedValue keyedScalar(aCx);
  JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
  // Get the aName keyed scalar object from the scalars snapshot.
  ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
    << "The keyed scalar must be reported.";

  CheckBoolScalar(aKey, aCx, keyedScalar, expectedValue);
}

void
CheckNumberOfProperties(const char* aName, JSContext* aCx, JS::HandleValue aSnapshot,
                        uint32_t expectedNumProperties)
{
  JS::RootedValue keyedScalar(aCx);
  JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
  // Get the aName keyed scalar object from the scalars snapshot.
  ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
    << "The keyed scalar must be reported.";

  JS::RootedObject keyedScalarObj(aCx, &keyedScalar.toObject());
  JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
  ASSERT_TRUE(JS_Enumerate(aCx, keyedScalarObj, &ids))
    << "We must be able to get keyed scalar members.";

  ASSERT_EQ(expectedNumProperties, ids.length())
    << "The scalar must report the expected number of properties.";
}

void
GetScalarsSnapshot(bool aKeyed, JSContext* aCx, JS::MutableHandle<JS::Value> aResult)
{
  nsCOMPtr<nsITelemetry> telemetry = do_GetService("@mozilla.org/base/telemetry;1");

  // Get a snapshot of the scalars.
  JS::RootedValue scalarsSnapshot(aCx);
  nsresult rv;

  if (aKeyed) {
    rv = telemetry->SnapshotKeyedScalars(nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN,
                                         false, aCx, 0, &scalarsSnapshot);
  } else {
    rv = telemetry->SnapshotScalars(nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN,
                                    false, aCx, 0, &scalarsSnapshot);
  }

  // Validate the snapshot.
  ASSERT_EQ(rv, NS_OK) << "Creating a snapshot of the data must not fail.";
  ASSERT_TRUE(scalarsSnapshot.isObject()) << "The snapshot must be an object.";

  // We currently only support scalars from the parent process in the gtests.
  JS::RootedValue parentScalars(aCx);
  JS::RootedObject scalarObj(aCx, &scalarsSnapshot.toObject());
  // Don't complain if no scalars for the parent process can be found. Just
  // return an empty object.
  Unused << JS_GetProperty(aCx, scalarObj, "parent", &parentScalars);

  aResult.set(parentScalars);
}

} // Anonymous namespace.

// Test that we can properly write unsigned scalars using the C++ API.
TEST_F(TelemetryTestFixture, ScalarUnsigned) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  // Make sure we don't get scalars from other tests.
  Unused << mTelemetry->ClearScalars();

  // Set the test scalar to a known value.
  const uint32_t kInitialValue = 1172015;
  const uint32_t kExpectedUint = 1172017;
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, kInitialValue);
  Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, kExpectedUint - kInitialValue);

  // Check the recorded value.
  JS::RootedValue scalarsSnapshot(cx.GetJSContext());
  GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
  CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(), scalarsSnapshot, kExpectedUint);

  // Try to use SetMaximum.
  const uint32_t kExpectedUintMaximum = kExpectedUint * 2;
  Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, kExpectedUintMaximum);

  // Make sure that calls of the unsupported type don't corrupt the stored value.
  // Don't run this part in debug builds as that intentionally asserts.
  #ifndef DEBUG
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, false);
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, NS_LITERAL_STRING("test"));
  #endif

  // Check the recorded value.
  GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
  CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(), scalarsSnapshot, kExpectedUintMaximum);
}

// Test that we can properly write boolean scalars using the C++ API.
TEST_F(TelemetryTestFixture, ScalarBoolean) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  Unused << mTelemetry->ClearScalars();

  // Set the test scalar to a known value.
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, true);

  // Make sure that calls of the unsupported type don't corrupt the stored value.
  // Don't run this part in debug builds as that intentionally asserts.
  #ifndef DEBUG
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, static_cast<uint32_t>(12));
    Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, 20);
    Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, 2);
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, NS_LITERAL_STRING("test"));
  #endif

  // Check the recorded value.
  JS::RootedValue scalarsSnapshot(cx.GetJSContext());
  GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
  CheckBoolScalar("telemetry.test.boolean_kind", cx.GetJSContext(), scalarsSnapshot, true);
}

// Test that we can properly write string scalars using the C++ API.
TEST_F(TelemetryTestFixture, ScalarString) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  Unused << mTelemetry->ClearScalars();

  // Set the test scalar to a known value.
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, NS_LITERAL_STRING(EXPECTED_STRING));

  // Make sure that calls of the unsupported type don't corrupt the stored value.
  // Don't run this part in debug builds as that intentionally asserts.
  #ifndef DEBUG
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, static_cast<uint32_t>(12));
    Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, 20);
    Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, 2);
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, true);
  #endif

  // Check the recorded value.
  JS::RootedValue scalarsSnapshot(cx.GetJSContext());
  GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
  CheckStringScalar("telemetry.test.string_kind", cx.GetJSContext(), scalarsSnapshot, EXPECTED_STRING);
}

// Test that we can properly write keyed unsigned scalars using the C++ API.
TEST_F(TelemetryTestFixture, KeyedScalarUnsigned) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  Unused << mTelemetry->ClearScalars();

  // Set the test scalar to a known value.
  const char* kScalarName = "telemetry.test.keyed_unsigned_int";
  const uint32_t kKey1Value = 1172015;
  const uint32_t kKey2Value = 1172017;
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
                       NS_LITERAL_STRING("key1"), kKey1Value);
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
                       NS_LITERAL_STRING("key2"), kKey1Value);
  Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
                       NS_LITERAL_STRING("key2"), 2);

  // Make sure that calls of the unsupported type don't corrupt the stored value.
  // Don't run this part in debug builds as that intentionally asserts.
  #ifndef DEBUG
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
                         NS_LITERAL_STRING("key1"), false);
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT, NS_LITERAL_STRING("test"));
  #endif

  // Check the recorded value.
  JS::RootedValue scalarsSnapshot(cx.GetJSContext());
  GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);

  // Check the keyed scalar we're interested in.
  CheckKeyedUintScalar(kScalarName, "key1", cx.GetJSContext(), scalarsSnapshot, kKey1Value);
  CheckKeyedUintScalar(kScalarName, "key2", cx.GetJSContext(), scalarsSnapshot, kKey2Value);
  CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 2);

  // Try to use SetMaximum.
  const uint32_t kExpectedUintMaximum = kKey1Value * 2;
  Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
                              NS_LITERAL_STRING("key1"), kExpectedUintMaximum);

  GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
  // The first key should be different and te second is expected to be the same.
  CheckKeyedUintScalar(kScalarName, "key1", cx.GetJSContext(), scalarsSnapshot, kExpectedUintMaximum);
  CheckKeyedUintScalar(kScalarName, "key2", cx.GetJSContext(), scalarsSnapshot, kKey2Value);
  CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 2);
}

TEST_F(TelemetryTestFixture, KeyedScalarBoolean) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  Unused << mTelemetry->ClearScalars();

  // Set the test scalar to a known value.
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
                       NS_LITERAL_STRING("key1"), false);
  Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
                       NS_LITERAL_STRING("key2"), true);

  // Make sure that calls of the unsupported type don't corrupt the stored value.
  // Don't run this part in debug builds as that intentionally asserts.
  #ifndef DEBUG
    Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
                         NS_LITERAL_STRING("key1"), static_cast<uint32_t>(12));
    Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
                                NS_LITERAL_STRING("key1"), 20);
    Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
                         NS_LITERAL_STRING("key1"), 2);
  #endif

  // Check the recorded value.
  JS::RootedValue scalarsSnapshot(cx.GetJSContext());
  GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);

  // Make sure that the keys contain the expected values.
  const char* kScalarName = "telemetry.test.keyed_boolean_kind";
  CheckKeyedBoolScalar(kScalarName, "key1", cx.GetJSContext(), scalarsSnapshot, false);
  CheckKeyedBoolScalar(kScalarName, "key2", cx.GetJSContext(), scalarsSnapshot, true);
  CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 2);
}

TEST_F(TelemetryTestFixture, NonMainThreadAdd) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  Unused << mTelemetry->ClearScalars();

  // Define the function that will be called on the testing thread.
  nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
    "TelemetryTestFixture_NonMainThreadAdd_Test::TestBody", []() -> void {
      Telemetry::ScalarAdd(
        Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, 37);
    });

  // Spawn the testing thread and run the function.
  nsCOMPtr<nsIThread> testingThread;
  nsresult rv =
    NS_NewNamedThread("Test thread", getter_AddRefs(testingThread), runnable);
  ASSERT_EQ(rv, NS_OK);

  // Shutdown the thread. This also waits for the runnable to complete.
  testingThread->Shutdown();

  // Check the recorded value.
  JS::RootedValue scalarsSnapshot(cx.GetJSContext());
  GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
  CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(), scalarsSnapshot, 37);
}

TEST_F(TelemetryTestFixture, ScalarUnknownID) {
  AutoJSContextWithGlobal cx(mCleanGlobal);

  // Make sure we don't get scalars from other tests.
  Unused << mTelemetry->ClearScalars();

// Don't run this part in debug builds as that intentionally asserts.
#ifndef DEBUG
  const uint32_t kTestFakeIds[] = {
    static_cast<uint32_t>(Telemetry::ScalarID::ScalarCount),
    static_cast<uint32_t>(Telemetry::ScalarID::ScalarCount) + 378537,
    std::numeric_limits<uint32_t>::max()
  };

  for (auto id : kTestFakeIds) {
    Telemetry::ScalarID scalarId = static_cast<Telemetry::ScalarID>(id);
    Telemetry::ScalarSet(scalarId, static_cast<uint32_t>(1));
    Telemetry::ScalarSet(scalarId, true);
    Telemetry::ScalarSet(scalarId, NS_LITERAL_STRING("test"));
    Telemetry::ScalarAdd(scalarId, 1);
    Telemetry::ScalarSetMaximum(scalarId, 1);

    // Make sure that nothing was recorded in the plain scalars.
    JS::RootedValue scalarsSnapshot(cx.GetJSContext());
    GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
    ASSERT_TRUE(scalarsSnapshot.isUndefined()) << "No scalar must be recorded";

    // Same for the keyed scalars.
    Telemetry::ScalarSet(scalarId, NS_LITERAL_STRING("key1"), static_cast<uint32_t>(1));
    Telemetry::ScalarSet(scalarId, NS_LITERAL_STRING("key1"), true);
    Telemetry::ScalarAdd(scalarId, NS_LITERAL_STRING("key1"), 1);
    Telemetry::ScalarSetMaximum(scalarId, NS_LITERAL_STRING("key1"), 1);

    // Make sure that nothing was recorded in the keyed scalars.
    JS::RootedValue keyedSnapshot(cx.GetJSContext());
    GetScalarsSnapshot(true, cx.GetJSContext(), &keyedSnapshot);
    ASSERT_TRUE(keyedSnapshot.isUndefined()) << "No keyed scalar must be recorded";
  }
#endif
}
