// 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 "chrome/browser/vr/test/xr_browser_test.h"

#include <cstring>

#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/debug/debugger.h"
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test_utils.h"
#include "device/vr/buildflags/buildflags.h"
#include "third_party/blink/public/common/features_generated.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/path_utils.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#else
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#endif

#if BUILDFLAG(ENABLE_VR)
#include "device/vr/public/cpp/features.h"
#endif
namespace vr {

constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalShort;
constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalLong;
constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutShort;
constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutMedium;
constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutLong;
constexpr char XrBrowserTestBase::kOpenXrConfigPathEnvVar[];
constexpr char XrBrowserTestBase::kOpenXrConfigPathVal[];
constexpr char XrBrowserTestBase::kTestFileDir[];
constexpr char XrBrowserTestBase::kSwitchIgnoreRuntimeRequirements[];
const std::vector<std::string> XrBrowserTestBase::kRequiredTestSwitches{
#if BUILDFLAG(IS_WIN)
    "enable-gpu", "enable-pixel-output-in-tests",
    "run-through-xr-wrapper-script", "enable-unsafe-swiftshader"
#endif
};
const std::vector<std::pair<std::string, std::string>>
    XrBrowserTestBase::kRequiredTestSwitchesWithValues{
#if BUILDFLAG(IS_WIN)
        std::pair<std::string, std::string>("test-launcher-jobs", "1")
#endif
    };

XrBrowserTestBase::XrBrowserTestBase() : env_(base::Environment::Create()) {
  enable_features_.push_back(features::kLogJsConsoleMessages);
#if BUILDFLAG(ENABLE_VR)
  enable_features_.push_back(device::features::kWebXrVisibleBlurred);
#if BUILDFLAG(IS_ANDROID)
  enable_features_.push_back(device::features::kWebXRLayers);
  enable_features_.push_back(blink::features::kWebXRMediaBinding);
#endif
#endif
}

XrBrowserTestBase::~XrBrowserTestBase() = default;

base::FilePath::StringType UTF8ToWideIfNecessary(std::string input) {
#if BUILDFLAG(IS_WIN)
  return base::UTF8ToWide(input);
#else
  return input;
#endif  // BUILDFLAG(IS_WIN)
}

std::string WideToUTF8IfNecessary(base::FilePath::StringType input) {
#if BUILDFLAG(IS_WIN)
  return base::WideToUTF8(input);
#else
  return input;
#endif  // BUILDFLAG(IS_WIN)
}

// Returns an std::string consisting of the given path relative to the test
// executable's path, e.g. if the executable is in out/Debug and the given path
// is "test", the returned string should be out/Debug/test.
std::string MakeExecutableRelative(const char* path) {
  base::FilePath executable_path;
#if BUILDFLAG(IS_ANDROID)
  NOTREACHED();
#else
  EXPECT_TRUE(
      base::PathService::Get(base::BasePathKey::FILE_EXE, &executable_path));
  executable_path = executable_path.DirName();
  // We need an std::string that is an absolute file path, which requires
  // platform-specific logic since Windows uses std::wstring instead of
  // std::string for FilePaths, but SetVar only accepts std::string.
  return WideToUTF8IfNecessary(
      base::MakeAbsoluteFilePath(
          executable_path.Append(base::FilePath(UTF8ToWideIfNecessary(path))))
          .value());
#endif
}

void XrBrowserTestBase::SetUp() {
  // Check whether the required flags were passed to the test - without these,
  // we can fail in ways that are non-obvious, so fail more explicitly here if
  // they aren't present.
  auto* cmd_line = base::CommandLine::ForCurrentProcess();
  for (auto req_switch : kRequiredTestSwitches) {
    ASSERT_TRUE(cmd_line->HasSwitch(req_switch))
        << "Missing switch " << req_switch << " required to run tests properly";
  }
  for (auto req_switch_pair : kRequiredTestSwitchesWithValues) {
    ASSERT_TRUE(cmd_line->HasSwitch(req_switch_pair.first))
        << "Missing switch " << req_switch_pair.first
        << " required to run tests properly";
    ASSERT_TRUE(cmd_line->GetSwitchValueASCII(req_switch_pair.first) ==
                req_switch_pair.second)
        << "Have required switch " << req_switch_pair.first
        << ", but not required value " << req_switch_pair.second;
  }

  // Get the set of runtime requirements to ignore.
  if (cmd_line->HasSwitch(kSwitchIgnoreRuntimeRequirements)) {
    auto reqs = cmd_line->GetSwitchValueASCII(kSwitchIgnoreRuntimeRequirements);
    if (reqs != "") {
      for (auto req : base::SplitString(
               reqs, ",", base::WhitespaceHandling::TRIM_WHITESPACE,
               base::SplitResult::SPLIT_WANT_NONEMPTY)) {
        ignored_requirements_.insert(req);
      }
    }
  }

  // Check whether we meet all runtime requirements for this test.
  XR_CONDITIONAL_SKIP_PRETEST(runtime_requirements_, ignored_requirements_,
                              &test_skipped_at_startup_)

  // OpenXr on Android cannot use the environment variable as the loader cannot
  // read it on Android at present.
#if !BUILDFLAG(IS_ANDROID)
  // Set the environment variable to use the mock OpenXR client.
  // If the kOpenXrConfigPathEnvVar environment variable is set, the OpenXR
  // loader will look for the OpenXR runtime specified in that json file. The
  // json file contains the path to the runtime, relative to the json file
  // itself. Otherwise, the OpenXR loader loads the active OpenXR runtime
  // installed on the system, which is specified by a registry key.
  ASSERT_TRUE(env_->SetVar(kOpenXrConfigPathEnvVar,
                           MakeExecutableRelative(kOpenXrConfigPathVal)))
      << "Failed to set OpenXR JSON location environment variable";
#endif

  // Set any command line flags that subclasses have set, e.g. enabling features
  // or specific runtimes.
  if (forced_runtime_.has_value() && !forced_runtime_->empty()) {
    cmd_line->AppendSwitchASCII(switches::kWebXrForceRuntime,
                                forced_runtime_.value());
  }

#if defined(MEMORY_SANITIZER)
  // Surprisingly enough, there is no constant for this.
  // TODO(crbug.com/40564748): Once generic wrapper scripts for tests are
  // supported, move the logic to avoid passing --enable-gpu to GN.
  if (cmd_line->HasSwitch("enable-gpu")) {
    LOG(WARNING) << "Ignoring --enable-gpu switch, which is incompatible with "
                    "MSan builds.";
    cmd_line->RemoveSwitch("enable-gpu");
  }
#endif

  scoped_feature_list_.InitWithFeatures(enable_features_, disable_features_);

  PlatformBrowserTest::SetUp();
}

void XrBrowserTestBase::TearDown() {
  if (test_skipped_at_startup_) {
    // Since we didn't complete startup, no need to do teardown, either. Doing
    // so can result in hitting a DCHECK.
    return;
  }
  PlatformBrowserTest::TearDown();
}

XrBrowserTestBase::RuntimeType XrBrowserTestBase::GetRuntimeType() const {
  return XrBrowserTestBase::RuntimeType::RUNTIME_NONE;
}

GURL XrBrowserTestBase::GetUrlForFile(const std::string& test_name) {
  // GetURL requires that the path start with /.
  return GetEmbeddedServer()->GetURL(std::string("/") + kTestFileDir +
                                     test_name + ".html");
}

net::EmbeddedTestServer* XrBrowserTestBase::GetEmbeddedServer() {
  if (server_ == nullptr) {
    server_ = std::make_unique<net::EmbeddedTestServer>(
        net::EmbeddedTestServer::Type::TYPE_HTTPS);
    // We need to serve from the root in order for the inclusion of the
    // test harness from //third_party to work.
    server_->ServeFilesFromSourceDirectory(".");
    EXPECT_TRUE(server_->Start()) << "Failed to start embedded test server";
  }
  return server_.get();
}

content::WebContents* XrBrowserTestBase::GetCurrentWebContents() {
#if !BUILDFLAG(IS_ANDROID)
  // `chrome_test_utils::GetActiveWebContents()` doesn't properly account for
  // the presence of an incognito browser, and only looks in the browser
  // returned by the base class, which doesn't get overridden by the incognito
  // browser.
  if (incognito_) {
    BrowserWindowInterface* incognito_browser =
        ProfileBrowserCollection::GetForProfile(
            browser()->GetProfile()->GetPrimaryOTRProfile(
                /*create_if_needed=*/false))
            ->FindTabbedBrowser();
    return incognito_browser->GetTabStripModel()->GetActiveWebContents();
  }
#endif
  return chrome_test_utils::GetActiveWebContents(this);
}

void XrBrowserTestBase::SetIncognito() {
  incognito_ = true;
  OpenNewTab(url::kAboutBlankURL);
}

void XrBrowserTestBase::OpenNewTab(const std::string& url) {
  OpenNewTab(url, incognito_);
}

void XrBrowserTestBase::OpenNewTab(const std::string& url, bool incognito) {
#if BUILDFLAG(IS_ANDROID)
  auto* profile = chrome_test_utils::GetProfile(this);
  if (incognito) {
    profile = profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  }

  TabModel* tab_model =
      TabModelList::GetTabModelForWebContents(GetCurrentWebContents());
  TabAndroid* first_tab = TabAndroid::FromWebContents(GetCurrentWebContents());
  std::unique_ptr<content::WebContents> contents =
      content::WebContents::Create(content::WebContents::CreateParams(profile));
  EXPECT_TRUE(content::NavigateToURL(contents.get(), GURL(url)));
  // TabModel takes ownership of the WebContents, so we release it here.
  tab_model->CreateTab(first_tab, std::move(contents), TabModel::kInvalidIndex,
                       TabModel::TabLaunchType::FROM_RECENT_TABS_FOREGROUND,
                       /*should_pin=*/false);
#else
  if (incognito) {
    OpenURLOffTheRecord(browser()->GetProfile(), GURL(url));
  } else {
    // -1 is a special index value used to append to the end of the tab list.
    chrome::AddTabAt(browser(), GURL(url), /*index=*/-1, /*foreground=*/true);
  }
#endif
}

void XrBrowserTestBase::CloseTab(content::WebContents* web_contents) {
#if BUILDFLAG(IS_ANDROID)
  TabModel* tab_model = TabModelList::GetTabModelForWebContents(web_contents);
  ASSERT_TRUE(tab_model);
  for (int i = 0; i < tab_model->GetTabCount(); ++i) {
    if (tab_model->GetWebContentsAt(i) == web_contents) {
      tab_model->CloseTabAt(i);
      return;
    }
  }
  ADD_FAILURE() << "Failed to find tab to close";
#else
  chrome::CloseWebContents(browser(), web_contents, /*add_to_history=*/false);
#endif
}

void XrBrowserTestBase::LoadFileAndAwaitInitialization(
    const std::string& test_name) {
  OnBeforeLoadFile();
  GURL url = GetUrlForFile(test_name);
  ASSERT_TRUE(content::NavigateToURL(GetCurrentWebContents(), url));
  ASSERT_TRUE(PollJavaScriptBoolean("isInitializationComplete()",
                                    kPollTimeoutMedium,
                                    GetCurrentWebContents()))
      << "Timed out waiting for JavaScript test initialization.";

#if BUILDFLAG(IS_WIN)
  // Now that the browser is opened and has focus, keep track of this window so
  // that we can restore the proper focus after entering each session. This is
  // required for tests that create multiple sessions to work properly.
  hwnd_ = GetForegroundWindow();
#endif
}

void XrBrowserTestBase::RunJavaScriptOrFail(
    const std::string& js_expression,
    content::WebContents* web_contents) {
  if (javascript_failed_) {
    LogJavaScriptFailure();
    return;
  }

  ASSERT_TRUE(content::ExecJs(web_contents, js_expression))
      << "Failed to run given JavaScript: " << js_expression;
}

bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail(
    const std::string& js_expression,
    content::WebContents* web_contents) {
  if (javascript_failed_) {
    LogJavaScriptFailure();
    return false;
  }

  DLOG(INFO) << "Run JavaScript: " << js_expression;
  return content::EvalJs(web_contents, js_expression).ExtractBool();
}

std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail(
    const std::string& js_expression,
    content::WebContents* web_contents) {
  if (javascript_failed_) {
    LogJavaScriptFailure();
    return "";
  }

  return content::EvalJs(web_contents, js_expression).ExtractString();
}

bool XrBrowserTestBase::PollJavaScriptBoolean(
    const std::string& bool_expression,
    const base::TimeDelta& timeout,
    content::WebContents* web_contents) {
  bool result = false;
  base::RunLoop wait_loop(base::RunLoop::Type::kNestableTasksAllowed);
  // Lambda used because otherwise BindRepeating gets confused about which
  // version of RunJavaScriptAndExtractBoolOrFail to use.
  BlockOnCondition(base::BindRepeating(
                       [](XrBrowserTestBase* base, std::string expression,
                          content::WebContents* contents) {
                         return base->RunJavaScriptAndExtractBoolOrFail(
                             expression, contents);
                       },
                       this, bool_expression, web_contents),
                   &result, &wait_loop, base::Time::Now(), timeout);
  wait_loop.Run();
  return result;
}

void XrBrowserTestBase::PollJavaScriptBooleanOrFail(
    const std::string& bool_expression,
    const base::TimeDelta& timeout,
    content::WebContents* web_contents) {
  ASSERT_TRUE(PollJavaScriptBoolean(bool_expression, timeout, web_contents))
      << "Timed out polling JavaScript boolean expression: " << bool_expression;
}

void XrBrowserTestBase::BlockOnCondition(
    base::RepeatingCallback<bool()> condition,
    bool* result,
    base::RunLoop* wait_loop,
    const base::Time& start_time,
    const base::TimeDelta& timeout,
    const base::TimeDelta& period) {
  if (!*result) {
    *result = condition.Run();
  }

  if (*result) {
    if (wait_loop->running()) {
      wait_loop->Quit();
      return;
    }
    // In the case where the condition is met fast enough that the given
    // RunLoop hasn't started yet, spin until it's available.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(&XrBrowserTestBase::BlockOnCondition,
                       base::Unretained(this), std::move(condition),
                       base::Unretained(result), base::Unretained(wait_loop),
                       start_time, timeout, period));
    return;
  }

  if (base::Time::Now() - start_time > timeout &&
      !base::debug::BeingDebugged()) {
    wait_loop->Quit();
    return;
  }

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&XrBrowserTestBase::BlockOnCondition,
                     base::Unretained(this), std::move(condition),
                     base::Unretained(result), base::Unretained(wait_loop),
                     start_time, timeout, period),
      period);
}

