/* 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 "AvailableMemoryWatcher.h"
#include "TelemetryFixture.h"
#include "TelemetryTestHelpers.h"
#include "gtest/gtest.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/glean/XpcomMetrics.h"
#include "mozilla/gtest/MozAssertions.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsITimer.h"
#include "nsMemoryPressure.h"

using namespace mozilla;

namespace {

template <typename ConditionT>
bool WaitUntil(const ConditionT& aCondition, uint32_t aTimeoutMs) {
  bool isTimeout = false;

  // The message queue can be empty and the loop stops
  // waiting for a new event before detecting timeout.
  // Creating a timer to fire a timeout event.
  nsCOMPtr<nsITimer> timer;
  NS_NewTimerWithFuncCallback(
      getter_AddRefs(timer),
      [](nsITimer*, void* isTimeout) {
        *reinterpret_cast<bool*>(isTimeout) = true;
      },
      &isTimeout, aTimeoutMs, nsITimer::TYPE_ONE_SHOT,
      "TestAvailableMemoryWatcherMac"_ns);

  SpinEventLoopUntil("TestAvailableMemoryWatcherMac"_ns, [&]() -> bool {
    if (isTimeout) {
      return true;
    }
    return aCondition();
  });

  return !isTimeout;
}

class Spinner final : public nsIObserver {
  nsCOMPtr<nsIObserverService> mObserverSvc;
  nsDependentCString mTopicToWatch;
  bool mTopicObserved;

  ~Spinner() = default;

 public:
  NS_DECL_ISUPPORTS

  Spinner(nsIObserverService* aObserverSvc, const char* const aTopic,
          const char16_t* const aSubTopic)
      : mObserverSvc(aObserverSvc),
        mTopicToWatch(aTopic),
        mTopicObserved(false) {}

  NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
                     const char16_t* aData) override {
    if (mTopicToWatch == aTopic) {
      mTopicObserved = true;
      mObserverSvc->RemoveObserver(this, aTopic);

      // Force the loop to move in case that there is no event in the queue.
      nsCOMPtr<nsIRunnable> dummyEvent = new Runnable(__func__);
      NS_DispatchToMainThread(dummyEvent);
    }
    return NS_OK;
  }

  void StartListening() {
    mTopicObserved = false;
    mObserverSvc->AddObserver(this, mTopicToWatch.get(), false);
  }

  bool Wait(uint32_t aTimeoutMs) {
    return WaitUntil([this]() { return this->mTopicObserved; }, aTimeoutMs);
  }
};

NS_IMPL_ISUPPORTS(Spinner, nsIObserver)

class MockTabUnloader final : public nsITabUnloader {
  ~MockTabUnloader() = default;

  uint32_t mCounter;

 public:
  MockTabUnloader() : mCounter(0) {}

  NS_DECL_THREADSAFE_ISUPPORTS

  void ResetCounter() { mCounter = 0; }
  uint32_t GetCounter() const { return mCounter; }

  NS_IMETHOD UnloadTabAsync() override {
    ++mCounter;
    // Issue a memory-pressure to verify OnHighMemory issues
    // a memory-pressure-stop event.
    NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory);
    return NS_OK;
  }
};

NS_IMPL_ISUPPORTS(MockTabUnloader, nsITabUnloader)

}  // namespace

class AvailableMemoryWatcherFixture : public TelemetryTestFixture {
  nsCOMPtr<nsIObserverService> mObserverSvc;

 protected:
  RefPtr<nsAvailableMemoryWatcherBase> mWatcher;
  RefPtr<Spinner> mHighMemoryObserver;
  RefPtr<Spinner> mLowMemoryObserver;
  RefPtr<MockTabUnloader> mTabUnloader;

  static constexpr uint32_t kStateChangeTimeoutMs = 20000;
  static constexpr uint32_t kNotificationTimeoutMs = 20000;

  void TestSpecificSetUp() override {
    mObserverSvc = do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
    ASSERT_TRUE(mObserverSvc);

    mHighMemoryObserver =
        new Spinner(mObserverSvc, "memory-pressure-stop", nullptr);
    mLowMemoryObserver = new Spinner(mObserverSvc, "memory-pressure", nullptr);

    mTabUnloader = new MockTabUnloader;

    mWatcher = nsAvailableMemoryWatcherBase::GetSingleton();
    mWatcher->RegisterTabUnloader(mTabUnloader);
  }
};

class MemoryWatcherTelemetryEvent {
  uint32_t mLastCountOfEvents;

  static uint32_t CurrentEventCount() {
    auto optEvents =
        mozilla::glean::memory_watcher::on_high_memory_stats.TestGetValue()
            .unwrap();
    return optEvents.isSome() ? optEvents.ref().Length() : 0;
  }

  static nsCString LastEventValue() {
    auto optEvents =
        mozilla::glean::memory_watcher::on_high_memory_stats.TestGetValue()
            .unwrap();
    if (optEvents.isNothing() || optEvents.ref().IsEmpty()) {
      return ""_ns;
    }
    for (const auto& extra : optEvents.ref().LastElement().mExtra) {
      if (std::get<0>(extra) == "value"_ns) {
        return nsCString(std::get<1>(extra));
      }
    }
    return ""_ns;
  }

 public:
  explicit MemoryWatcherTelemetryEvent(JSContext*)
      : mLastCountOfEvents(CurrentEventCount()) {}

  void ValidateLastEvent(JSContext*) {
    uint32_t currentCount = CurrentEventCount();
    // A new event was generated.
    EXPECT_EQ(currentCount, mLastCountOfEvents + 1);
    if (currentCount <= mLastCountOfEvents) {
      // No new event was added; nothing new to validate.
      return;
    }

    // Update mLastCountOfEvents for a subsequent call to ValidateLastEvent
    ++mLastCountOfEvents;

    nsCString value = LastEventValue();
    nsTArray<nsCString> tokens;
    for (const nsACString& token : value.Split(',')) {
      tokens.AppendElement(token);
    }
    EXPECT_EQ(tokens.Length(), 3U);

    // The third token should be a valid floating number.
    nsresult rv;
    tokens[2].ToDouble(&rv);
    EXPECT_NS_SUCCEEDED(rv);
  }
};

/*
 * Test the browser memory pressure reponse by artificially putting the system
 * into the "critical" level and ensuring 1) a tab unload attempt occurs and
 * 2) the Gecko memory-pressure notitificiation start and stop events occur.
 */
TEST_F(AvailableMemoryWatcherFixture, MemoryPressureResponse) {
  // Set the memory pressure state to normal in case we are already
  // running in a low memory pressure state.
  mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eNormal);

  // Reset state
  mTabUnloader->ResetCounter();
  AutoJSContextWithGlobal cx(mCleanGlobal);
  MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext());

  // Simulate a low memory OS callback and make sure we observe
  // a memory-pressure event and a tab unload.
  mLowMemoryObserver->StartListening();
  mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eWarning);
  mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eCritical);
  EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
                        kStateChangeTimeoutMs));
  EXPECT_TRUE(mLowMemoryObserver->Wait(kStateChangeTimeoutMs));

  // Simulate the normal memory pressure OS callback and make
  // sure we observe a memory-pressure-stop event.
  mHighMemoryObserver->StartListening();
  mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eWarning);
  mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eNormal);
  EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));

  telemetryEvent.ValidateLastEvent(cx.GetJSContext());
}
