import json
import re
import string
from pathlib import Path


from openpilot.common.parameterized import parameterized
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.ui.translations.potools import parse_po
from openpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR

PERCENT_PLACEHOLDER_RE = re.compile(r"%(?:n|\d+)")
BAD_ENTITY_RE = re.compile(r'@(\w+);')
LINE_NUMBER_REF_RE = re.compile(r'^#:\s+.+:\d+(?:\s|$)')
FORMATTER = string.Formatter()
PO_DIR = Path(str(TRANSLATIONS_DIR))

with LANGUAGES_FILE.open(encoding='utf-8') as f:
  TRANSLATION_LANGUAGES = json.load(f)


def extract_placeholders(text: str) -> list[str]:
  placeholders = PERCENT_PLACEHOLDER_RE.findall(text)

  try:
    parsed = list(FORMATTER.parse(text))
  except ValueError as e:
    raise AssertionError(f"invalid brace formatting in {text!r}: {e}") from e

  for _, field_name, format_spec, conversion in parsed:
    if field_name is None:
      continue

    token = "{"
    token += field_name
    if conversion:
      token += f"!{conversion}"
    if format_spec:
      token += f":{format_spec}"
    token += "}"
    placeholders.append(token)

  return sorted(placeholders)


def load_po_text(po_path: Path) -> str:
  return po_path.read_text(encoding='utf-8')


class TestTranslations(OpenpilotTestCase):
  @parameterized.expand(sorted(TRANSLATION_LANGUAGES.values()))
  def test_translation_file_exists(self, language_code: str):
    po_path = PO_DIR / f"app_{language_code}.po"
    assert po_path.exists(), f"missing translation file: {po_path}"

  @parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
  def test_translation_placeholders_are_preserved(self, po_path: Path):
    _, entries = parse_po(po_path)
    language = po_path.stem.removeprefix("app_")

    for entry in entries:
      source_placeholders = extract_placeholders(entry.msgid)

      if entry.is_plural:
        plural_placeholders = extract_placeholders(entry.msgid_plural)
        message = (
          f"{language}: source plural placeholders do not match singular for "
          + f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}"
        )
        assert plural_placeholders == source_placeholders, message

        for idx, msgstr in sorted(entry.msgstr_plural.items()):
          if not msgstr:
            continue

          translated_placeholders = extract_placeholders(msgstr)
          message = (
            f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: "
            + f"expected {source_placeholders}, got {translated_placeholders}"
          )
          assert translated_placeholders == source_placeholders, message
      else:
        if not entry.msgstr:
          continue

        translated_placeholders = extract_placeholders(entry.msgstr)
        message = (
          f"{language}: translation changes placeholders for {entry.msgid!r}: "
          + f"expected {source_placeholders}, got {translated_placeholders}"
        )
        assert translated_placeholders == source_placeholders, message

  @parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
  def test_translation_refs_do_not_include_line_numbers(self, po_path: Path):
    for line in load_po_text(po_path).splitlines():
      assert not LINE_NUMBER_REF_RE.match(line), (
        f"{po_path.name}: line-number source reference found: {line}"
      )

  @parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
  def test_translation_entities_are_valid(self, po_path: Path):
    matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
    assert not matches, (
      f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}"
    )