void XrBrowserTestBase::WaitOnJavaScriptStep(
    content::WebContents* web_contents) {
  // Make sure we aren't trying to wait on a JavaScript test step without the
  // code to do so.
  bool code_available = RunJavaScriptAndExtractBoolOrFail(
      "typeof javascriptDone !== 'undefined'", web_contents);
  ASSERT_TRUE(code_available) << "Attempted to wait on a JavaScript test step "
                              << "without the code to do so. You either forgot "
                              << "to import webxr_e2e.js or "
                              << "are incorrectly using a C++ function.";

  // Actually wait for the step to finish.
  bool success =
      PollJavaScriptBoolean("javascriptDone", kPollTimeoutLong, web_contents);

  // Check what state we're in to make sure javascriptDone wasn't called
  // because the test failed.
  XrBrowserTestBase::TestStatus test_status = CheckTestStatus(web_contents);
  if (!success || test_status == XrBrowserTestBase::TestStatus::STATUS_FAILED) {
    // Failure states: Either polling failed or polling succeeded, but because
    // the test failed.
    std::string reason;
    if (!success) {
      reason = "Timed out waiting for JavaScript step to finish.";
    } else {
      reason =
          "JavaScript testharness reported failure while waiting for "
          "JavaScript step to finish";
    }

    std::string result_string =
        RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
    if (result_string.empty()) {
      reason +=
          " Did not obtain specific failure reason from JavaScript "
          "testharness.";
    } else {
      reason +=
          " JavaScript testharness reported failure reason: " + result_string;
    }
    // Store that we've failed waiting for a JavaScript step so we can abort
    // further attempts to run JavaScript, which has the potential to do weird
    // things and produce non-useful output due to JavaScript code continuing
    // to run when it's in a known bad state.
    // This is a workaround for the fact that FAIL() and other gtest macros that
    // cause test failures only abort the current function. Thus, a failure here
    // will show up as a test failure, but there's nothing that actually stops
    // the test from continuing to run since FAIL() is not being called in the
    // main test body.
    javascript_failed_ = true;
    // Newlines to help the failure reason stick out.
    LOG(ERROR) << "\n\n\nvvvvvvvvvvvvvvvvv Useful Stack vvvvvvvvvvvvvvvvv\n\n";
    FAIL() << reason;
  }

  // Reset the synchronization boolean.
  RunJavaScriptOrFail("javascriptDone = false", web_contents);
}

