// Copyright 2023 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/web_ui_mocha_browser_test.h"

#include <optional>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/gtest_sub_test_results.h"
#include "base/test/gtest_tags.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/test_switches.h"
#include "chrome/test/base/web_ui_test_data_source.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/page_type.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/base/resource/resource_bundle.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/test/base/android/android_ui_test_utils.h"
#else
#include "chrome/test/base/ui_test_utils.h"
#endif

SubTestResult::SubTestResult() = default;
SubTestResult::SubTestResult(const SubTestResult& other) = default;
SubTestResult::SubTestResult(SubTestResult&& other)
    : name(std::move(other.name)),
      duration(other.duration),
      failure_reason(std::move(other.failure_reason)) {}
SubTestResult& SubTestResult::operator=(SubTestResult&& other) {
  name = std::move(other.name);
  duration = other.duration;
  failure_reason = std::move(other.failure_reason);
  return *this;
}
SubTestResult::~SubTestResult() = default;

namespace webui {

void CanonicalizeTestName(std::string* test_name) {
  if (!test_name) {
    return;
  }
  std::replace_if(
      test_name->begin(), test_name->end(),
      [](char c) {
        return !(base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '_');
      },
      '_');
}

std::tuple<bool, std::vector<SubTestResult>> ProcessMessagesFromJsTest(
    content::WebContents* web_contents) {
  content::DOMMessageQueue message_queue(web_contents);
  std::vector<SubTestResult> results;
  std::string message;
  while (message_queue.WaitForMessage(&message)) {
    if (message == "\"SUCCESS\"") {
      return std::make_tuple(true, results);
    } else if (message == "\"FAILURE\"") {
      return std::make_tuple(false, results);
    }

    std::optional<base::Value> msg =
        base::JSONReader::Read(message, base::JSON_PARSE_CHROMIUM_EXTENSIONS,
                               base::JSON_PARSE_CHROMIUM_EXTENSIONS);

    SubTestResult sub_test_result;
    std::string* test_name = msg->GetDict().FindString("fullTitle");
    CHECK(test_name);
    sub_test_result.name = *test_name;
    CanonicalizeTestName(&sub_test_result.name);

    std::optional<int> duration = msg->GetDict().FindInt("duration");
    CHECK(duration);
    sub_test_result.duration = *duration;

    std::string* failure_reason = msg->GetDict().FindString("failureReason");
    if (failure_reason) {
      sub_test_result.failure_reason.emplace(*failure_reason);
    }

    results.push_back(std::move(sub_test_result));
  }
  NOTREACHED();
}

}  // namespace webui

WebUIMochaBrowserTest::WebUIMochaBrowserTest()
    : test_loader_host_(chrome::kChromeUIWebUITestHost),
      test_loader_scheme_(content::kChromeUIScheme),
// XmlUnitTestResultPrinter is not supported on Android.
#if BUILDFLAG(IS_ANDROID)
      sub_test_reporter_(nullptr)
#else
      sub_test_reporter_(std::make_unique<SubTestReporter>())
#endif
{
}

WebUIMochaBrowserTest::~WebUIMochaBrowserTest() = default;

void WebUIMochaBrowserTest::set_test_loader_host(const std::string& host) {
  test_loader_host_ = host;
}

void WebUIMochaBrowserTest::set_test_loader_scheme(const std::string& scheme) {
  // Only chrome:// and chrome-untrusted:// are supported.
  CHECK(scheme == content::kChromeUIScheme ||
        scheme == content::kChromeUIUntrustedScheme);
  test_loader_scheme_ = scheme;
}

Profile* WebUIMochaBrowserTest::GetProfileForSetup() {
  return chrome_test_utils::GetProfile(this);
}

