# 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.

from __future__ import annotations

import contextlib
import io
import json
from typing import TYPE_CHECKING
from unittest import mock

import pytest

import crossbench.browsers.all as all_browsers
from crossbench import plt
from crossbench.cli.cli import CrossBenchCLI
from tests import test_helper
from tests.end2end.conftest import mock_patch_chrome_stable

if TYPE_CHECKING:
  import pathlib

  from tests.test_helper import TestEnv


class SysExitException(Exception):

  def __init__(self):
    super().__init__("sys.exit")


def _run_cli(*args: str,
             extra_flags: tuple[str, ...] = (),
             test_env: TestEnv | None = None,
             auto_headless: bool = False) -> tuple[CrossBenchCLI, io.StringIO]:
  if test_env is not None:
    args += (f"--out-dir={test_env.results_dir}", *test_env.cq_flags)
  if auto_headless and not plt.PLATFORM.has_display and ("--headless"
                                                         not in args):
    args += ("--headless",)
    args = tuple(arg for arg in args if not arg.startswith("--viewport="))
  args += extra_flags
  cli = CrossBenchCLI()
  with contextlib.redirect_stdout(io.StringIO()) as stdout, mock.patch(
      "sys.exit", side_effect=SysExitException):
    cli.run(args)
  return cli, stdout


def _get_browser_dirs(results_dir: pathlib.Path) -> list[pathlib.Path]:
  assert results_dir.is_dir()
  browser_dirs = []
  for path in results_dir.iterdir():
    if not path.is_dir():
      continue
    if path.is_symlink():
      continue
    if path.name in ("runs", "sessions"):
      continue
    browser_dirs.append(path)
  return browser_dirs


def _get_v8_log_files(results_dir: pathlib.Path) -> list[pathlib.Path]:
  return list(results_dir.glob("**/*-v8.log"))


@pytest.mark.xdist_group("end2end-benchmark")
def test_speedometer_2_0(test_env: TestEnv, browser_path) -> None:
  # - Speedometer 2.0
  # - Speedometer --iterations flag
  # - Tracing probe with inline args
  # - --browser-config
  with pytest.raises(SysExitException):
    _run_cli("speedometer_2.0", "--help")
  _run_cli("describe", "benchmark", "speedometer_2.0")
  browser_config = test_env.root_dir / "config/doc/browser.config.hjson"
  assert browser_config.is_file()
  results_dir = test_env.results_dir
  assert not results_dir.exists()
  with mock_patch_chrome_stable(browser_path):
    _run_cli(
        "sp_2.0",
        f"--browser-config={browser_config}",
        "--iterations=2",
        "--stories=jQuery-TodoMVC",
        "--env-validation=skip",
        "--probe=tracing:{preset:'minimal'}",
        test_env=test_env,
        auto_headless=True)


