//===- llvm/unittest/Telemetry/TelemetryTest.cpp - Telemetry unittests ---===//
//
// 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 "llvm/Telemetry/Telemetry.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/Error.h"
#include "gtest/gtest.h"
#include <optional>
#include <vector>

namespace llvm {
namespace telemetry {
// Testing parameters.
//
// These are set by each test to force certain outcomes.
struct TestContext {
  // Controlling whether there is vendor plugin.  In "real" implementation, the
  // plugin-registration framework will handle the overrides but for tests, we
  // just use a bool flag to decide which function to call.
  bool HasVendorPlugin = false;

  // This field contains data emitted by the framework for later
  // verification by the tests.
  std::string Buffer = "";

  // The expected Uuid generated by the fake tool.
  std::string ExpectedUuid = "";
};

class StringSerializer : public Serializer {
public:
  const std::string &getString() { return Buffer; }

  Error init() override {
    if (Started)
      return createStringError("Serializer already in use");
    Started = true;
    Buffer.clear();
    return Error::success();
  }

  void write(StringRef KeyName, bool Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, StringRef Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, int Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, long Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, long long Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, unsigned int Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, unsigned long Value) override {
    writeHelper(KeyName, Value);
  }

  void write(StringRef KeyName, unsigned long long Value) override {
    writeHelper(KeyName, Value);
  }

  void beginObject(StringRef KeyName) override {
    Children.push_back(std::string("\n"));
    ChildrenNames.push_back(KeyName.str());
  }

  void endObject() override {
    assert(!Children.empty() && !ChildrenNames.empty());
    std::string ChildBuff = Children.back();
    std::string Name = ChildrenNames.back();
    Children.pop_back();
    ChildrenNames.pop_back();
    writeHelper(Name, ChildBuff);
  }

  Error finalize() override {
    assert(Children.empty() && ChildrenNames.empty());
    if (!Started)
      return createStringError("Serializer not currently in use");
    Started = false;
    return Error::success();
  }

private:
  template <typename T> void writeHelper(StringRef Name, T Value) {
    assert(Started && "serializer not started");
    if (Children.empty())
      Buffer.append((Name + ":" + Twine(Value) + "\n").str());
    else
      Children.back().append((Name + ":" + Twine(Value) + "\n").str());
  }

  bool Started = false;
  std::string Buffer;
  std::vector<std::string> Children;
  std::vector<std::string> ChildrenNames;
};

namespace vendor {
struct VendorConfig : public Config {
  VendorConfig(bool Enable) : Config(Enable) {}
  std::optional<std::string> makeSessionId() override {
    static int seed = 0;
    return std::to_string(seed++);
  }
};

std::shared_ptr<Config> getTelemetryConfig(const TestContext &Ctxt) {
  return std::make_shared<VendorConfig>(/*EnableTelemetry=*/true);
}

class TestStorageDestination : public Destination {
public:
  TestStorageDestination(TestContext *Ctxt) : CurrentContext(Ctxt) {}

  Error receiveEntry(const TelemetryInfo *Entry) override {
    if (Error Err = serializer.init())
      return Err;

    Entry->serialize(serializer);
    if (Error Err = serializer.finalize())
      return Err;

    CurrentContext->Buffer.append(serializer.getString());
    return Error::success();
  }

  StringLiteral name() const override { return "TestDestination"; }

private:
  TestContext *CurrentContext;
  StringSerializer serializer;
};

struct StartupInfo : public TelemetryInfo {
  std::string ToolName;
  std::map<std::string, std::string> MetaData;

  void serialize(Serializer &serializer) const override {
    TelemetryInfo::serialize(serializer);
    serializer.write("ToolName", ToolName);
    serializer.write("MetaData", MetaData);
  }
};

struct ExitInfo : public TelemetryInfo {
  int ExitCode;
  std::string ExitDesc;
  void serialize(Serializer &serializer) const override {
    TelemetryInfo::serialize(serializer);
    serializer.write("ExitCode", ExitCode);
    serializer.write("ExitDesc", ExitDesc);
  }
};

class TestManager : public Manager {
public:
  static std::unique_ptr<TestManager>
  createInstance(Config *Config, TestContext *CurrentContext) {
    if (!Config->EnableTelemetry)
      return nullptr;
    CurrentContext->ExpectedUuid = *(Config->makeSessionId());
    std::unique_ptr<TestManager> Ret = std::make_unique<TestManager>(
        CurrentContext, CurrentContext->ExpectedUuid);

    // Add a destination.
    Ret->addDestination(
        std::make_unique<TestStorageDestination>(CurrentContext));

    return Ret;
  }

  TestManager(TestContext *Ctxt, std::string Id)
      : CurrentContext(Ctxt), SessionId(Id) {}

  Error preDispatch(TelemetryInfo *Entry) override {
    Entry->SessionId = SessionId;
    (void)CurrentContext;
    return Error::success();
  }

  std::string getSessionId() { return SessionId; }

private:
  TestContext *CurrentContext;
  const std::string SessionId;
};
} // namespace vendor

std::shared_ptr<Config> getTelemetryConfig(const TestContext &Ctxt) {
  if (Ctxt.HasVendorPlugin)
    return vendor::getTelemetryConfig(Ctxt);

  return std::make_shared<Config>(false);
}

#if LLVM_ENABLE_TELEMETRY
#define TELEMETRY_TEST(suite, test) TEST(suite, test)
#else
#define TELEMETRY_TEST(suite, test) TEST(DISABLED_##suite, test)
#endif

TELEMETRY_TEST(TelemetryTest, TelemetryDisabled) {
  TestContext Context;
  Context.HasVendorPlugin = false;

  std::shared_ptr<Config> Config = getTelemetryConfig(Context);
  auto Manager = vendor::TestManager::createInstance(Config.get(), &Context);
  EXPECT_EQ(nullptr, Manager);
}

TELEMETRY_TEST(TelemetryTest, TelemetryEnabled) {
  const std::string ToolName = "TelemetryTestTool";

  // Preset some params.
  TestContext Context;
  Context.HasVendorPlugin = true;
  Context.Buffer.clear();

  std::shared_ptr<Config> Config = getTelemetryConfig(Context);
  auto Manager = vendor::TestManager::createInstance(Config.get(), &Context);

  EXPECT_STREQ(Manager->getSessionId().c_str(), Context.ExpectedUuid.c_str());

  vendor::StartupInfo S;
  S.ToolName = ToolName;
  S.MetaData["a"] = "A";
  S.MetaData["b"] = "B";

  Error startupEmitStatus = Manager->dispatch(&S);
  EXPECT_FALSE(startupEmitStatus);
  std::string ExpectedBuffer =
      "SessionId:0\nToolName:TelemetryTestTool\nMetaData:\na:A\nb:B\n\n";
  EXPECT_EQ(ExpectedBuffer, Context.Buffer);
  Context.Buffer.clear();

  vendor::ExitInfo E;
  E.ExitCode = 0;
  E.ExitDesc = "success";
  Error exitEmitStatus = Manager->dispatch(&E);
  EXPECT_FALSE(exitEmitStatus);
  ExpectedBuffer = "SessionId:0\nExitCode:0\nExitDesc:success\n";
  EXPECT_EQ(ExpectedBuffer, Context.Buffer);
}

} // namespace telemetry
} // namespace llvm