void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function,
                                           content::WebContents* web_contents) {
  RunJavaScriptOrFail(step_function, web_contents);
  WaitOnJavaScriptStep(web_contents);
}

XrBrowserTestBase::TestStatus XrBrowserTestBase::CheckTestStatus(
    content::WebContents* web_contents) {
  std::string result_string =
      RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
  bool test_passed =
      RunJavaScriptAndExtractBoolOrFail("testPassed", web_contents);
  if (test_passed) {
    return XrBrowserTestBase::TestStatus::STATUS_PASSED;
  } else if (!test_passed && result_string.empty()) {
    return XrBrowserTestBase::TestStatus::STATUS_RUNNING;
  }
  // !test_passed && result_string != ""
  return XrBrowserTestBase::TestStatus::STATUS_FAILED;
}

void XrBrowserTestBase::EndTest(content::WebContents* web_contents) {
  switch (CheckTestStatus(web_contents)) {
    case XrBrowserTestBase::TestStatus::STATUS_PASSED:
      break;
    case XrBrowserTestBase::TestStatus::STATUS_FAILED:
      FAIL() << "JavaScript testharness failed with reason: "
             << RunJavaScriptAndExtractStringOrFail("resultString",
                                                    web_contents);
    case XrBrowserTestBase::TestStatus::STATUS_RUNNING:
      FAIL() << "Attempted to end test in C++ without finishing in JavaScript.";
    default:
      FAIL() << "Received unknown test status.";
  }
}

