// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/test/chromedriver/performance_logger.h"

#include <string>
#include <vector>

#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/log.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/chrome/web_view_impl.h"
#include "chrome/test/chromedriver/net/timeout.h"
#include "chrome/test/chromedriver/session.h"

namespace {

// DevTools event domain prefixes to intercept.
const char* const kDomains[] = {"Network.", "Page."};

// Whitelist of WebDriver commands on which to request buffered trace events.
const char* const kRequestTraceCommands[] = {"GetLog" /* required */,
    "Navigate"};

bool IsBrowserwideClient(DevToolsClient* client) {
  return (client->GetId() == DevToolsClientImpl::kBrowserwideDevToolsClientId);
}

bool IsEnabled(const PerfLoggingPrefs::InspectorDomainStatus& domain_status) {
    return (domain_status ==
            PerfLoggingPrefs::InspectorDomainStatus::kDefaultEnabled ||
            domain_status ==
            PerfLoggingPrefs::InspectorDomainStatus::kExplicitlyEnabled);
}

// Returns whether |command| is in kRequestTraceCommands[] (case-insensitive).
// In the case of GetLog, also check if it has been called previously, that it
// was emptying an empty log.
bool ShouldRequestTraceEvents(const std::string& command,
                              const bool log_emptied) {
  if (base::EqualsCaseInsensitiveASCII(command, "GetLog") && !log_emptied)
    return false;

  for (auto* request_command : kRequestTraceCommands) {
    if (base::EqualsCaseInsensitiveASCII(command, request_command))
      return true;
  }
  return false;
}

// Returns whether the event belongs to one of kDomains.
bool ShouldLogEvent(const std::string& method) {
  for (auto* domain : kDomains) {
    if (base::StartsWith(method, domain, base::CompareCase::SENSITIVE))
      return true;
  }
  return false;
}

}  // namespace

PerformanceLogger::PerformanceLogger(Log* log, const Session* session)
    : log_(log),
      session_(session),
      browser_client_(nullptr),
      trace_buffering_(false),
      enable_service_worker_(false) {}

PerformanceLogger::PerformanceLogger(Log* log,
                                     const Session* session,
                                     const PerfLoggingPrefs& prefs,
                                     bool enable_service_worker)
    : log_(log),
      session_(session),
      prefs_(prefs),
      browser_client_(nullptr),
      trace_buffering_(false),
      enable_service_worker_(enable_service_worker) {}

bool PerformanceLogger::subscribes_to_browser() {
  return true;
}

Status PerformanceLogger::OnConnected(DevToolsClient* client) {
  if (IsBrowserwideClient(client)) {
    browser_client_ = client;
    if (prefs_.trace_categories.empty())
      return Status(kOk);
    return StartTrace();
  }
  if (client->IsTabTarget()) {
    // Tab Targets do not support Network.enable
    return Status(kOk);
  }
  return EnableInspectorDomains(client);
}

Status PerformanceLogger::OnEvent(DevToolsClient* client,
                                  const std::string& method,
                                  const base::DictValue& params) {
  if (method == "Target.attachedToTarget") {
    const std::string* type = params.FindStringByDottedPath("targetInfo.type");
    if (!type) {
      return Status(kUnknownError,
                    "missing target type in Target.attachedToTarget event");
    }
    if (enable_service_worker_ && *type == "service_worker") {
      const std::string* target_id =
          params.FindStringByDottedPath("targetInfo.targetId");
      if (!target_id) {
        return Status(kUnknownError,
                      "missing target ID in Target.attachedToTarget event");
      }

      std::list<std::string> tabview_ids;
      Status status = session_->chrome->GetTopLevelWebViewIds(
          &tabview_ids, session_->w3c_compliant);
      if (status.IsError())
        return status;

      WebView* webview = nullptr;
      status = session_->chrome->GetWebViewById(*target_id, &webview);
      if (status.IsError())
        return status;
    }
  }
  if (IsBrowserwideClient(client)) {
    return HandleTraceEvents(client, method, params);
  } else {
    return HandleInspectorEvents(client, method, params);
  }
}

Status PerformanceLogger::BeforeCommand(const std::string& command_name) {
  // Only dump trace buffer after tracing has been started.
  if (trace_buffering_ &&
      ShouldRequestTraceEvents(command_name, log_->Emptied())) {
    Status status = CollectTraceEvents();
    if (status.IsError())
      return status;
  }
  return Status(kOk);
}

void PerformanceLogger::AddLogEntry(Log::Level level,
                                    const std::string& webview,
                                    const std::string& method,
                                    const base::DictValue& params) {
  base::DictValue log_message_dict;
  log_message_dict.Set("webview", webview);
  log_message_dict.SetByDottedPath("message.method", method);
  log_message_dict.SetByDottedPath("message.params", params.Clone());
  std::string log_message_json = base::WriteJson(log_message_dict).value_or("");

  // TODO(klm): extract timestamp from params?
  // Look at where it is for Page, Network, and trace events.
  log_->AddEntry(level, log_message_json);
}

