#include "../src/tracer.h"

#include <datadog/tags.h>
#include <unistd.h>

#include <catch2/catch.hpp>
#include <ctime>

#include "../src/sample.h"
#include "../src/span.h"
#include "../src/tracer_options.h"
#include "mocks.h"
using namespace datadog::opentracing;

TEST_CASE("tracer") {
  int id = 100;  // Starting span id.
  // Starting calendar time 2007-03-12 00:00:00
  std::tm start{};
  start.tm_mday = 12;
  start.tm_mon = 2;
  start.tm_year = 107;
  TimePoint time{std::chrono::system_clock::from_time_t(timegm(&start)),
                 std::chrono::steady_clock::time_point{}};
  auto buffer = std::make_shared<MockBuffer>();
  TimeProvider get_time = [&time]() { return time; };  // Mock clock.
  IdProvider get_id = [&id]() { return id++; };        // Mock ID provider.
  auto logger = std::make_shared<const MockLogger>();
  TracerOptions tracer_options{"", 0, "service_name", "web"};
  std::shared_ptr<Tracer> tracer{new Tracer{tracer_options, buffer, get_time, get_id}};
  const ot::StartSpanOptions span_options;

  SECTION("names spans correctly") {
    auto span = tracer->StartSpanWithOptions("/what_up", span_options);
    const ot::FinishSpanOptions finish_options;
    span->FinishWithOptions(finish_options);

    auto& result = buffer->traces().at(100).finished_spans->at(0);
    REQUIRE(result->type == "web");
    REQUIRE(result->service == "service_name");
    REQUIRE(result->name == "/what_up");
    REQUIRE(result->resource == "/what_up");
    REQUIRE(result->meta.find("_dd.hostname") == result->meta.end());
    REQUIRE(result->metrics.find("_dd1.sr.eausr") == result->metrics.end());
  }

  SECTION("spans receive id") {
    auto span = tracer->StartSpanWithOptions("", span_options);
    const ot::FinishSpanOptions finish_options;
    span->FinishWithOptions(finish_options);

    auto& result = buffer->traces().at(100).finished_spans->at(0);
    REQUIRE(result->span_id == 100);
    REQUIRE(result->trace_id == 100);
    REQUIRE(result->parent_id == 0);
  }

  SECTION("span context is propagated") {
    MockTextMapCarrier carrier;
    SpanContext context{logger, 420, 69, "", {{"ayy", "lmao"}, {"hi", "haha"}}};
    auto success = tracer->Inject(context, carrier);
    REQUIRE(success);
    auto span_context_maybe = tracer->Extract(carrier);
    REQUIRE(span_context_maybe);
    auto span = tracer->StartSpan("fred", {ChildOf(span_context_maybe->get())});
    const ot::FinishSpanOptions finish_options;
    span->FinishWithOptions(finish_options);
    auto& result = buffer->traces().at(69).finished_spans->at(0);
    REQUIRE(result->span_id == 100);
    REQUIRE(result->trace_id == 69);
    REQUIRE(result->parent_id == 420);
  }

  SECTION("empty span context starts new root span") {
    MockTextMapCarrier carrier;
    auto span_context_maybe = tracer->Extract(carrier);
    auto span = tracer->StartSpan("fred", {ChildOf(span_context_maybe->get())});
    const ot::FinishSpanOptions finish_options;
    span->FinishWithOptions(finish_options);
    auto& result = buffer->traces().at(100).finished_spans->at(0);
    REQUIRE(result->span_id == 100);
    REQUIRE(result->trace_id == 100);
    REQUIRE(result->parent_id == 0);
  }

  SECTION("hostname is added as a tag") {
    buffer->setHostname("testhostname");
    auto root_span = tracer->StartSpanWithOptions("/root", span_options);
    auto child_span = tracer->StartSpan("/child", {ChildOf(&root_span->context())});
    root_span->SetTag("foo", "bar");
    child_span->SetTag("baz", "bing");
    const ot::FinishSpanOptions finish_options;
    child_span->FinishWithOptions(finish_options);
    root_span->FinishWithOptions(finish_options);
    buffer->setHostname("");

    // Tag should exist with the correct value on the root / local-root span.
    auto& root_result = buffer->traces().at(100).finished_spans->at(1);
    REQUIRE(root_result->meta["_dd.hostname"] == "testhostname");

    // Tag should not exist on the child span(s).
    auto& child_result = buffer->traces().at(100).finished_spans->at(0);
    REQUIRE(child_result->meta.find("_dd.hostname") == child_result->meta.end());
  }

  SECTION("analytics rate is added as a metric") {
    buffer->setAnalyticsRate(1.0);
    auto root_span = tracer->StartSpanWithOptions("/root", span_options);
    auto child_span = tracer->StartSpan("/child", {ChildOf(&root_span->context())});
    root_span->SetTag("foo", "bar");
    child_span->SetTag("baz", "bing");
    const ot::FinishSpanOptions finish_options;
    child_span->FinishWithOptions(finish_options);
    root_span->FinishWithOptions(finish_options);
    buffer->setAnalyticsRate(std::nan(""));

    // Metric should exist with the correct value on the root / local-root span.
    auto& root_result = buffer->traces().at(100).finished_spans->at(1);
    REQUIRE(root_result->metrics["_dd1.sr.eausr"] == 1.0);

    // Tag should not exist on the child span(s).
    auto& child_result = buffer->traces().at(100).finished_spans->at(0);
    REQUIRE(child_result->metrics.find("_dd1.sr.eausr") == child_result->metrics.end());
  }
}

