//===-- ProgressReportTest.cpp --------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "Plugins/Platform/MacOSX/PlatformMacOSX.h"
#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h"
#include "TestingSupport/SubsystemRAII.h"
#include "TestingSupport/TestUtilities.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/Progress.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/HostInfo.h"
#include "lldb/Utility/Listener.h"
#include "gtest/gtest.h"
#include <memory>
#include <mutex>
#include <thread>

using namespace lldb;
using namespace lldb_private;

static std::chrono::milliseconds TIMEOUT(500);

class ProgressReportTest : public ::testing::Test {
public:
  ListenerSP CreateListenerFor(uint32_t bit) {
    // Set up the debugger, make sure that was done properly.
    ArchSpec arch("x86_64-apple-macosx-");
    Platform::SetHostPlatform(
        PlatformRemoteMacOSX::CreateInstance(true, &arch));

    m_debugger_sp = Debugger::CreateInstance();

    // Get the debugger's broadcaster.
    Broadcaster &broadcaster = m_debugger_sp->GetBroadcaster();

    // Create a listener, make sure it can receive events and that it's
    // listening to the correct broadcast bit.
    m_listener_sp = Listener::MakeListener("progress-listener");
    m_listener_sp->StartListeningForEvents(&broadcaster, bit);
    return m_listener_sp;
  }

protected:
  // The debugger's initialization function can't be called with no arguments
  // so calling it using SubsystemRAII will cause the test build to fail as
  // SubsystemRAII will call Initialize with no arguments. As such we set it up
  // here the usual way.
  void SetUp() override {
    std::call_once(TestUtilities::g_debugger_initialize_flag,
                   []() { Debugger::Initialize(nullptr); });
  };

  DebuggerSP m_debugger_sp;
  ListenerSP m_listener_sp;
  SubsystemRAII<FileSystem, HostInfo, PlatformMacOSX> subsystems;
};

TEST_F(ProgressReportTest, TestReportCreation) {
  ListenerSP listener_sp = CreateListenerFor(lldb::eBroadcastBitProgress);
  EventSP event_sp;
  const ProgressEventData *data;

  // Scope this for RAII on the progress objects.
  // Create progress reports and check that their respective events for having
  // started and ended are broadcasted.
  {
    Progress progress1("Progress report 1", "Starting report 1");
    Progress progress2("Progress report 2", "Starting report 2");
    Progress progress3("Progress report 3", "Starting report 3");
  }

  // Start popping events from the queue, they should have been recevied
  // in this order:
  // Starting progress: 1, 2, 3
  // Ending progress: 3, 2, 1
  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetDetails(), "Starting report 1");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_FALSE(data->GetCompleted());
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Progress report 1: Starting report 1");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetDetails(), "Starting report 2");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_FALSE(data->GetCompleted());
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Progress report 2: Starting report 2");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetDetails(), "Starting report 3");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_FALSE(data->GetCompleted());
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Progress report 3: Starting report 3");

  // Progress report objects should be destroyed at this point so
  // get each report from the queue and check that they've been
  // destroyed in reverse order.
  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetTitle(), "Progress report 3");
  EXPECT_TRUE(data->GetCompleted());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetMessage(), "Progress report 3: Starting report 3");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetTitle(), "Progress report 2");
  EXPECT_TRUE(data->GetCompleted());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetMessage(), "Progress report 2: Starting report 2");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetTitle(), "Progress report 1");
  EXPECT_TRUE(data->GetCompleted());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetMessage(), "Progress report 1: Starting report 1");
}

TEST_F(ProgressReportTest, TestReportDestructionWithPartialProgress) {
  ListenerSP listener_sp = CreateListenerFor(lldb::eBroadcastBitProgress);
  EventSP event_sp;
  const ProgressEventData *data;

  // Create a finite progress report and only increment to a non-completed
  // state before destruction.
  {
    Progress progress("Finite progress", "Report 1", 100);
    progress.Increment(3);
  }

  // Verify that the progress in the events are:
  // 1. At construction: 0 out of 100
  // 2. At increment: 3 out of 100
  // 3. At destruction: 100 out of 100
  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_EQ(data->GetDetails(), "Report 1");
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), (uint64_t)0);
  EXPECT_EQ(data->GetTotal(), (uint64_t)100);
  EXPECT_EQ(data->GetMessage(), "Finite progress: Report 1");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_EQ(data->GetDetails(), "Report 1");
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), (uint64_t)3);
  EXPECT_EQ(data->GetTotal(), (uint64_t)100);
  EXPECT_EQ(data->GetMessage(), "Finite progress: Report 1");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_EQ(data->GetDetails(), "Report 1");
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), (uint64_t)100);
  EXPECT_EQ(data->GetTotal(), (uint64_t)100);
  EXPECT_EQ(data->GetMessage(), "Finite progress: Report 1");

  // Create an infinite progress report and increment by some amount.
  {
    Progress progress("Infinite progress", "Report 2");
    progress.Increment(3);
  }

  // Verify that the progress in the events are:
  // 1. At construction: 0
  // 2. At increment: 3
  // 3. At destruction: Progress::kNonDeterministicTotal
  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_EQ(data->GetDetails(), "Report 2");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), (uint64_t)0);
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Infinite progress: Report 2");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_EQ(data->GetDetails(), "Report 2");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), (uint64_t)3);
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Infinite progress: Report 2");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_EQ(data->GetDetails(), "Report 2");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Infinite progress: Report 2");
}