void WebUIMochaBrowserTest::SetUpOnMainThread() {
  // Load browser_tests.pak.
  base::FilePath pak_path;
#if BUILDFLAG(IS_ANDROID)
  // on Android all pak files are inside the paks folder.
  ASSERT_TRUE(base::PathService::Get(base::DIR_ANDROID_APP_DATA, &pak_path));
  pak_path = pak_path.Append(FILE_PATH_LITERAL("paks"));
#else
  ASSERT_TRUE(base::PathService::Get(base::DIR_ASSETS, &pak_path));
#endif  // BUILDFLAG(IS_ANDROID)
  pak_path = pak_path.AppendASCII("browser_tests.pak");
  ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
      pak_path, ui::kScaleFactorNone);

  // Register the chrome://webui-test data source.
  Profile* profile = GetProfileForSetup();
  if (test_loader_scheme_ == content::kChromeUIScheme) {
    webui::CreateAndAddWebUITestDataSource(profile);
  } else {
    // Must be untrusted
    webui::CreateAndAddUntrustedWebUITestDataSource(profile);
  }

#if !BUILDFLAG(IS_ANDROID)
  // Necessary setup for reporting code coverage metrics.
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kDevtoolsCodeCoverage)) {
    base::FilePath devtools_code_coverage_dir =
        command_line->GetSwitchValuePath(switches::kDevtoolsCodeCoverage);
    coverage_handler_ = std::make_unique<DevToolsAgentCoverageObserver>(
        devtools_code_coverage_dir, base::NullCallback(), test_loader_host_);
  }
#endif
}

void WebUIMochaBrowserTest::RunTest(const std::string& file,
                                    const std::string& trigger) {
  RunTest(file, trigger, /*skip_test_loader=*/false);
}

void WebUIMochaBrowserTest::OnWebContentsAvailable(
    content::WebContents* web_contents) {
  // Nothing to do here. Should be overridden by any subclasses if additional
  // setup steps are needed.
}

void WebUIMochaBrowserTest::RunTest(const std::string& file,
                                    const std::string& trigger,
                                    const bool& skip_test_loader) {
  // Construct URL to load the test module file.
  GURL url(skip_test_loader
               ? std::string(test_loader_scheme_ + "://" + test_loader_host_)
               : std::string(test_loader_scheme_ + "://" + test_loader_host_ +
                             "/test_loader.html?module=") +
                     file);

#if BUILDFLAG(IS_ANDROID)
  android_ui_test_utils::OpenUrlInNewTab(
      chrome_test_utils::GetProfile(this),
      chrome_test_utils::GetActiveWebContents(this), url);
#else
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
#endif
  content::WebContents* web_contents =
      chrome_test_utils::GetActiveWebContents(this);
  ASSERT_TRUE(web_contents);

  // Check that the navigation does not point to an error page like
  // chrome-error://chromewebdata/.
  bool is_error_page =
      web_contents->GetController().GetLastCommittedEntry()->GetPageType() ==
      content::PageType::PAGE_TYPE_ERROR;
  if (is_error_page) {
    FAIL() << "Navigation to '" << url.spec() << "' failed.";
  }

  // Hook for subclasses that need access to the WebContents before the Mocha
  // test runs.
  OnWebContentsAvailable(web_contents);

  ASSERT_TRUE(
      RunTestOnWebContents(web_contents, file, trigger, skip_test_loader));
}