void PerformanceLogger::AddLogEntry(const std::string& webview,
                                    const std::string& method,
                                    const base::DictValue& params) {
  AddLogEntry(Log::kInfo, webview, method, params);
}

Status PerformanceLogger::EnableInspectorDomains(DevToolsClient* client) {
  std::vector<std::string> enable_commands;
  if (IsEnabled(prefs_.network)) {
    enable_commands.push_back("Network.enable");
  }
  for (const auto& enable_command : enable_commands) {
    base::DictValue params;  // All the enable commands have empty params.
    Status status = client->SendCommand(enable_command, params);
    if (status.IsError())
      return status;
  }
  return Status(kOk);
}

Status PerformanceLogger::HandleInspectorEvents(DevToolsClient* client,
                                                const std::string& method,
                                                const base::DictValue& params) {
  if (!ShouldLogEvent(method))
    return Status(kOk);

  AddLogEntry(client->GetId(), method, params);
  return Status(kOk);
}

Status PerformanceLogger::HandleTraceEvents(DevToolsClient* client,
                                            const std::string& method,
                                            const base::DictValue& params) {
  if (method == "Tracing.tracingComplete") {
    trace_buffering_ = false;
  } else if (method == "Tracing.dataCollected") {
    // The Tracing.dataCollected event contains a list of trace events.
    // Add each one as an individual log entry of method Tracing.dataCollected.
    const base::ListValue* traces = params.FindList("value");
    if (!traces) {
      return Status(kUnknownError,
                    "received DevTools trace data in unexpected format");
    }
    for (const auto& trace : *traces) {
      const base::DictValue* event_dict = trace.GetIfDict();
      if (!event_dict)
        return Status(kUnknownError, "trace event must be a dictionary");
      AddLogEntry(client->GetId(), "Tracing.dataCollected", *event_dict);
    }
  } else if (method == "Tracing.bufferUsage") {
    // 'value' will be between 0-1 and represents how full the DevTools trace
    // buffer is. If the buffer is full, warn the user.
    std::optional<double> maybe_buffer_usage = params.FindDouble("percentFull");
    if (!maybe_buffer_usage.has_value()) {
      // Tracing.bufferUsage event will occur once per second, and it really
      // only serves as a warning, so if we can't reliably tell whether the
      // buffer is full, just fail silently instead of spamming the logs.
      return Status(kOk);
    }
    if (maybe_buffer_usage.value() >= 0.99999) {
      base::DictValue error_params;
      std::string err("Chrome's trace buffer filled while collecting events, "
                      "so some trace events may have been lost");
      error_params.Set("error", err);
      // Expose error to client via perf log using same format as other entries.
      AddLogEntry(Log::kWarning,
                  DevToolsClientImpl::kBrowserwideDevToolsClientId,
                  "Tracing.bufferUsage", error_params);
      LOG(WARNING) << err;
    }
  }
  return Status(kOk);
}

Status PerformanceLogger::StartTrace() {
  if (!browser_client_) {
    return Status(kUnknownError, "tried to start tracing, but connection to "
                  "browser was not yet established");
  }
  if (trace_buffering_) {
    LOG(WARNING) << "tried to start tracing, but a trace was already started";
    return Status(kOk);
  }
  base::ListValue categories;
  const std::vector<std::string> str_list =
      base::SplitString(prefs_.trace_categories, ",", base::TRIM_WHITESPACE,
                        base::SPLIT_WANT_NONEMPTY);
  for (const std::string& str : str_list) {
    categories.Append(str);
  }
  base::DictValue params;
  params.SetByDottedPath("traceConfig.includedCategories",
                         std::move(categories));
  params.SetByDottedPath("traceConfig.recordingMode", "recordAsMuchAsPossible");
  // Ask DevTools to report buffer usage.
  params.Set("bufferUsageReportingInterval",
             prefs_.buffer_usage_reporting_interval);
  Status status = browser_client_->SendCommand("Tracing.start", params);
  if (status.IsError()) {
    LOG(ERROR) << "error when starting trace: " << status.message();
    return status;
  }
  trace_buffering_ = true;
  return Status(kOk);
}

Status PerformanceLogger::CollectTraceEvents() {
  if (!browser_client_) {
    return Status(kUnknownError, "tried to collect trace events, but "
                  "connection to browser was not yet established");
  }
  if (!trace_buffering_) {
    return Status(kUnknownError, "tried to collect trace events, but tracing "
                  "was not started");
  }

  base::DictValue params;
  Status status = browser_client_->SendCommand("Tracing.end", params);
  if (status.IsError()) {
    LOG(ERROR) << "error when stopping trace: " << status.message();
    return status;
  }

  // Block up to 30 seconds until Tracing.tracingComplete event is received.
  status = browser_client_->HandleEventsUntil(
      base::BindRepeating(&PerformanceLogger::IsTraceDone,
                          base::Unretained(this)),
      Timeout(base::Seconds(30)));
  if (status.IsError())
    return status;

  return StartTrace();
}

Status PerformanceLogger::IsTraceDone(bool* trace_done) const {
  *trace_done = !trace_buffering_;
  return Status(kOk);
}