TEST_F(ProgressReportTest, TestFiniteOverflow) {
  ListenerSP listener_sp = CreateListenerFor(lldb::eBroadcastBitProgress);
  EventSP event_sp;
  const ProgressEventData *data;

  // Increment the report beyond its limit and make sure we only get one
  // completed event.
  {
    Progress progress("Finite progress", "Report 1", 10);
    progress.Increment(11);
    progress.Increment(47);
  }

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), 0U);
  EXPECT_EQ(data->GetTotal(), 10U);

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), 10U);
  EXPECT_EQ(data->GetTotal(), 10U);

  ASSERT_FALSE(listener_sp->GetEvent(event_sp, TIMEOUT));
}

TEST_F(ProgressReportTest, TestNonDeterministicOverflow) {
  ListenerSP listener_sp = CreateListenerFor(lldb::eBroadcastBitProgress);
  EventSP event_sp;
  const ProgressEventData *data;
  constexpr uint64_t max_minus_1 = std::numeric_limits<uint64_t>::max() - 1;

  // Increment the report beyond its limit and make sure we only get one
  // completed event. The event which overflows the counter should be ignored.
  {
    Progress progress("Non deterministic progress", "Report 1");
    progress.Increment(max_minus_1);
    progress.Increment(max_minus_1);
  }

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), 0U);
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), max_minus_1);
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);

  ASSERT_FALSE(listener_sp->GetEvent(event_sp, TIMEOUT));
}

TEST_F(ProgressReportTest, TestMinimumReportTime) {
  ListenerSP listener_sp = CreateListenerFor(lldb::eBroadcastBitProgress);
  EventSP event_sp;
  const ProgressEventData *data;

  {
    Progress progress("Finite progress", "Report 1", /*total=*/20,
                      m_debugger_sp.get(),
                      /*minimum_report_time=*/std::chrono::seconds(1));
    // Send 10 events in quick succession. These should not generate any events.
    for (int i = 0; i < 10; ++i)
      progress.Increment();

    // Sleep, then send 10 more. This should generate one event for the first
    // increment, and then another for completion.
    std::this_thread::sleep_for(std::chrono::seconds(1));
    for (int i = 0; i < 10; ++i)
      progress.Increment();
  }

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), 0U);
  EXPECT_EQ(data->GetTotal(), 20U);

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), 11U);
  EXPECT_EQ(data->GetTotal(), 20U);

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
  EXPECT_TRUE(data->IsFinite());
  EXPECT_EQ(data->GetCompleted(), 20U);
  EXPECT_EQ(data->GetTotal(), 20U);

  ASSERT_FALSE(listener_sp->GetEvent(event_sp, TIMEOUT));
}

TEST_F(ProgressReportTest, TestExternalReportCreation) {
  ListenerSP listener_sp =
      CreateListenerFor(lldb::eBroadcastBitExternalProgress);
  EventSP event_sp;
  const ProgressEventData *data;

  // Scope this for RAII on the progress objects.
  // Create progress reports and check that their respective events for having
  // started and ended are broadcasted.
  {
    Progress progress1("Progress report 1", "Starting report 1",
                       /*total=*/std::nullopt, /*debugger=*/nullptr,
                       /*minimum_report_time=*/std::chrono::seconds(0),
                       Progress::Origin::eExternal);
    Progress progress2("Progress report 2", "Starting report 2",
                       /*total=*/std::nullopt, /*debugger=*/nullptr,
                       /*minimum_report_time=*/std::chrono::seconds(0),
                       Progress::Origin::eExternal);
    Progress progress3("Progress report 3", "Starting report 3",
                       /*total=*/std::nullopt, /*debugger=*/nullptr,
                       /*minimum_report_time=*/std::chrono::seconds(0),
                       Progress::Origin::eExternal);
  }

  // Start popping events from the queue, they should have been recevied
  // in this order:
  // Starting progress: 1, 2, 3
  // Ending progress: 3, 2, 1
  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetDetails(), "Starting report 1");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_FALSE(data->GetCompleted());
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Progress report 1: Starting report 1");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetDetails(), "Starting report 2");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_FALSE(data->GetCompleted());
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Progress report 2: Starting report 2");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetDetails(), "Starting report 3");
  EXPECT_FALSE(data->IsFinite());
  EXPECT_FALSE(data->GetCompleted());
  EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
  EXPECT_EQ(data->GetMessage(), "Progress report 3: Starting report 3");

  // Progress report objects should be destroyed at this point so
  // get each report from the queue and check that they've been
  // destroyed in reverse order.
  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetTitle(), "Progress report 3");
  EXPECT_TRUE(data->GetCompleted());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetMessage(), "Progress report 3: Starting report 3");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetTitle(), "Progress report 2");
  EXPECT_TRUE(data->GetCompleted());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetMessage(), "Progress report 2: Starting report 2");

  ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
  data = ProgressEventData::GetEventDataFromEvent(event_sp.get());

  EXPECT_EQ(data->GetTitle(), "Progress report 1");
  EXPECT_TRUE(data->GetCompleted());
  EXPECT_FALSE(data->IsFinite());
  EXPECT_EQ(data->GetMessage(), "Progress report 1: Starting report 1");
}

TEST_F(ProgressReportTest, TestExternalReportNotReceived) {
  ListenerSP listener_sp = CreateListenerFor(lldb::eBroadcastBitProgress);
  EventSP event_sp;

  // Scope this for RAII on the progress objects.
  // Create progress reports and check that their respective events for having
  // started and ended are broadcasted.
  {
    Progress progress1("External Progress report 1",
                       "Starting external report 1",
                       /*total=*/std::nullopt, /*debugger=*/nullptr,
                       /*minimum_report_time=*/std::chrono::seconds(0),
                       Progress::Origin::eExternal);
  }

  ASSERT_FALSE(listener_sp->GetEvent(event_sp, TIMEOUT));
}