"""Integration guard: every ``services.yml`` flag that resolves via
the dynamic ``"{{ '<role>' in group_names }}"`` form MUST be exercised
by ``meta/variants.yml`` — at least one variant entry pinning the
flag to literal ``true``, AND at least one pinning it to literal
``false``.

Rationale
---------

The matrix-deploy CLI iterates the per-role variant list (see
``docs/contributing/artefact/files/role/variants.md``) and produces
one inventory folder per entry. A flag declared as dynamic in
``services.yml`` only takes its boolean shape once the inventory
templar resolves it against the host's ``group_names``. Without
explicit variant overrides on both sides, the matrix only ever
exercises one branch — the role can ship for years with the
``false`` (or ``true``) path effectively dead.

This test makes that coverage explicit: the role's own
``meta/variants.yml`` MUST contain, for each dynamic
``(service_key, flag)`` pair, at least one variant overriding the
flag to ``true`` and one overriding it to ``false``. Pairs may share
variants — a single entry may pin multiple unrelated services true
while pinning others false; the test only checks that the union of
all variants covers both polarities for every dynamic flag.

For databases the same rule reduces to coverage of ``shared``: the
DB-consumer ``enabled`` flag stays literal ``true`` (with
``# nocheck: dynamic-flag``), so only ``shared`` is dynamic and needs
the two-polarity coverage.

Exempt a service with ``# nocheck: variants-coverage`` on (or directly
above) its ``meta/services.yml`` key — for flags whose group-derived
value is a hard invariant that variants must never override (e.g. a
browser frontend that must stay off on onion nodes).
"""

from __future__ import annotations

import unittest
from typing import TYPE_CHECKING

from utils.annotations.suppress import line_has_rule
from utils.cache.files import read_text
from utils.cache.yaml import load_yaml_any
from utils.roles.mapping import (
    ROLE_FILE_META_SERVICES,
    ROLE_FILE_META_VARIANTS,
    ROLE_TYPE_APPLICATION,
)
from utils.roles.type import get_role_types

from . import PROJECT_ROOT

if TYPE_CHECKING:
    from pathlib import Path

ROLES_DIR = PROJECT_ROOT / "roles"


_RULES = ("dynamic-flag", "variants-coverage")


def _is_dynamic_flag(value) -> bool:
    return isinstance(value, str) and "in group_names" in value


def _suppressed_top_level_keys(file_path: Path) -> set[str]:
    """Return the set of top-level service keys whose preceding comment
    block carries a ``# nocheck: dynamic-flag`` or
    ``# nocheck: variants-coverage`` marker (mirrors
    ``tests.integration.roles.meta.services.test_dynamic_flags``)."""
    exceptions: set[str] = set()
    pending = False
    for raw_line in read_text(str(file_path)).splitlines():
        stripped = raw_line.strip()
        if stripped.startswith("#"):
            if any(line_has_rule(raw_line, rule) for rule in _RULES):
                pending = True
            continue
        if not stripped:
            pending = False
            continue
        is_top_level = not raw_line.startswith((" ", "\t"))
        if pending and is_top_level and ":" in stripped:
            key = stripped.split(":", 1)[0].strip()
            if key:
                exceptions.add(key)
        pending = False
    return exceptions


def _dynamic_pairs(services: dict, suppressed: set[str]) -> list[tuple[str, str]]:
    """Return ``[(service_key, flag_name), ...]`` for every flag whose
    value is a Jinja string carrying ``in group_names``."""
    pairs: list[tuple[str, str]] = []
    for key, entry in services.items():
        if not isinstance(entry, dict) or key in suppressed:
            continue
        pairs.extend(
            (key, flag)
            for flag in ("enabled", "shared")
            if _is_dynamic_flag(entry.get(flag))
        )
    return pairs


def _load_yaml(path: Path) -> object:
    if not path.is_file():
        return None
    try:
        return load_yaml_any(str(path), default_if_missing=None)
    except Exception:
        return None


def _variant_overrides_for(variant: dict, service_key: str, flag: str) -> object:
    """Return the literal override value for ``services.<key>.<flag>``
    in ``variant``, or a sentinel ``MISSING`` if not overridden."""
    services = variant.get("services") if isinstance(variant, dict) else None
    if not isinstance(services, dict):
        return _MISSING
    entry = services.get(service_key)
    if not isinstance(entry, dict):
        return _MISSING
    if flag not in entry:
        return _MISSING
    return entry[flag]


_MISSING = object()


class TestVariantsCoverage(unittest.TestCase):
    def test_every_dynamic_flag_has_true_and_false_variant(self):
        offenders: list[str] = []

        for role_dir in sorted(p for p in ROLES_DIR.iterdir() if p.is_dir()):
            role_name = role_dir.name
            if ROLE_TYPE_APPLICATION not in get_role_types(role_dir):
                continue
            services_path = role_dir / ROLE_FILE_META_SERVICES
            services = _load_yaml(services_path)
            if not isinstance(services, dict):
                continue

            pairs = _dynamic_pairs(services, _suppressed_top_level_keys(services_path))
            if not pairs:
                continue

            variants_path = role_dir / ROLE_FILE_META_VARIANTS
            variants = _load_yaml(variants_path)
            if not isinstance(variants, list):
                offenders.append(
                    f"{role_name}: services.yml declares {len(pairs)} dynamic "
                    f"flag(s) but {variants_path.relative_to(PROJECT_ROOT)} "
                    f"is missing or not a YAML list. Add a list with at least "
                    f"two entries that pin each dynamic flag to ``true`` and "
                    f"``false`` respectively."
                )
                continue

            normalised_variants = [v if isinstance(v, dict) else {} for v in variants]

            for service_key, flag in sorted(pairs):
                seen_true = False
                seen_false = False
                for variant in normalised_variants:
                    override = _variant_overrides_for(variant, service_key, flag)
                    if override is True:
                        seen_true = True
                    elif override is False:
                        seen_false = True
                if not seen_true:
                    offenders.append(
                        f"{role_name}: services.{service_key}.{flag} is "
                        f"dynamic but no variant in meta/variants.yml pins "
                        f"it to ``true``. Add an entry with "
                        f"``services.{service_key}.{flag}: true``."
                    )
                if not seen_false:
                    offenders.append(
                        f"{role_name}: services.{service_key}.{flag} is "
                        f"dynamic but no variant in meta/variants.yml pins "
                        f"it to ``false``. Add an entry with "
                        f"``services.{service_key}.{flag}: false``."
                    )

        if offenders:
            self.fail(
                "Dynamic-flag variant coverage is incomplete (every "
                "``in group_names`` flag needs a true-pinning AND a "
                "false-pinning variant entry):\n"
                + "\n".join(f"  - {o}" for o in offenders)
            )


if __name__ == "__main__":  # pragma: no cover
    unittest.main()
