// 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/base/ash/extension_js_browser_test.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_extension_constants.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/run_until.h"
#include "base/values.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/ash/javascript_browser_test.h"
#include "chrome/test/base/test_switches.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
#include "extensions/browser/extension_registry_test_helper.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "ui/base/ime/ash/extension_ime_util.h"

namespace {

const std::vector<std::string>& GetExtensionIdsToCollectCoverage() {
  static const std::vector<std::string> extensions_for_coverage = {
      extension_misc::kChromeVoxExtensionId,
      extension_misc::kSelectToSpeakExtensionId,
      extension_misc::kSwitchAccessExtensionId,
      extension_misc::kAccessibilityCommonExtensionId,
      extension_misc::kEnhancedNetworkTtsExtensionId,
      ash::extension_ime_util::kBrailleImeExtensionId,
  };
  return extensions_for_coverage;
}

}  // namespace

ExtensionJSBrowserTest::ExtensionJSBrowserTest() = default;

ExtensionJSBrowserTest::~ExtensionJSBrowserTest() = default;

void ExtensionJSBrowserTest::SetUpOnMainThread() {
  JavaScriptBrowserTest::SetUpOnMainThread();
  content::BrowserAccessibilityState::GetInstance()
      ->SetActivationFromPlatformEnabled(true);

  // Set up coverage collection.
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kDevtoolsCodeCoverage)) {
    base::FilePath devtools_code_coverage_dir =
        command_line->GetSwitchValuePath(switches::kDevtoolsCodeCoverage);
    ShouldInspectDevToolsAgentHostCallback callback =
        base::BindRepeating([](content::DevToolsAgentHost* host) {
          const auto& ext_ids = GetExtensionIdsToCollectCoverage();
          for (const auto& ext_id : ext_ids) {
            if (host->GetURL().GetPath().contains(ext_id) &&
                host->GetType() == "background_page") {
              return true;
            }
          }
          return false;
        });
    coverage_handler_ = std::make_unique<DevToolsAgentCoverageObserver>(
        devtools_code_coverage_dir, std::move(callback));
  }
}

void ExtensionJSBrowserTest::WaitForExtension(const char* extension_id,
                                              base::OnceClosure load_cb) {
  extension_id_ = extension_id;

  // ExtensionHosts only exist when there is an associated RenderFrame.
  // Initialize both an ExtensionHostTestHelper and a ServiceWorkerObserver
  // before running the load callback, to avoid missing the relevant event.
  extensions::ExtensionHostTestHelper host_helper(GetProfile(), extension_id);
  extensions::ExtensionRegistryTestHelper observer(extension_id, GetProfile());
  std::move(load_cb).Run();

  if (observer.WaitForManifestVersion() == 3) {
    observer.WaitForServiceWorkerStart();
    extension_host_browser_context_ = GetProfile();

    // Wait until the extension is registered by the ProcessManager (this
    // happens asynchronously) - otherwise, we won't be able to run a script
    // in the service worker context.
    ASSERT_TRUE(base::test::RunUntil([&]() {
      std::vector<extensions::WorkerId> worker_ids =
          extensions::ProcessManager::Get(GetProfile())
              ->GetServiceWorkersForExtension(extension_id);
      return !worker_ids.empty();
    }));
    return;
  }

  extensions::ExtensionHost* extension_host =
      host_helper.WaitForHostCompletedFirstLoad();
  ASSERT_TRUE(extension_host);
  extension_host_browser_context_ = extension_host->browser_context();
}

bool ExtensionJSBrowserTest::RunJavascriptTestF(bool is_async,
                                                const std::string& test_fixture,
                                                const std::string& test_name) {
  if (!extension_host_browser_context_) {
    ADD_FAILURE() << "ExtensionHost failed to start";
    return false;
  }
  std::vector<std::u16string> scripts;

  base::DictValue test_runner_params;
  if (embedded_test_server()->Started()) {
    test_runner_params.Set("testServerBaseUrl",
                           embedded_test_server()->base_url().spec());
  }

  if (!libs_loaded_) {
    if (!BuildJavascriptLibraries(&scripts)) {
      ADD_FAILURE() << "Failed to build JavaScript libraries";
      return false;
    }
    libs_loaded_ = true;
  }

  scripts.push_back(base::UTF8ToUTF16(content::JsReplace(
      "const testRunnerParams = $1;", std::move(test_runner_params))));

  scripts.push_back(BuildRunTestJSCall(
      is_async, "RUN_TEST_F",
      base::ListValue().Append(test_fixture).Append(test_name)));

  std::u16string script_16 = base::JoinString(scripts, u"\n");
  std::string script = base::UTF16ToUTF8(script_16);

  auto result = extensions::BackgroundScriptExecutor::ExecuteScript(
      extension_host_browser_context_, extension_id_, script,
      extensions::BackgroundScriptExecutor::ResultCapture::kSendScriptResult);

  if (!result.is_string())
    return false;

  if (coverage_handler_ && coverage_handler_->CoverageEnabled()) {
    const std::string& full_test_name = base::StrCat({test_fixture, test_name});
    coverage_handler_->CollectCoverage(full_test_name);
  }

  std::string result_str = result.GetString();
  std::optional<base::Value> value_result =
      base::JSONReader::Read(result_str, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  const base::DictValue& dict_value = value_result->GetDict();

  bool test_result = dict_value.FindBool("result").value();
  const std::string* test_result_message = dict_value.FindString("message");
  CHECK(test_result_message);
  if (!test_result_message->empty()) {
    ADD_FAILURE() << *test_result_message;
  }
  return test_result;
}
