# Copyright 2024 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 logging
import pathlib
import re
import unittest

import pytest
import tomllib
from tabulate import tabulate

from tests import test_helper

RUN_SNIPPET = """
if __name__ == "__main__":
  test_helper.run_pytest(__file__)
""".strip()
FUTURE_ANNOTATIONS_SNIPPET = "from __future__ import annotations"

COMMENTS_ONLY_RE = re.compile(r"^(?:#.*|\s*)*$", re.MULTILINE)

UNITTEST_DIR = pathlib.Path(__file__).parent
ROOT_DIR = UNITTEST_DIR.parents[1]
CROSSBENCH_DIR = ROOT_DIR / "crossbench"


class MetaTestCase(unittest.TestCase):

  def test_unittest_runner_snippet(self):
    # - All unittests files must end with the snippet for the CQ to pick it up.
    # - pytest files (in end2end) use a different approach that doesn't rely
    #   on a per-file runner
    for test_file in UNITTEST_DIR.glob("**/test_*.py"):
      with self.subTest(test_file=str(test_file)):
        self.assertTrue(
            test_file.read_text().rstrip().endswith(RUN_SNIPPET),
            f"{test_file} misses runner snippet: "
            "test_helper.run_pytest(__file__)")

  def test_future_annotation(self):
    for py_file in CROSSBENCH_DIR.glob("**/*.py"):
      with self.subTest(py_file=str(py_file)):
        text = py_file.read_text()
        if FUTURE_ANNOTATIONS_SNIPPET in text:
          continue
        if "pytype: skip-file" in text:
          continue
        if py_file.name == "__init__.py" and COMMENTS_ONLY_RE.fullmatch(text):
          continue
        self.fail(f"{py_file} is missing future annotation")

  @pytest.mark.xfail
  def test_vpython_poetry_version_match(self):
    vpython_content = (ROOT_DIR / ".vpython3").read_text()
    poetry_lock_content = (ROOT_DIR / "poetry.lock").read_text()

    vpython_re = re.compile(
        r"name: \"infra/python/wheels/(?P<name>[^/\"]+).*?"
        r"version: \"version:(?P<version>[^\"]+)\"", re.DOTALL)
    poetry_re = re.compile(
        r'name = "(?P<name>[^"]+)".*?'
        r'version = "(?P<version>[^"]+)"', re.DOTALL)

    vpython_packages = {}
    for wheel_block in vpython_content.split("wheel: <"):
      if not wheel_block.strip():
        continue
      match = vpython_re.search(wheel_block)
      if match:
        name = match.group("name").split("-py")[0]
        version = match.group("version").split(".chromium")[0]
        vpython_packages[name] = version

    poetry_packages = {}
    for package_block in poetry_lock_content.split("[[package]]"):
      if not package_block.strip():
        continue
      match = poetry_re.search(package_block)
      if match:
        name = match.group("name")
        version = match.group("version")
        poetry_packages[name] = version

    self.assertGreater(
        len(vpython_packages), 0, "No packages found in .vpython3")
    self.assertGreater(
        len(poetry_packages), 0, "No packages found in poetry.lock")

    vpython_only = sorted(vpython_packages.keys() - poetry_packages.keys())
    if vpython_only:
      logging.warning("Packages only in .vpython3: %s", ", ".join(vpython_only))
    poetry_only = sorted(poetry_packages.keys() - vpython_packages.keys())
    if poetry_only:
      logging.warning("Packages only in poetry.lock: %s",
                      ", ".join(poetry_only))

    mismatches = []
    for name, vpython_version_str in vpython_packages.items():
      if name not in poetry_packages:
        continue
      poetry_version_str = poetry_packages[name]
      if vpython_version_str == poetry_version_str:
        continue
      mismatches.append((name, vpython_version_str, poetry_version_str))
    mismatches.sort(key=lambda row: row[0])
    if mismatches:
      headers = ["Package", ".vpython3", "poetry.lock"]
      self.fail("Version mismatches found:\n" +
                tabulate(mismatches, headers=headers))

  def test_dependencies_are_sorted(self):
    pyproject_toml_path = ROOT_DIR / "pyproject.toml"
    pyproject_toml = tomllib.loads(pyproject_toml_path.read_text())
    poetry_options = pyproject_toml["tool"]["poetry"]
    dependencies = poetry_options["dependencies"]
    sorted_dependencies = sorted(dependencies.keys())
    self.assertEqual(
        list(dependencies.keys()),
        sorted_dependencies,
        "Dependencies in pyproject.toml are not sorted alphabetically.",
    )
    dev_dependencies = poetry_options["group"]["dev"]["dependencies"]
    sorted_dev_dependencies = sorted(dev_dependencies.keys())
    self.assertEqual(
        list(dev_dependencies.keys()),
        sorted_dev_dependencies,
        "Dev dependencies in pyproject.toml are not sorted alphabetically.",
    )


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