"""Tests for ``mgf-common standards`` subcommand."""

from __future__ import annotations

import argparse
import io
from contextlib import redirect_stderr, redirect_stdout

from mgf.fed.cli._standards import (
    _ALIASES,
    _resolve_standards_dir,
    _resolve_topic,
    add_standards_args,
    run_standards,
)


def _make_args(topic: str | None = None, paths: bool = False) -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    add_standards_args(parser)
    argv: list[str] = []
    if topic is not None:
        argv.append(topic)
    if paths:
        argv.append("--paths")
    return parser.parse_args(argv)


class TestResolution:
    def test_standards_dir_found(self) -> None:
        # Resolved through mgf.standard.standards_dir() (the corpus
        # ships in the mgf-standard sibling — wheel data when installed,
        # repo-relative for an editable sibling checkout). MUST resolve.
        path = _resolve_standards_dir()
        assert path is not None
        assert (path / "README.md").is_file()
        assert (path / "DESIGN_PRINCIPLES.md").is_file()

    def test_canonical_topic_resolution(self) -> None:
        topics = ["API_DESIGN", "LOGGING", "SECURITY_STANDARD"]
        # Exact match.
        assert _resolve_topic("LOGGING", topics) == "LOGGING"
        # Case-insensitive.
        assert _resolve_topic("logging", topics) == "LOGGING"
        assert _resolve_topic("Logging", topics) == "LOGGING"

    def test_alias_resolution(self) -> None:
        topics = ["API_DESIGN", "LOGGING", "SECURITY_STANDARD", "ERROR_HANDLING"]
        # Aliases from the alias map.
        assert _resolve_topic("api", topics) == "API_DESIGN"
        assert _resolve_topic("errors", topics) == "ERROR_HANDLING"
        # The "security" alias resolves to the renamed standard
        # (closes the dual-SECURITY.md naming clash; v0.16.0).
        assert _resolve_topic("security", topics) == "SECURITY_STANDARD"
        assert _resolve_topic("SECURITY", topics) == "SECURITY_STANDARD"

    def test_unknown_topic(self) -> None:
        topics = ["LOGGING"]
        assert _resolve_topic("nonsense", topics) is None

    def test_alias_map_has_canonical_targets(self) -> None:
        # Every alias must point at a real file stem; otherwise the
        # alias is a typo waiting to surprise a user.
        path = _resolve_standards_dir()
        assert path is not None
        canonical_stems = {p.stem for p in path.glob("*.md")}
        for alias, target in _ALIASES.items():
            assert target in canonical_stems, (
                f"alias {alias!r} → {target!r} but {target}.md doesn't exist"
            )


class TestListMode:
    def test_no_arg_lists_topics(self) -> None:
        buf = io.StringIO()
        with redirect_stdout(buf):
            exit_code = run_standards(_make_args())
        assert exit_code == 0
        out = buf.getvalue()
        # Every canonical topic must be in the listing.
        for expected in (
            "API_DESIGN",
            "DESIGN_PRINCIPLES",
            "ERROR_HANDLING",
            "LOGGING",
            "SECURITY_STANDARD",
            "TESTING",
        ):
            assert expected in out

    def test_no_arg_paths_lists_files(self) -> None:
        buf = io.StringIO()
        with redirect_stdout(buf):
            exit_code = run_standards(_make_args(paths=True))
        assert exit_code == 0
        out = buf.getvalue()
        # Each line should be an absolute-ish path ending in .md.
        for line in out.strip().splitlines():
            assert line.endswith(".md")


class TestPrintMode:
    def test_print_known_topic(self) -> None:
        buf = io.StringIO()
        with redirect_stdout(buf):
            exit_code = run_standards(_make_args(topic="LOGGING"))
        assert exit_code == 0
        # The doc starts with a markdown H1.
        assert "# Logging" in buf.getvalue()

    def test_print_via_alias(self) -> None:
        buf = io.StringIO()
        with redirect_stdout(buf):
            exit_code = run_standards(_make_args(topic="logging"))
        assert exit_code == 0
        assert "# Logging" in buf.getvalue()

    def test_unknown_topic_returns_error(self) -> None:
        out_buf = io.StringIO()
        err_buf = io.StringIO()
        with redirect_stdout(out_buf), redirect_stderr(err_buf):
            exit_code = run_standards(_make_args(topic="nonsense"))
        assert exit_code == 1
        assert "unknown standards topic" in err_buf.getvalue()

    def test_paths_for_known_topic(self) -> None:
        # Resolves through the "security" alias → SECURITY_STANDARD.md
        # since v0.16.0 (closes the dual-SECURITY.md naming clash).
        buf = io.StringIO()
        with redirect_stdout(buf):
            exit_code = run_standards(_make_args(topic="security", paths=True))
        assert exit_code == 0
        # One line, ending in SECURITY_STANDARD.md.
        assert buf.getvalue().strip().endswith("SECURITY_STANDARD.md")


class TestAliasCompleteness:
    """The alias map must stay accurate + complete against the live docs."""

    def test_every_alias_resolves_to_a_real_doc(self) -> None:
        """No alias may point at a non-existent standards doc."""
        standards_dir = _resolve_standards_dir()
        assert standards_dir is not None
        topics = {p.stem for p in standards_dir.glob("*.md")}
        for alias, canonical in _ALIASES.items():
            assert canonical in topics, (
                f"alias {alias!r} -> {canonical!r} but {canonical}.md "
                f"doesn't exist in {standards_dir}"
            )

    def test_every_standards_doc_has_at_least_one_alias(self) -> None:
        """Completeness: every shippable standard (except the README
        index) is reachable via at least one short alias, so the command
        stays discoverable as the corpus grows."""
        standards_dir = _resolve_standards_dir()
        assert standards_dir is not None
        topics = {p.stem for p in standards_dir.glob("*.md")}
        missing = topics - set(_ALIASES.values()) - {"README"}
        assert not missing, (
            f"standards docs with no alias in _ALIASES: {sorted(missing)}. "
            f"Add at least one short alias each (full names still work, "
            f"but aliases keep the command discoverable)."
        )