@pytest.mark.xdist_group("end2end-benchmark")
def test_speedometer_2_1(test_env: TestEnv, test_chrome_name) -> None:
  # - Speedometer 2.1
  # - Story filtering with regexp
  # - V8 probes
  # - minimal splashscreen
  # - inline probe arguments
  with pytest.raises(SysExitException):
    _run_cli("speedometer_2.1", "--help")
  _run_cli("describe", "benchmark", "speedometer_2.1")
  _run_cli(
      "sp2.1",
      f"--browser={test_chrome_name}",
      "--splashscreen=minimal",
      "--iterations=2",
      "--env-validation=skip",
      "--stories=.*Vanilla.*",
      # V8 --prof doesn't always work on linux, skip it.
      "--probe=v8.log:"
      "{log_all:false, js_flags:['--log-maps'], prof:false, profview:false}",
      "--probe=v8.turbolizer",
      "--debug",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1
  v8_log_files = _get_v8_log_files(test_env.results_dir)
  assert len(v8_log_files) > 1


@pytest.mark.skip_mock
@pytest.mark.flaky(retries=3, delay=5)
def test_speedometer_2_1_custom_chrome_download(test_env: TestEnv) -> None:
  # - Custom chrome version downloads
  # - headless
  # Flaky due to downloading live custom builds.
  # TODO: speed up --browser=chrome-M111 and add it.
  _run_cli(
      "sp2.1",
      "--browser=chrome-M113-any",
      "--browser=chrome-111.0.5563.110",
      "--browser=chrome-latest-dev",
      "--headless",
      "--iterations=1",
      "--env-validation=skip",
      "--stories=.*Vanilla.*",
      "--debug",
      test_env=test_env)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 3
  v8_log_files = _get_v8_log_files(test_env.results_dir)
  assert not v8_log_files


@pytest.mark.xdist_group("end2end-benchmark")
def test_speedometer_2_1_chrome_safari(test_env: TestEnv, driver_path,
                                       test_chrome_name) -> None:
  # - Speedometer 3
  # - Merging stories over multiple iterations and browsers
  # - Testing safari
  # - --verbose flag
  # - no splashscreen
  # This fails on the CQ bot, so make sure we skip it there:
  if driver_path:
    pytest.skip("Skipping test on CQ.")
  if not plt.PLATFORM.is_macos:
    return
  platform = plt.PLATFORM
  if not platform.is_macos and (not platform.exists(
      all_browsers.Safari.default_path(platform))):
    pytest.skip("Test requires Safari, skipping on non macOS devices.")
  _run_cli(
      "sp2.1",
      f"--browser={test_chrome_name}",
      "--browser=safari",
      "--splashscreen=none",
      "--iterations=1",
      "--repeat=2",
      "--env-validation=skip",
      "--verbose",
      "--stories=.*React.*",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 2
  v8_log_files = _get_v8_log_files(test_env.results_dir)
  assert not v8_log_files


@pytest.mark.xdist_group("end2end-benchmark")
def test_jetstream_2_0(test_env: TestEnv, test_chrome_name) -> None:
  # - jetstream 2.0
  # - merge / run separate stories
  # - custom multiple --js-flags
  # - custom viewport
  # - quiet flag
  with pytest.raises(SysExitException):
    _run_cli("jetstream_2.0", "--help")
  _run_cli("describe", "--json", "benchmark", "jetstream_2.0")
  _run_cli(
      "jetstream_2.0",
      f"--browser={test_chrome_name}",
      "--separate",
      "--repeat=2",
      "--env-validation=skip",
      "--viewport=maximised",
      "--stories=.*date-format.*",
      "--quiet",
      "--js-flags=--log,--log-opt,--log-deopt",
      extra_flags=(
          "--",
          "--no-sandbox",
      ),
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1


@pytest.mark.xdist_group("end2end-benchmark")
def test_jetstream_2_1(test_env: TestEnv, test_chrome_name) -> None:
  # - jetstream 2.1
  # - custom --time-unit
  # - explicit single story
  # - custom splashscreen
  # - custom viewport
  # - --probe-config
  with pytest.raises(SysExitException):
    _run_cli("jetstream_2.1", "--help")
  _run_cli("describe", "benchmark", "jetstream_2.1")
  probe_config = test_env.root_dir / "config/doc/probe.config.hjson"
  assert probe_config.is_file()
  _run_cli(
      "jetstream_2.1",
      f"--browser={test_chrome_name}",
      "--env-validation=skip",
      "--splashscreen=http://google.com",
      "--viewport=900x800",
      "--stories=Box2D",
      "--time-unit=0.9",
      f"--probe-config={probe_config}",
      "--throw",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1
  v8_log_files = _get_v8_log_files(test_env.results_dir)
  assert len(v8_log_files) > 1


@pytest.mark.xdist_group("end2end-benchmark")
def test_jetstream_2_2(test_env: TestEnv, test_chrome_name) -> None:
  # - jetstream 2.2
  # - custom --time-unit
  # - explicit single story
  # - custom splashscreen
  # - custom viewport
  # - --probe-config
  with pytest.raises(SysExitException):
    _run_cli("jetstream_2.2", "--help")
  _run_cli("describe", "benchmark", "jetstream_2.2")
  probe_config = test_env.root_dir / "config/doc/probe.config.hjson"
  assert probe_config.is_file()
  _run_cli(
      "jetstream_2.2",
      f"--browser={test_chrome_name}",
      "--env-validation=skip",
      "--splashscreen=http://google.com",
      "--viewport=900x800",
      "--stories=Box2D",
      "--time-unit=0.9",
      f"--probe-config={probe_config}",
      "--throw",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1
  v8_log_files = _get_v8_log_files(test_env.results_dir)
  assert len(v8_log_files) > 1


@pytest.mark.xdist_group("end2end-benchmark")
def test_loading(test_env: TestEnv, test_chrome_name) -> None:
  # - loading using named pages with timeouts
  # - custom cooldown time
  # - custom viewport
  # - performance.mark probe
  with pytest.raises(SysExitException):
    _run_cli("loading", "--help")
  _run_cli("describe", "benchmark", "loading")
  _run_cli(
      "loading",
      f"--browser={test_chrome_name}",
      "--env-validation=skip",
      "--viewport=headless",
      "--stories=wikipedia",
      "--cool-down-time=2.5",
      "--probe=performance.entries",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1


def test_loading_page_config(test_env: TestEnv, test_chrome_name) -> None:
  # - loading with config file
  page_config = test_env.root_dir / "config/doc/page.config.hjson"
  assert page_config.is_file()
  _run_cli(
      "loading",
      f"--browser={test_chrome_name}",
      "--env-validation=skip",
      f"--page-config={page_config}",
      "--probe=performance.entries",
      "--no-splash",
      "--cool-down-time=0",
      "--throw",
      test_env=test_env,
      auto_headless=True)


@pytest.mark.xdist_group("end2end-benchmark")
def test_loading_playback_urls(test_env: TestEnv, test_chrome_name) -> None:
  # - loading using url
  # - combined pages and --playback controller
  _run_cli(
      "loading",
      f"--browser={test_chrome_name}",
      "--env-validation=skip",
      "--verbose-driver",
      "--playback=5.3s",
      "--viewport=fullscreen",
      "--stories=http://google.com,0.5,http://bing.com,0.4",
      "--probe=performance.entries",
      test_env=test_env,
      auto_headless=True)


@pytest.mark.xdist_group("end2end-benchmark")
@pytest.mark.skipif(
    plt.PLATFORM.is_win, reason="Fails on Windows; crbug.com/463323491")
def test_loading_playback(test_env: TestEnv, test_chrome_name) -> None:
  # - loading using named pages with timeouts
  # - separate pages and --playback controller
  # - viewport-size via chrome flag
  args = [
      "loading",
      f"--browser={test_chrome_name}",
      "--env-validation=skip",
      "--playback=5.3s",
      "--separate",
      "--stories=twitter,2,facebook,0.4",
      "--probe=performance.entries",
      "--debug",
  ]
  if not plt.PLATFORM.is_linux:
    args.extend([
        "--",
        "--window-size=900,500",
        "--window-position=150,150",
    ])
  _run_cli(*args, test_env=test_env, auto_headless=True)


@pytest.mark.skipif(
    not plt.PLATFORM.has_display, reason="Firefox cannot run headless")
@pytest.mark.xdist_group("end2end-benchmark")
def test_loading_playback_firefox(test_env: TestEnv, test_chrome_name) -> None:
  # - loading using named pages with timeouts
  # - --playback controller
  # - Firefox
  platform = plt.PLATFORM
  try:
    if not platform.exists(all_browsers.Firefox.default_path(platform)):
      pytest.skip("Test requires Firefox.")
  except Exception:  # noqa: BLE001
    pytest.skip("Test requires Firefox.")
  _run_cli(
      "loading",
      f"--browser={test_chrome_name}",
      "--browser=ff",
      "--env-validation=skip",
      "--playback=2x",
      "--stories=twitter,1,facebook,0.4",
      "--probe=performance.entries",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 2


@pytest.mark.xdist_group("end2end-benchmark")
@pytest.mark.skipif(
    plt.PLATFORM.is_win,
    reason="stdout forwarding is not always supported on windows")
def test_chrome_stdout_logging(test_env: TestEnv) -> None:
  # - loading inline hjson
  # - executing custom JS
  # - validating chrome browser stdout using generated content to make sure it
  #   is not just in the driver log as part of the script source.
  out_dir = test_env.results_dir
  assert not list(out_dir.glob("**/*.stdout.log"))
  page_config = {
      "pages": {
          "STDOUT TEST": [{
              "action": "get",
              "url": "https://www.google.com"
          }, {
              "action": "js",
              "script": "%DebugPrint('TestOutput'.repeat(3))"
          }]
      }
  }
  _run_cli(
      "loading",
      "--browser=chrome",
      "--env-validation=skip",
      "--js-flags=--allow-natives-syntax",
      "--fast",
      f"--page-config={json.dumps(page_config)}",
      test_env=test_env,
      auto_headless=True)

  stdout_files = list(out_dir.glob("**/*.stdout.log"))
  assert len(stdout_files) == 1
  stdout_file = stdout_files[0]
  test_output = "TestOutput" * 3
  assert test_output in stdout_file.read_text()


@pytest.mark.xdist_group("end2end-benchmark")
def test_devtools_frontend_all(test_env: TestEnv, test_chrome_name,
                               test_chrome_version) -> None:
  if test_chrome_version < 144:
    pytest.skip("Skipping test for Chrome versions below 144; "
                "CDP command may not be supported")
  pytest.skip("Skip until b/487909749 is addressed")
  _run_cli(
      "devtools_frontend",
      f"--browser={test_chrome_name}",
      test_env=test_env,
      auto_headless=True)
  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1


@pytest.mark.xdist_group("end2end-benchmark")
def test_devtools_frontend_selection(test_env: TestEnv, test_chrome_name,
                                     test_chrome_version) -> None:
  if test_chrome_version < 144:
    pytest.skip("Skipping test for Chrome versions below 144; "
                "CDP command may not be supported")
  pytest.skip("Skip until b/487909749 is addressed")
  _run_cli(
      "devtools_frontend",
      f"--browser={test_chrome_name}",
      "--sites=blank,speedometertests",
      "--panels=elements,resources",
      test_env=test_env,
      auto_headless=True)

  browser_dirs = _get_browser_dirs(test_env.results_dir)
  assert len(browser_dirs) == 1


if __name__ == "__main__":
  test_helper.run_pytest(__file__)