TEST_CASE("env overrides") {
  int id = 100;  // Starting span id.
  // Starting calendar time 2007-03-12 00:00:00
  std::tm start{};
  start.tm_mday = 12;
  start.tm_mon = 2;
  start.tm_year = 107;
  TimePoint time{std::chrono::system_clock::from_time_t(timegm(&start)),
                 std::chrono::steady_clock::time_point{}};
  // auto buffer = std::make_shared<MockBuffer>();
  TimeProvider get_time = [&time]() { return time; };  // Mock clock.
  IdProvider get_id = [&id]() { return id++; };        // Mock ID provider.
  auto sampler = std::make_shared<RulesSampler>();
  auto writer = std::make_shared<MockWriter>(sampler);
  TracerOptions tracer_options{"", 0, "service_name", "web"};
  const ot::StartSpanOptions span_options;

  struct EnvOverrideTest {
    std::string env;
    std::string val;
    bool enabled;
    std::string hostname;
    double rate;
    bool error;
    std::string environment;
    std::string version;
    std::map<std::string, std::string> extra_tags;
  };

  char buf[256];
  ::gethostname(buf, 256);
  std::string hostname(buf);

  auto env_test = GENERATE_COPY(values<EnvOverrideTest>({
      // Normal cases
      {"DD_TRACE_ENABLED", "false", false, "", std::nan(""), false, "test-env", "", {}},
      {"DD_ENV", "test-env", true, "", std::nan(""), false, "test-env", "", {}},
      {"DD_VERSION",
       "test-version v0.0.1",
       true,
       "",
       std::nan(""),
       false,
       "",
       "test-version v0.0.1",
       {}},
      {"DD_TRACE_REPORT_HOSTNAME", "true", true, hostname, std::nan(""), false, "", "", {}},
      {"DD_TRACE_ANALYTICS_ENABLED", "true", true, "", 1.0, false, "", "", {}},
      {"DD_TRACE_ANALYTICS_ENABLED", "false", true, "", 0.0, false, "", "", {}},
      {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "0.5", true, "", 0.5, false, "", "", {}},
      {"DD_TAGS",
       "host:my-host-name,region:us-east-1,datacenter:us,partition:5",
       true,
       "",
       std::nan(""),
       false,
       "",
       "",
       {
           {"host", "my-host-name"},
           {"region", "us-east-1"},
           {"datacenter", "us"},
           {"partition", "5"},
       }},
      {"", "", true, "", std::nan(""), false, "", "", {}},
      // Unexpected values handled gracefully
      {"DD_TRACE_ANALYTICS_ENABLED", "yes please", true, "", std::nan(""), true, "", "", {}},
      {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "1.1", true, "", std::nan(""), true, "", "", {}},
      {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "half", true, "", std::nan(""), true, "", "", {}},
  }));

  SECTION("set correct tags and metrics") {
    // Setup
    ::setenv(env_test.env.c_str(), env_test.val.c_str(), 0);
    auto maybe_options = applyTracerOptionsFromEnvironment(tracer_options);
    if (env_test.error) {
      REQUIRE(!maybe_options);
      return;
    }
    REQUIRE(maybe_options);
    TracerOptions opts = maybe_options.value();
    opts.log_func = [](LogLevel, ot::string_view) {};  // noise suppression
    std::shared_ptr<Tracer> tracer{
        new Tracer{opts, writer, sampler, std::make_shared<MockLogger>()}};

    // Create span
    auto span = tracer->StartSpanWithOptions("/env-override", span_options);
    const ot::FinishSpanOptions finish_options;
    span->FinishWithOptions(finish_options);

    if (env_test.enabled) {
      auto& result = writer->traces[0][0];
      // Check the hostname matches the expected value.
      if (env_test.hostname.empty()) {
        REQUIRE(result->meta.find("_dd.hostname") == result->meta.end());
      } else {
        REQUIRE(result->meta["_dd.hostname"] == env_test.hostname);
      }
      // Check the analytics rate matches the expected value.
      if (std::isnan(env_test.rate)) {
        REQUIRE(result->metrics.find("_dd1.sr.eausr") == result->metrics.end());
      } else {
        REQUIRE(result->metrics["_dd1.sr.eausr"] == env_test.rate);
      }
      // Check the environment matches the expected value.
      if (env_test.environment.empty()) {
        REQUIRE(result->meta.find(datadog::tags::environment) == result->meta.end());
      } else {
        REQUIRE(result->meta[datadog::tags::environment] == env_test.environment);
      }
      // Check the version matches the expected value.
      if (env_test.version.empty()) {
        REQUIRE(result->meta.find(datadog::tags::version) == result->meta.end());
      } else {
        REQUIRE(result->meta[datadog::tags::version] == env_test.version);
      }
      // Check spans are tagged with values from DD_TAGS
      for (auto& tag : env_test.extra_tags) {
        REQUIRE(result->meta.find(tag.first) != result->meta.end());
        REQUIRE(result->meta[tag.first] == tag.second);
      }
    } else {
      REQUIRE(writer->traces.empty());
    }

    // Tear-down
    ::unsetenv(env_test.env.c_str());
  }
}

