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

#include "content/public/test/test_devtools_protocol_client.h"

#include <memory>
#include <string_view>

#include "base/auto_reset.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/public/test/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/mojom/loader/mixed_content.mojom.h"

namespace content {

namespace {

const char kIdParam[] = "id";
const char kSessionIdParam[] = "sessionId";
const char kMethodParam[] = "method";
const char kParamsParam[] = "params";

}  // namespace

TestDevToolsProtocolClient::TestDevToolsProtocolClient() = default;
TestDevToolsProtocolClient::~TestDevToolsProtocolClient() = default;

const base::DictValue* TestDevToolsProtocolClient::SendSessionCommand(
    const std::string method,
    base::DictValue params,
    const std::string session_id,
    bool wait) {
  response_.clear();
  base::AutoReset<bool> reset_in_dispatch(&in_dispatch_, true);
  base::DictValue command;
  command.Set(kIdParam, ++last_sent_id_);
  command.Set(kMethodParam, std::move(method));
  if (params.size())
    command.Set(kParamsParam, std::move(params));
  if (!session_id.empty())
    command.Set(kSessionIdParam, std::move(session_id));

  std::string json_command = base::WriteJson(command).value_or("");
  agent_host_->DispatchProtocolMessage(this, base::as_byte_span(json_command));
  // Some messages are dispatched synchronously.
  // Only run loop if we are not finished yet.
  if (in_dispatch_ && wait)
    WaitForResponse();
  return result();
}

void TestDevToolsProtocolClient::WaitForResponse() {
  waiting_for_command_result_id_ = last_sent_id_;
  RunLoopUpdatingQuitClosure();
}

void TestDevToolsProtocolClient::AttachToFrameTreeHost(RenderFrameHost* frame) {
  FrameTreeNode* ftn =
      FrameTreeNode::GloballyFindByID(frame->GetFrameTreeNodeId());
  agent_host_ = RenderFrameDevToolsAgentHost::GetOrCreateFor(ftn);
  agent_host_->AttachClient(this);
}

void TestDevToolsProtocolClient::AttachToWebContents(WebContents* wc) {
  agent_host_ = DevToolsAgentHost::GetOrCreateFor(wc);
  agent_host_->AttachClient(this);
}

void TestDevToolsProtocolClient::AttachToTabTarget(WebContents* wc) {
  agent_host_ = DevToolsAgentHost::GetOrCreateForTab(wc);
  agent_host_->AttachClient(this);
}

void TestDevToolsProtocolClient::AttachToBrowserTarget() {
  // Tethering domain is not used in tests.
  agent_host_ = DevToolsAgentHost::CreateForBrowser(
      nullptr, DevToolsAgentHost::CreateServerSocketCallback());
  agent_host_->AttachClient(this);
}

bool TestDevToolsProtocolClient::HasExistingNotification(
    const std::string& search) const {
  return HasExistingNotificationMatching(
      [&search](const base::DictValue& notification) {
        return *notification.FindString(kMethodParam) == search;
      });
}

bool TestDevToolsProtocolClient::HasExistingNotificationMatching(
    base::FunctionRef<bool(const base::DictValue&)> pred) const {
  for (const auto& notification : notifications_) {
    if (pred(notification)) {
      return true;
    }
  }
  return false;
}

base::DictValue TestDevToolsProtocolClient::WaitForNotification(
    const std::string& notification,
    bool allow_existing) {
  if (allow_existing) {
    for (auto it = notifications_.begin(); it != notifications_.end(); ++it) {
      if (*it->FindString(kMethodParam) != notification)
        continue;
      base::DictValue result;
      if (base::DictValue* params = it->FindDict(kParamsParam)) {
        result = std::move(*params);
      }
      notifications_.erase(it);
      return result;
    }
  }

  waiting_for_notification_ = notification;
  RunLoopUpdatingQuitClosure();
  return std::move(received_notification_params_);
}

base::DictValue TestDevToolsProtocolClient::WaitForMatchingNotification(
    const std::string& notification,
    const NotificationMatcher& matcher) {
  for (auto it = notifications_.begin(); it != notifications_.end(); ++it) {
    if (*it->FindString(kMethodParam) != notification)
      continue;
    base::Value* params = it->Find(kParamsParam);
    if (!params || !matcher.Run(params->GetDict()))
      continue;
    base::DictValue result = std::move(*params).TakeDict();
    notifications_.erase(it);
    return result;
  }

  waiting_for_notification_ = notification;
  waiting_for_notification_matcher_ = matcher;
  RunLoopUpdatingQuitClosure();
  return std::move(received_notification_params_);
}

const base::DictValue* TestDevToolsProtocolClient::result() const {
  return response_.FindDict("result");
}

const base::DictValue* TestDevToolsProtocolClient::error() const {
  return response_.FindDict("error");
}

void TestDevToolsProtocolClient::RunLoopUpdatingQuitClosure() {
  base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
  run_loop_quit_closure_ = run_loop.QuitClosure();
  run_loop.Run();
}

void TestDevToolsProtocolClient::DispatchProtocolMessage(
    DevToolsAgentHost* agent_host,
    base::span<const uint8_t> message) {
  std::string_view message_str(reinterpret_cast<const char*>(message.data()),
                               message.size());
  base::Value parsed = *base::JSONReader::Read(
      message_str, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (std::optional<int> id = parsed.GetDict().FindInt("id")) {
    received_responses_count_++;
    response_ = std::move(parsed).TakeDict();
    in_dispatch_ = false;
    if (*id && *id == waiting_for_command_result_id_) {
      waiting_for_command_result_id_ = 0;
      std::move(run_loop_quit_closure_).Run();
    }
  } else {
    const std::string* notification = parsed.GetDict().FindString("method");
    if (waiting_for_notification_ != *notification) {
      notifications_.push_back(std::move(parsed).TakeDict());
      return;
    }
    base::Value* params = parsed.GetDict().Find(kParamsParam);
    if (waiting_for_notification_matcher_.is_null() ||
        waiting_for_notification_matcher_.Run(params->GetDict())) {
      waiting_for_notification_ = std::string();
      waiting_for_notification_matcher_ = NotificationMatcher();
      received_notification_params_ = std::move(*params).TakeDict();
      std::move(run_loop_quit_closure_).Run();
    }
  }
}

void TestDevToolsProtocolClient::AgentHostClosed(
    DevToolsAgentHost* agent_host) {
  if (!agent_host_can_close_) {
    NOTREACHED();
  }
}

bool TestDevToolsProtocolClient::AllowUnsafeOperations() {
  return allow_unsafe_operations_;
}

bool TestDevToolsProtocolClient::IsTrusted() {
  return is_trusted_;
}

bool TestDevToolsProtocolClient::MayReadLocalFiles() {
  return may_read_local_files_;
}

bool TestDevToolsProtocolClient::MayWriteLocalFiles() {
  return may_write_local_files_;
}

bool TestDevToolsProtocolClient::MayAttachToURL(const GURL& url,
                                                bool is_webui) {
  return not_attachable_hosts_.find(url.GetHost()) ==
         not_attachable_hosts_.end();
}

std::optional<url::Origin>
TestDevToolsProtocolClient::GetNavigationInitiatorOrigin() {
  return navigation_initiator_origin_;
}

}  // namespace content