void XrBrowserTestBase::AssertNoJavaScriptErrors(
    content::WebContents* web_contents) {
  if (CheckTestStatus(web_contents) ==
      XrBrowserTestBase::TestStatus::STATUS_FAILED) {
    FAIL() << "JavaScript testharness failed with reason: "
           << RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
  }
}

void XrBrowserTestBase::RunJavaScriptOrFail(const std::string& js_expression) {
  RunJavaScriptOrFail(js_expression, GetCurrentWebContents());
}

bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail(
    const std::string& js_expression) {
  return RunJavaScriptAndExtractBoolOrFail(js_expression,
                                           GetCurrentWebContents());
}

std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail(
    const std::string& js_expression) {
  return RunJavaScriptAndExtractStringOrFail(js_expression,
                                             GetCurrentWebContents());
}

bool XrBrowserTestBase::PollJavaScriptBoolean(
    const std::string& bool_expression,
    const base::TimeDelta& timeout) {
  return PollJavaScriptBoolean(bool_expression, timeout,
                               GetCurrentWebContents());
}

void XrBrowserTestBase::PollJavaScriptBooleanOrFail(
    const std::string& bool_expression,
    const base::TimeDelta& timeout) {
  PollJavaScriptBooleanOrFail(bool_expression, timeout,
                              GetCurrentWebContents());
}

void XrBrowserTestBase::WaitOnJavaScriptStep() {
  WaitOnJavaScriptStep(GetCurrentWebContents());
}

void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function) {
  ExecuteStepAndWait(step_function, GetCurrentWebContents());
}

void XrBrowserTestBase::EndTest() {
  EndTest(GetCurrentWebContents());
}

void XrBrowserTestBase::AssertNoJavaScriptErrors() {
  AssertNoJavaScriptErrors(GetCurrentWebContents());
}

void XrBrowserTestBase::LogJavaScriptFailure() {
  LOG(ERROR) << "HEY! LISTEN! Not running requested JavaScript due to previous "
                "failure. Failures below this are likely garbage. Look for the "
                "useful stack above.";
}

}  // namespace vr