testing::AssertionResult WebUIMochaBrowserTest::RunTestOnWebContents(
    content::WebContents* web_contents,
    const std::string& file,
    const std::string& trigger,
    const bool& skip_test_loader) {
  testing::AssertionResult result(testing::AssertionFailure());

  if (skip_test_loader) {
    // Perform setup steps normally done by test_loader.html.
    result = SimulateTestLoader(web_contents, file);
    if (!result) {
      return result;
    }
  }

  // Trigger the Mocha tests, and wait for completion.
  result = ExecJs(web_contents->GetPrimaryMainFrame(), trigger);
  if (!result) {
    return result;
  }

  // Receive messages from JS.
  auto [success, sub_test_results] =
      webui::ProcessMessagesFromJsTest(web_contents);

  // Report individual JS test results if reporting is enabled.
  if (sub_test_reporter_) {
    // ResultDB limits test identifiers to 512 bytes. However, GTest code isn't
    // privy to the exact schema used (for that, see TestResultsTracker::
    // SaveSummaryAsJSON). Here, it is simply assumed that test name length can
    // be estimated as a sum of the lengths of the GTest fixture name, GTest
    // test name, and Mocha JS test name, plus a few extra bytes for delimiters.
    // Retrieve GTest fixture name and GTest test name to build an estimate.
    const testing::TestInfo* info =
        testing::UnitTest::GetInstance()->current_test_info();
    CHECK(info);

    for (const auto& sub_test_result : sub_test_results) {
      // Estimate the final test identifier length. Allocate 3 bytes for
      // delimiters.
      size_t estimate = strlen(info->name()) + strlen(info->test_suite_name()) +
                        sub_test_result.name.size() + 3ul;

      if (estimate > 512ul) {
        testing::Message msg;
        msg << "Test name too long. Test identifier size estimate is "
            << estimate << ". ResultDB limits test identifiers to 512 bytes. "
            << "Please reduce total test name length by at least "
            << (estimate - 512ul) << " bytes.  name=\"" << info->name()
            << "\", test_suite_name=\"" << info->test_suite_name()
            << "\", js_test_name=\"" << sub_test_result.name << "\"";
        return testing::AssertionFailure(msg);
      }

      sub_test_reporter_->Report(sub_test_result.name, sub_test_result.duration,
                                 sub_test_result.failure_reason);
    }
  }

#if !BUILDFLAG(IS_ANDROID)
  // Report code coverage metrics.
  if (coverage_handler_ && coverage_handler_->CoverageEnabled()) {
    const std::string& full_test_name = base::StrCat({
        ::testing::UnitTest::GetInstance()
            ->current_test_info()
            ->test_suite_name(),
        ::testing::UnitTest::GetInstance()->current_test_info()->name(),
    });
    coverage_handler_->CollectCoverage(full_test_name);
  }
#endif

  if (!success) {
    testing::Message msg;
    msg << "Mocha test failures detected in file: " << file
        << ", triggered by '" << trigger << "'";
    return testing::AssertionFailure(msg);
  }

  return testing::AssertionSuccess();
}

void WebUIMochaBrowserTest::RunTestWithoutTestLoader(
    const std::string& file,
    const std::string& trigger) {
  RunTest(file, trigger, /*skip_test_loader=*/true);
}

void WebUIMochaBrowserTest::SetSubTestResultReportingEnabled(bool enabled) {
  if (enabled) {
    sub_test_reporter_ = std::make_unique<SubTestReporter>();
  } else {
    sub_test_reporter_.reset();
  }
}

testing::AssertionResult WebUIMochaBrowserTest::SimulateTestLoader(
    content::WebContents* web_contents,
    const std::string& file) {
  // Step 1: Programmatically loads mocha.js and mocha_adapter_simple.js.
  std::string loadMochaScript(base::StringPrintf(
      R"(
async function load() {
  await import('//%s/mocha.js');
  await import('//%s/mocha_adapter_simple.js');
}
load();
)",
      chrome::kChromeUIWebUITestHost, chrome::kChromeUIWebUITestHost));

  testing::AssertionResult result =
      ExecJs(web_contents->GetPrimaryMainFrame(), loadMochaScript);
  if (!result) {
    return result;
  }

  // Step 2: Programmatically loads the Mocha test file.
  std::string loadTestModuleScript(base::StringPrintf(
      "import('//%s/%s');", chrome::kChromeUIWebUITestHost, file.c_str()));
  return ExecJs(web_contents->GetPrimaryMainFrame(), loadTestModuleScript);
}

void WebUIMochaFocusTest::OnWebContentsAvailable(
    content::WebContents* web_contents) {
  // Focus the web contents before running the test, used for tests running as
  // interactive_ui_tests.
  web_contents->Focus();
}

void SubTestReporter::Report(
    std::string_view name,
    testing::TimeInMillis elapsed_time,
    std::optional<std::string_view> failure_message) const {
  base::AddSubTestResult(name, elapsed_time, failure_message);
}
