//===-- DumpValueObjectOptionsTests.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/Linux/PlatformLinux.h"
#include "Plugins/ScriptInterpreter/None/ScriptInterpreterNone.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
#include "TestingSupport/SubsystemRAII.h"
#include "TestingSupport/Symbol/ClangTestUtils.h"
#include "lldb/Core/Debugger.h"
#include "lldb/DataFormatters/DumpValueObjectOptions.h"
#include "lldb/ValueObject/ValueObject.h"
#include "lldb/ValueObject/ValueObjectConstResult.h"

#include "gtest/gtest.h"

#include <type_traits>

using namespace lldb;
using namespace lldb_private;

struct MockProcess : Process {
  MockProcess(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp)
      : Process(target_sp, listener_sp) {}

  llvm::StringRef GetPluginName() override { return "mock process"; }

  bool CanDebug(lldb::TargetSP target, bool plugin_specified_by_name) override {
    return false;
  };

  Status DoDestroy() override { return {}; }

  void RefreshStateAfterStop() override {}

  bool DoUpdateThreadList(ThreadList &old_thread_list,
                          ThreadList &new_thread_list) override {
    return false;
  };

  size_t DoReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
                      Status &error) override {
    // No need to read memory in these tests.
    return size;
  }
};

class ValueObjectMockProcessTest : public ::testing::Test {
public:
  void SetUp() override {
    ArchSpec arch("i386-pc-linux");
    Platform::SetHostPlatform(
        platform_linux::PlatformLinux::CreateInstance(true, &arch));
    m_debugger_sp = Debugger::CreateInstance();
    ASSERT_TRUE(m_debugger_sp);
    m_debugger_sp->GetTargetList().CreateTarget(*m_debugger_sp, "", arch,
                                                eLoadDependentsNo,
                                                m_platform_sp, m_target_sp);
    ASSERT_TRUE(m_target_sp);
    ASSERT_TRUE(m_target_sp->GetArchitecture().IsValid());
    ASSERT_TRUE(m_platform_sp);
    m_listener_sp = Listener::MakeListener("dummy");
    m_process_sp = std::make_shared<MockProcess>(m_target_sp, m_listener_sp);
    ASSERT_TRUE(m_process_sp);
    m_exe_ctx = ExecutionContext(m_process_sp);

    m_holder = std::make_unique<clang_utils::TypeSystemClangHolder>("test");
    m_type_system = m_holder->GetAST();
  }

  template <typename UnderlyingType>
  void TestDumpEnum(
      const std::vector<std::pair<const char *, UnderlyingType>> enumerators,
      const std::vector<std::tuple<UnderlyingType, DumpValueObjectOptions,
                                   const char *>> &tests) {
    CompilerType enum_type = MakeEnumType(enumerators);
    StreamString strm;
    ConstString var_name("test_var");
    ByteOrder endian = endian::InlHostByteOrder();
    ExecutionContextScope *exe_scope = m_exe_ctx.GetBestExecutionContextScope();
    for (auto [value, options, expected] : tests) {
      DataExtractor data_extractor{&value, sizeof(value), endian, 4};
      auto valobj_sp = ValueObjectConstResult::Create(exe_scope, enum_type,
                                                      var_name, data_extractor);
      if (llvm::Error error = valobj_sp->Dump(strm, options))
        llvm::consumeError(std::move(error));
      ASSERT_STREQ(strm.GetString().str().c_str(), expected);
      strm.Clear();
    }
  }

  template <typename UnderlyingType>
  CompilerType MakeEnumType(
      const std::vector<std::pair<const char *, UnderlyingType>> enumerators) {
    CompilerType int_type = m_type_system->GetBuiltinTypeForEncodingAndBitSize(
        std::is_same<UnderlyingType, int>::value ? lldb::eEncodingSint
                                                 : lldb::eEncodingUint,
        32);
    CompilerType enum_type = m_type_system->CreateEnumerationType(
        "TestEnum", m_type_system->GetTranslationUnitDecl(),
        OptionalClangModuleID(), Declaration(), int_type, false);

    m_type_system->StartTagDeclarationDefinition(enum_type);
    Declaration decl;
    for (auto [name, value] : enumerators)
      m_type_system->AddEnumerationValueToEnumerationType(enum_type, decl, name,
                                                          value, 32);
    m_type_system->CompleteTagDeclarationDefinition(enum_type);

    return enum_type;
  }