TEST_CASE("startup log") {
  auto enabled = GENERATE(false, true);
  std::string env_var = "DD_TRACE_STARTUP_LOGS";
  if (!enabled) {
    ::setenv(env_var.c_str(), "false", 0);
  }
  TracerOptions opts;
  opts.agent_url = "/path/to/unix.socket";
  opts.analytics_rate = 0.3;
  opts.tags.emplace("foo", "bar");
  opts.tags.emplace("themeaningoflifetheuniverseandeverything", "42");
  opts.operation_name_override = "meaningful.name";
  std::stringstream ss;
  opts.log_func = [&](LogLevel, ot::string_view message) { ss << message; };

  auto sampler = std::make_shared<RulesSampler>();
  auto writer = std::make_shared<MockWriter>(sampler);
  std::shared_ptr<Tracer> tracer{
      new Tracer{opts, writer, sampler, std::make_shared<MockLogger>()}};

  if (enabled) {
    REQUIRE(!ss.str().empty());
    std::string log_message = ss.str();
    std::string prefix = "DATADOG TRACER CONFIGURATION - ";
    REQUIRE(log_message.substr(0, prefix.size()) == prefix);
    json j = json::parse(log_message.substr(prefix.size()));  // may throw an exception
    REQUIRE(j["date"].get<std::string>().size() == 24);
    REQUIRE(j["agent_url"] == opts.agent_url);
    REQUIRE(j["analytics_sample_rate"] == opts.analytics_rate);
    REQUIRE(j["tags"] == opts.tags);
    REQUIRE(j["operation_name_override"] == opts.operation_name_override);
  } else {
    REQUIRE(ss.str().empty());
  }
  ::unsetenv(env_var.c_str());
}
