"""v0.4 Phase E pin tests.

Composable CLI registry. Three test classes:

  1. :class:`TestRegistryShape` — direct tests of
     :func:`register_cli_subcommand`, the decorator form,
     double-registration, and unregister semantics.

  2. :class:`TestBuildCli` — :func:`build_cli` parser construction:
     ``prog`` argument, ``include_builtins`` toggle, help text
     composition.

  3. :class:`TestDispatchEndToEnd` — full round-trip through
     :func:`build_cli` + :func:`dispatch` with a consumer-registered
     subcommand.

Note (federation reorg Wave 3): the CLI built-ins and the
``_mgfcli`` console-script entry point moved to ``mgf-fed`` —
``build_cli()`` now returns an EMPTY parser in this pure library.
The built-in-registration / ``_mgfcli.main`` round-trip pins live
in ``mgf-fed/tests/unit/cli/test_builtins_registered.py``; what
stays here is the registry mechanics + the empty-by-default
``build_cli`` surface that consumers compose on top of.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

import mgf.common.cli
from mgf.common.cli import (
    EXIT_UNKNOWN,
    CliSubcommand,
    build_cli,
    register_cli_subcommand,
    unregister_cli_subcommand,
)
from mgf.common.cli._subcommand_registry import (
    _registered_cli_subcommands,
    dispatch,
)

if TYPE_CHECKING:
    from collections.abc import Iterator


# Common cleanup: tests register subcommands, MUST unregister them
# afterwards so other tests + the four built-ins keep working.
@pytest.fixture
def _scratch_subcommands() -> Iterator[list[str]]:
    """Yield a list to which tests append the names they register;
    unregister all of them on teardown."""
    registered: list[str] = []
    yield registered
    for name in registered:
        unregister_cli_subcommand(name)


# ---------------------------------------------------------------------------
# E.1 — Registry shape
# ---------------------------------------------------------------------------


class TestRegistryShape:
    """Direct registry semantics."""

    def test_plain_call_registers(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        def _handler(args: object) -> int:
            return 0

        register_cli_subcommand("phase_e_plain", handler=_handler, help="x")
        _scratch_subcommands.append("phase_e_plain")
        assert "phase_e_plain" in _registered_cli_subcommands()
        record = _registered_cli_subcommands()["phase_e_plain"]
        assert isinstance(record, CliSubcommand)
        assert record.handler is _handler
        assert record.help == "x"
        assert record.parser_setup is None

    def test_decorator_registers(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        @register_cli_subcommand("phase_e_deco", help="y")
        def _handler(args: object) -> int:
            return 0

        _scratch_subcommands.append("phase_e_deco")
        # The decorator returns the handler unchanged (so `_handler`
        # is still callable).
        assert callable(_handler)
        assert _registered_cli_subcommands()["phase_e_deco"].handler is _handler

    def test_decorator_with_parser_setup(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        def _setup(p: object) -> None:
            return None

        @register_cli_subcommand(
            "phase_e_setup",
            help="z",
            parser_setup=_setup,
        )
        def _handler(args: object) -> int:
            return 0

        _scratch_subcommands.append("phase_e_setup")
        record = _registered_cli_subcommands()["phase_e_setup"]
        assert record.parser_setup is _setup

    def test_double_registration_raises(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        def _h(args: object) -> int:
            return 0

        register_cli_subcommand("phase_e_dup", handler=_h, help="x")
        _scratch_subcommands.append("phase_e_dup")

        with pytest.raises(ValueError, match="already registered"):
            register_cli_subcommand("phase_e_dup", handler=_h, help="x")

    def test_unregister_unknown_is_noop(self) -> None:
        # No exception, no return value asserted — just must not raise.
        unregister_cli_subcommand("phase_e_does_not_exist")

    def test_unregister_then_register_works(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        """The supported way to replace a registered subcommand:
        unregister first, then re-register."""

        def _h1(args: object) -> int:
            return 0

        def _h2(args: object) -> int:
            return 0

        register_cli_subcommand("phase_e_replace", handler=_h1, help="x")
        _scratch_subcommands.append("phase_e_replace")
        unregister_cli_subcommand("phase_e_replace")
        register_cli_subcommand("phase_e_replace", handler=_h2, help="y")
        record = _registered_cli_subcommands()["phase_e_replace"]
        assert record.handler is _h2
        assert record.help == "y"


# ---------------------------------------------------------------------------
# E.2 — build_cli
# ---------------------------------------------------------------------------


class TestBuildCli:
    """Parser construction from the registry."""

    def test_default_prog_name(self) -> None:
        parser = build_cli()
        assert parser.prog == "mgf-common"

    def test_custom_prog_name(self) -> None:
        """Consumer top-level CLI shape — pass your own prog."""
        parser = build_cli(prog="myapp")
        assert parser.prog == "myapp"

    def test_empty_by_default(self) -> None:
        """In this pure library ``build_cli()`` returns an EMPTY parser
        (no built-ins registered — those moved to mgf-fed). A consumer
        must register its own subcommands before any appear."""
        import argparse

        parser = build_cli()
        sub_action = next(a for a in parser._actions if isinstance(a, argparse._SubParsersAction))
        # No consumer/built-in subcommands are present out of the box.
        assert sub_action.choices == {}

    def test_consumer_subcommand_appears(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        """Newly-registered subcommand shows up immediately."""

        @register_cli_subcommand(
            "phase_e_visible",
            help="Phase E visible test subcommand.",
        )
        def _h(args: object) -> int:
            return 0

        _scratch_subcommands.append("phase_e_visible")
        parser = build_cli()
        help_text = parser.format_help()
        assert "phase_e_visible" in help_text

    def test_parser_setup_runs_at_build_time(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        """The registered ``parser_setup`` callback is invoked when
        :func:`build_cli` constructs the parser — not at registration
        time."""
        called = {"count": 0}

        def _setup(subparser: object) -> None:
            called["count"] += 1
            # type: ignore[attr-defined]
            subparser.add_argument(  # type: ignore[attr-defined]
                "--phase-e-flag",
                action="store_true",
            )

        @register_cli_subcommand(
            "phase_e_with_flag",
            help="z",
            parser_setup=_setup,
        )
        def _h(args: object) -> int:
            return 0

        _scratch_subcommands.append("phase_e_with_flag")
        # Pre-build: setup not called.
        assert called["count"] == 0
        # Build: setup runs once.
        parser = build_cli()
        assert called["count"] == 1
        # The flag the setup added IS parseable:
        args = parser.parse_args(["phase_e_with_flag", "--phase-e-flag"])
        assert args.phase_e_flag is True


# ---------------------------------------------------------------------------
# E.3 — dispatch end-to-end
# ---------------------------------------------------------------------------


class TestDispatchEndToEnd:
    """Round-trip: register → ``mgf-common <name>`` → handler runs →
    exit code propagates."""

    def test_consumer_subcommand_dispatched_via_build_cli(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        """Consumer round-trip through the KEEP surface: register →
        ``build_cli()`` → ``parse_args`` → ``dispatch`` → handler runs →
        exit code propagates. (The ``_mgfcli.main`` console-script
        round-trip moved to mgf-fed with the entry point.)"""
        called = {"got_args": None}

        def _handler(args: object) -> int:
            called["got_args"] = args
            return 7  # arbitrary non-zero, distinguishable from EXIT_SUCCESS

        register_cli_subcommand(
            "phase_e_dispatch",
            handler=_handler,
            help="d",
        )
        _scratch_subcommands.append("phase_e_dispatch")

        parser = build_cli()
        args = parser.parse_args(["phase_e_dispatch"])
        rc = dispatch(args, parser=parser)
        assert rc == 7
        assert called["got_args"] is not None

    def test_no_subcommand_returns_unknown(self) -> None:
        """No subcommand → ``dispatch`` prints help to stderr and
        returns EXIT_UNKNOWN — the consumer's no-subcommand fallback.
        Exercised via the KEEP ``build_cli`` + ``dispatch`` surface
        (the ``_mgfcli.main`` entry point moved to mgf-fed)."""
        parser = build_cli()
        args = parser.parse_args([])
        rc = dispatch(args, parser=parser)
        assert rc == EXIT_UNKNOWN

    def test_dispatch_helper_directly(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        """The :func:`dispatch` helper is callable directly — for
        consumers building their own top-level CLI."""

        @register_cli_subcommand("phase_e_direct", help="x")
        def _h(args: object) -> int:
            return 42

        _scratch_subcommands.append("phase_e_direct")
        parser = build_cli()
        args = parser.parse_args(["phase_e_direct"])
        rc = dispatch(args, parser=parser)
        assert rc == 42

    def test_dispatch_unknown_command_returns_unknown(self) -> None:
        """``dispatch`` with an unknown / empty command name returns
        :data:`EXIT_UNKNOWN`. Defensive — argparse normally prevents
        this from happening, but the registry-level fallback is
        important for consumer-built CLIs."""
        # Simulate an empty namespace (e.g. consumer parsed args
        # without subcommand):
        import argparse

        ns = argparse.Namespace(command=None)
        rc = dispatch(ns)
        assert rc == EXIT_UNKNOWN


# ---------------------------------------------------------------------------
# E.4 — public surface assertions
# ---------------------------------------------------------------------------


class TestPublicSurface:
    """The four new public names live on ``mgf.common.cli``."""

    def test_register_in_all(self) -> None:
        for name in (
            "register_cli_subcommand",
            "unregister_cli_subcommand",
            "CliSubcommand",
            "build_cli",
        ):
            assert name in mgf.common.cli.__all__, (
                f"v0.4 Phase E added {name!r} to mgf.common.cli — MUST appear in __all__"
            )

    def test_subcommand_alias_is_same_function(self) -> None:
        """``subcommand`` is a short alias for
        :func:`register_cli_subcommand` (FEEDBACK V04-02). Both names
        refer to the SAME function — identity check, not just equality."""
        from mgf.common.cli import register_cli_subcommand, subcommand

        assert subcommand is register_cli_subcommand

    def test_subcommand_alias_works_as_decorator(
        self,
        _scratch_subcommands: list[str],
    ) -> None:
        """End-to-end: registering via the short alias is functionally
        identical to the long form."""
        from mgf.common.cli import subcommand

        @subcommand("phase_e_short_alias", help="x")
        def _h(args: object) -> int:
            return 0

        _scratch_subcommands.append("phase_e_short_alias")
        assert "phase_e_short_alias" in _registered_cli_subcommands()

    def test_subcommand_in_all(self) -> None:
        """The short alias is in ``mgf.common.cli.__all__``."""
        assert "subcommand" in mgf.common.cli.__all__

    def test_clisubcommand_is_frozen(self) -> None:
        """``CliSubcommand`` is a frozen dataclass — mutation
        raises so registry records can't be tampered with."""
        record = CliSubcommand(
            name="x",
            help="y",
            handler=lambda args: 0,
            parser_setup=None,
        )
        with pytest.raises((AttributeError, Exception)):
            record.name = "z"  # type: ignore[misc]
