"""CLI dispatch tests for obliteratus.cli.main().

These tests verify argument parsing and subcommand routing without
downloading real models or running any pipeline.  They use
``unittest.mock.patch`` to capture stdout/stderr and
``pytest.raises(SystemExit)`` for argparse exits.
"""

from __future__ import annotations

from io import StringIO
from unittest.mock import patch

import pytest

from obliteratus.cli import main


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _capture_exit(argv: list[str] | None, *, expect_code: int | None = None):
    """Call main(argv), expecting SystemExit; return captured stderr text."""
    buf = StringIO()
    with pytest.raises(SystemExit) as exc_info, patch("sys.stderr", buf):
        main(argv)
    if expect_code is not None:
        assert exc_info.value.code == expect_code
    return buf.getvalue()


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestCLIDispatch:
    """Test suite for CLI argument parsing and subcommand dispatch."""

    # 1. No args -> prints help / exits with error
    def test_main_no_args_prints_help(self):
        """Calling main() with no args should exit (subcommand is required)."""
        stderr_text = _capture_exit([], expect_code=2)
        # argparse prints usage info to stderr on error
        assert "usage" in stderr_text.lower() or "required" in stderr_text.lower()

    # 2. models command lists models without error
    def test_models_command(self):
        """Calling main(['models']) should list models without raising."""
        with patch("obliteratus.cli.console") as mock_console:
            main(["models"])
        # console.print is called at least once to render the table
        assert mock_console.print.call_count >= 1

    # 3. obliterate without model arg -> error
    def test_obliterate_requires_model(self):
        """Calling main(['obliterate']) without a model arg should error."""
        stderr_text = _capture_exit(["obliterate"], expect_code=2)
        assert "model" in stderr_text.lower() or "required" in stderr_text.lower()

    # 4. obliterate --method accepts valid methods
    def test_obliterate_valid_methods(self):
        """Test that --method accepts all 9 pipeline methods."""
        valid_methods = [
            "basic", "advanced", "aggressive", "spectral_cascade",
            "informed", "surgical", "optimized", "inverted", "nuclear",
        ]
        for method in valid_methods:
            # Patch the actual pipeline execution so nothing runs
            with patch("obliteratus.cli._cmd_abliterate") as mock_cmd:
                main(["obliterate", "fake/model", "--method", method])
                mock_cmd.assert_called_once()
                args_passed = mock_cmd.call_args[0][0]
                assert args_passed.method == method

    # 4b. invalid methods are rejected
    def test_obliterate_rejects_invalid_method(self):
        """The CLI --method flag rejects unknown method names."""
        stderr_text = _capture_exit(
            ["obliterate", "fake/model", "--method", "nonexistent"],
            expect_code=2,
        )
        assert "invalid choice" in stderr_text.lower()

    # 5. run requires config path
    def test_run_requires_config(self):
        """Calling main(['run']) without a config path should error."""
        stderr_text = _capture_exit(["run"], expect_code=2)
        assert "config" in stderr_text.lower() or "required" in stderr_text.lower()

    # 6. aggregate with nonexistent dir handles gracefully
    def test_aggregate_command_missing_dir(self):
        """Calling main(['aggregate']) with nonexistent dir should handle gracefully."""
        with patch("obliteratus.cli.console") as mock_console:
            main(["aggregate", "--dir", "/nonexistent/path/to/nowhere"])
        # The command prints a message about no contributions found and returns
        printed_text = " ".join(
            str(call) for call in mock_console.print.call_args_list
        )
        assert "no contributions found" in printed_text.lower() or mock_console.print.called

    def test_aggregate_accepts_format_metric_min_runs(self):
        """aggregate accepts --format, --metric and --min-runs flags."""
        with patch("obliteratus.cli._cmd_aggregate") as mock_cmd:
            main([
                "aggregate",
                "--format", "latex",
                "--metric", "refusal_rate",
                "--min-runs", "3",
            ])
            mock_cmd.assert_called_once()
            args_passed = mock_cmd.call_args[0][0]
            assert args_passed.format == "latex"
            assert args_passed.metric == "refusal_rate"
            assert args_passed.min_runs == 3

    def test_aggregate_rejects_invalid_format(self):
        """aggregate rejects unknown --format choices."""
        stderr_text = _capture_exit(
            ["aggregate", "--format", "invalid"],
            expect_code=2,
        )
        assert "invalid choice" in stderr_text.lower()

    # 7. --help flag prints help
    def test_help_flag(self):
        """Calling main(['--help']) should print help and exit 0."""
        buf = StringIO()
        with pytest.raises(SystemExit) as exc_info, patch("sys.stdout", buf):
            main(["--help"])
        assert exc_info.value.code == 0
        output = buf.getvalue()
        assert "obliteratus" in output.lower() or "usage" in output.lower()

    # 8. interactive subcommand is registered
    def test_interactive_command_exists(self):
        """Verify 'interactive' subcommand is registered and dispatches."""
        with patch("obliteratus.cli._cmd_interactive") as mock_cmd:
            main(["interactive"])
            mock_cmd.assert_called_once()

    # 9. --contribute and --contribute-notes are accepted on obliterate
    def test_contribute_flags_on_obliterate(self):
        """Verify --contribute and --contribute-notes are accepted args."""
        with patch("obliteratus.cli._cmd_abliterate") as mock_cmd:
            main([
                "obliterate", "fake/model",
                "--contribute",
                "--contribute-notes", "Testing contribution system",
            ])
            mock_cmd.assert_called_once()
            args_passed = mock_cmd.call_args[0][0]
            assert args_passed.contribute is True
            assert args_passed.contribute_notes == "Testing contribution system"