  ExecutionContext m_exe_ctx;
  TypeSystemClang *m_type_system;

private:
  SubsystemRAII<FileSystem, HostInfo, platform_linux::PlatformLinux,
                ScriptInterpreterNone>
      m_subsystems;

  std::unique_ptr<clang_utils::TypeSystemClangHolder> m_holder;
  lldb::DebuggerSP m_debugger_sp;
  lldb::TargetSP m_target_sp;
  lldb::PlatformSP m_platform_sp;
  lldb::ListenerSP m_listener_sp;
  lldb::ProcessSP m_process_sp;
};

TEST_F(ValueObjectMockProcessTest, EmptyEnum) {
  // All values of an empty enum should be shown as plain numbers.
  TestDumpEnum<unsigned>({}, {{0, {}, "(TestEnum) test_var = 0\n"},
                              {1, {}, "(TestEnum) test_var = 1\n"},
                              {2, {}, "(TestEnum) test_var = 2\n"}});

  TestDumpEnum<int>({}, {{-2, {}, "(TestEnum) test_var = -2\n"},
                         {-1, {}, "(TestEnum) test_var = -1\n"},
                         {0, {}, "(TestEnum) test_var = 0\n"},
                         {1, {}, "(TestEnum) test_var = 1\n"},
                         {2, {}, "(TestEnum) test_var = 2\n"}});
}

TEST_F(ValueObjectMockProcessTest, Enum) {
  // This is not a bitfield-like enum, so values are printed as decimal by
  // default. Also we only show the enumerator name if the value is an
  // exact match.
  TestDumpEnum<unsigned>(
      {{"test_2", 2}, {"test_3", 3}},
      {{0, {}, "(TestEnum) test_var = 0\n"},
       {1, {}, "(TestEnum) test_var = 1\n"},
       {2, {}, "(TestEnum) test_var = test_2\n"},
       {3, {}, "(TestEnum) test_var = test_3\n"},
       {4, {}, "(TestEnum) test_var = 4\n"},
       {5, {}, "(TestEnum) test_var = 5\n"},
       {1, DumpValueObjectOptions().SetHideRootName(true), "(TestEnum) 1\n"},
       {1, DumpValueObjectOptions().SetHideRootType(true), "test_var = 1\n"},
       {1, DumpValueObjectOptions().SetHideRootName(true).SetHideRootType(true),
        "1\n"},
       {1, DumpValueObjectOptions().SetHideName(true), "(TestEnum) 1\n"},
       {1, DumpValueObjectOptions().SetHideValue(true),
        "(TestEnum) test_var =\n"},
       {1, DumpValueObjectOptions().SetHideName(true).SetHideValue(true),
        "(TestEnum) \n"}});
}

TEST_F(ValueObjectMockProcessTest, BitFieldLikeEnum) {
  // These enumerators set individual bits in the value, as if it were a flag
  // set. lldb treats this as a "bitfield like enum". This means we show values
  // as hex, and values without exact matches are shown as a combination of
  // enumerators and any remaining value left over.
  TestDumpEnum<unsigned>(
      {{"test_2", 2}, {"test_4", 4}},
      {
          {0, {}, "(TestEnum) test_var = 0x0\n"},
          {1, {}, "(TestEnum) test_var = 0x1\n"},
          {2, {}, "(TestEnum) test_var = test_2\n"},
          {4, {}, "(TestEnum) test_var = test_4\n"},
          {6, {}, "(TestEnum) test_var = test_2 | test_4\n"},
          {7, {}, "(TestEnum) test_var = test_2 | test_4 | 0x1\n"},
          {8, {}, "(TestEnum) test_var = 0x8\n"},
          {1, DumpValueObjectOptions().SetHideRootName(true),
           "(TestEnum) 0x1\n"},
          {1, DumpValueObjectOptions().SetHideRootType(true),
           "test_var = 0x1\n"},
          {1,
           DumpValueObjectOptions().SetHideRootName(true).SetHideRootType(true),
           "0x1\n"},
          {1, DumpValueObjectOptions().SetHideName(true), "(TestEnum) 0x1\n"},
          {1, DumpValueObjectOptions().SetHideValue(true),
           "(TestEnum) test_var =\n"},
          {1, DumpValueObjectOptions().SetHideName(true).SetHideValue(true),
           "(TestEnum) \n"},
      });
}
