"""cli.py のテスト。"""

from __future__ import annotations

import argparse
from unittest.mock import MagicMock, patch

import pytest

from gfo.cli import (
    _DISPATCH,
    _ensure_utf8_stdio,
    _positive_int,
    _pre_parse_format,
    _resolve_format,
    create_parser,
    main,
)
from gfo.exceptions import (
    AuthenticationError,
    AuthError,
    ConfigError,
    GfoError,
    NetworkError,
    NotFoundError,
    NotSupportedError,
    RateLimitError,
)

# ── _positive_int のテスト ──


def test_positive_int_valid():
    assert _positive_int("1") == 1
    assert _positive_int("100") == 100


def test_positive_int_zero_raises():
    with pytest.raises(argparse.ArgumentTypeError, match="0 is not a positive integer"):
        _positive_int("0")


def test_positive_int_negative_raises():
    with pytest.raises(argparse.ArgumentTypeError, match="-5 is not a positive integer"):
        _positive_int("-5")


def test_positive_int_non_integer_raises():
    with pytest.raises(argparse.ArgumentTypeError, match="abc is not a positive integer"):
        _positive_int("abc")


def test_positive_int_float_string_raises():
    with pytest.raises(argparse.ArgumentTypeError, match="1.5 is not a positive integer"):
        _positive_int("1.5")


# ── create_parser のテスト ──


def test_parser_version(capsys):
    parser, _ = create_parser()
    with pytest.raises(SystemExit) as exc:
        parser.parse_args(["--version"])
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "gfo 0.5.0" in captured.out


def test_parser_init_defaults():
    parser, _ = create_parser()
    args = parser.parse_args(["init"])
    assert args.command == "init"
    assert args.non_interactive is False
    assert args.type is None
    assert args.host is None


def test_parser_init_non_interactive():
    parser, _ = create_parser()
    args = parser.parse_args(
        ["init", "--non-interactive", "--type", "github", "--host", "github.com"]
    )
    assert args.non_interactive is True
    assert args.type == "github"
    assert args.host == "github.com"


def test_parser_auth_login():
    parser, _ = create_parser()
    args = parser.parse_args(["auth", "login", "--host", "github.com", "--token", "tok"])
    assert args.command == "auth"
    assert args.subcommand == "login"
    assert args.host == "github.com"
    assert args.token == "tok"


def test_parser_auth_status():
    parser, _ = create_parser()
    args = parser.parse_args(["auth", "status"])
    assert args.command == "auth"
    assert args.subcommand == "status"


def test_parser_pr_list_defaults():
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "list"])
    assert args.command == "pr"
    assert args.subcommand == "list"
    assert args.state == "open"
    assert args.limit == 30


def test_parser_pr_create():
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "create", "--title", "My PR", "--draft"])
    assert args.title == "My PR"
    assert args.draft is True


def test_parser_pr_view():
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "view", "42"])
    assert args.number == 42


def test_parser_pr_merge():
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "merge", "5", "--squash"])
    assert args.number == 5
    assert args.squash is True
    assert args.rebase is False


def test_parser_pr_close():
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "close", "3"])
    assert args.number == 3


def test_parser_pr_checkout():
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "checkout", "7"])
    assert args.number == 7


def test_parser_issue_list():
    parser, _ = create_parser()
    args = parser.parse_args(["issue", "list", "--state", "closed", "--assignee", "alice"])
    assert args.state == "closed"
    assert args.assignee == "alice"


def test_parser_issue_create():
    parser, _ = create_parser()
    args = parser.parse_args(["issue", "create", "--title", "Bug"])
    assert args.title == "Bug"


def test_parser_issue_view():
    parser, _ = create_parser()
    args = parser.parse_args(["issue", "view", "10"])
    assert args.number == 10


def test_parser_issue_close():
    parser, _ = create_parser()
    args = parser.parse_args(["issue", "close", "10"])
    assert args.number == 10


def test_parser_issue_delete():
    parser, _ = create_parser()
    args = parser.parse_args(["issue", "delete", "7"])
    assert args.subcommand == "delete"
    assert args.number == 7


def test_parser_repo_list():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "list", "--limit", "10"])
    assert args.limit == 10


def test_parser_repo_create():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "create", "my-repo", "--private"])
    assert args.name == "my-repo"
    assert args.private is True


def test_parser_repo_clone_dest_repo():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "clone", "owner/repo"])
    assert args.repo == "owner/repo"


def test_parser_repo_clone_project_arg():
    """--project 引数が正しく parse される。"""
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "clone", "owner/repo", "--project", "myproj"])
    assert args.project == "myproj"


def test_parser_repo_clone_project_default_none():
    """--project 未指定時は None。"""
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "clone", "owner/repo"])
    assert args.project is None


def test_parser_repo_view_dest_repo():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "view", "owner/repo"])
    assert args.repo == "owner/repo"


def test_parser_repo_view_optional():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "view"])
    assert args.repo is None


def test_parser_repo_delete():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "delete"])
    assert args.subcommand == "delete"
    assert args.yes is False


def test_parser_repo_delete_yes_flag():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "delete", "--yes"])
    assert args.yes is True


def test_parser_repo_delete_short_yes_flag():
    parser, _ = create_parser()
    args = parser.parse_args(["repo", "delete", "-y"])
    assert args.yes is True


def test_parser_release_list():
    parser, _ = create_parser()
    args = parser.parse_args(["release", "list"])
    assert args.limit == 30


def test_parser_release_create():
    parser, _ = create_parser()
    args = parser.parse_args(["release", "create", "v1.0.0", "--draft", "--prerelease"])
    assert args.tag == "v1.0.0"
    assert args.draft is True
    assert args.prerelease is True


def test_parser_label_list():
    parser, _ = create_parser()
    args = parser.parse_args(["label", "list"])
    assert args.subcommand == "list"


def test_parser_label_create():
    parser, _ = create_parser()
    args = parser.parse_args(["label", "create", "bug", "--color", "red"])
    assert args.name == "bug"
    assert args.color == "red"


def test_parser_milestone_list():
    parser, _ = create_parser()
    args = parser.parse_args(["milestone", "list"])
    assert args.subcommand == "list"


def test_parser_milestone_create():
    parser, _ = create_parser()
    args = parser.parse_args(["milestone", "create", "v2.0", "--due", "2026-12-31"])
    assert args.title == "v2.0"
    assert args.due == "2026-12-31"


def test_parser_release_delete():
    parser, _ = create_parser()
    args = parser.parse_args(["release", "delete", "v1.0.0"])
    assert args.subcommand == "delete"
    assert args.tag == "v1.0.0"


def test_parser_label_delete():
    parser, _ = create_parser()
    args = parser.parse_args(["label", "delete", "bug"])
    assert args.subcommand == "delete"
    assert args.name == "bug"


def test_parser_milestone_delete():
    parser, _ = create_parser()
    args = parser.parse_args(["milestone", "delete", "5"])
    assert args.subcommand == "delete"
    assert args.number == 5


def test_parser_format_option():
    parser, _ = create_parser()
    args = parser.parse_args(["--format", "json", "pr", "list"])
    assert args.format == "json"


# ── browse パーサーのテスト ──


def test_parser_browse_defaults():
    parser, _ = create_parser()
    args = parser.parse_args(["browse"])
    assert args.command == "browse"
    assert args.pr is None
    assert args.issue is None
    assert args.settings is False
    assert getattr(args, "print") is False


def test_parser_browse_pr():
    parser, _ = create_parser()
    args = parser.parse_args(["browse", "--pr", "42"])
    assert args.pr == 42
    assert args.issue is None


def test_parser_browse_issue():
    parser, _ = create_parser()
    args = parser.parse_args(["browse", "--issue", "7"])
    assert args.issue == 7
    assert args.pr is None


def test_parser_browse_settings():
    parser, _ = create_parser()
    args = parser.parse_args(["browse", "--settings"])
    assert args.settings is True


def test_parser_browse_print():
    parser, _ = create_parser()
    args = parser.parse_args(["browse", "--print"])
    assert getattr(args, "print") is True


def test_parser_browse_mutually_exclusive():
    """--pr と --issue は同時指定できない。"""
    parser, _ = create_parser()
    with pytest.raises(ConfigError):
        parser.parse_args(["browse", "--pr", "1", "--issue", "2"])


# ── ssh-key パーサーのテスト ──


def test_parser_ssh_key_list():
    parser, _ = create_parser()
    args = parser.parse_args(["ssh-key", "list"])
    assert args.command == "ssh-key"
    assert args.subcommand == "list"
    assert args.limit == 30


def test_parser_ssh_key_create():
    parser, _ = create_parser()
    args = parser.parse_args(["ssh-key", "create", "--title", "my-key", "--key", "ssh-rsa AAAA"])
    assert args.subcommand == "create"
    assert args.title == "my-key"
    assert args.key == "ssh-rsa AAAA"


def test_parser_ssh_key_delete():
    parser, _ = create_parser()
    args = parser.parse_args(["ssh-key", "delete", "123"])
    assert args.subcommand == "delete"
    assert args.id == 123


# ── branch-protect パーサーのテスト ──


def test_parser_branch_protect_list():
    parser, _ = create_parser()
    args = parser.parse_args(["branch-protect", "list"])
    assert args.command == "branch-protect"
    assert args.subcommand == "list"
    assert args.limit == 30


def test_parser_branch_protect_view():
    parser, _ = create_parser()
    args = parser.parse_args(["branch-protect", "view", "main"])
    assert args.subcommand == "view"
    assert args.branch == "main"


def test_parser_branch_protect_set():
    parser, _ = create_parser()
    args = parser.parse_args(
        [
            "branch-protect",
            "set",
            "main",
            "--require-reviews",
            "2",
            "--enforce-admins",
            "--allow-force-push",
        ]
    )
    assert args.subcommand == "set"
    assert args.branch == "main"
    assert args.require_reviews == 2
    assert args.enforce_admins is True
    assert args.allow_force_push is True


def test_parser_branch_protect_set_negations():
    parser, _ = create_parser()
    args = parser.parse_args(
        [
            "branch-protect",
            "set",
            "main",
            "--no-enforce-admins",
            "--no-allow-force-push",
            "--no-allow-deletions",
        ]
    )
    assert args.enforce_admins is False
    assert args.allow_force_push is False
    assert args.allow_deletions is False


def test_parser_branch_protect_remove():
    parser, _ = create_parser()
    args = parser.parse_args(["branch-protect", "remove", "main"])
    assert args.subcommand == "remove"
    assert args.branch == "main"


# ── notification パーサーのテスト ──


def test_parser_notification_list():
    parser, _ = create_parser()
    args = parser.parse_args(["notification", "list"])
    assert args.command == "notification"
    assert args.subcommand == "list"
    assert args.unread_only is False
    assert args.limit == 30


def test_parser_notification_list_unread_only():
    parser, _ = create_parser()
    args = parser.parse_args(["notification", "list", "--unread-only"])
    assert args.unread_only is True


def test_parser_notification_read_by_id():
    parser, _ = create_parser()
    args = parser.parse_args(["notification", "read", "42"])
    assert args.subcommand == "read"
    assert args.id == "42"
    assert args.mark_all is False


def test_parser_notification_read_all():
    parser, _ = create_parser()
    args = parser.parse_args(["notification", "read", "--all"])
    assert args.subcommand == "read"
    assert args.id is None
    assert args.mark_all is True


# ── org パーサーのテスト ──


def test_parser_org_list():
    parser, _ = create_parser()
    args = parser.parse_args(["org", "list"])
    assert args.command == "org"
    assert args.subcommand == "list"
    assert args.limit == 30


def test_parser_org_view():
    parser, _ = create_parser()
    args = parser.parse_args(["org", "view", "my-org"])
    assert args.subcommand == "view"
    assert args.name == "my-org"


def test_parser_org_members():
    parser, _ = create_parser()
    args = parser.parse_args(["org", "members", "my-org", "--limit", "10"])
    assert args.subcommand == "members"
    assert args.name == "my-org"
    assert args.limit == 10


def test_parser_org_repos():
    parser, _ = create_parser()
    args = parser.parse_args(["org", "repos", "my-org"])
    assert args.subcommand == "repos"
    assert args.name == "my-org"


# ── _DISPATCH テーブルのテスト ──


def test_dispatch_table_entry_count():
    assert len(_DISPATCH) == 165


def test_dispatch_table_all_keys():
    expected_keys = {
        ("init", None),
        ("auth", "login"),
        ("auth", "status"),
        ("auth", "switch"),
        ("auth", "logout"),
        ("pr", "list"),
        ("pr", "create"),
        ("pr", "view"),
        ("pr", "merge"),
        ("pr", "close"),
        ("pr", "checkout"),
        ("pr", "edit"),
        ("pr", "reopen"),
        ("pr", "lock"),
        ("pr", "unlock"),
        ("pr", "diff"),
        ("pr", "checks"),
        ("pr", "files"),
        ("pr", "commits"),
        ("pr", "reviewers"),
        ("pr", "update-branch"),
        ("pr", "ready"),
        ("pr", "status"),
        ("issue", "list"),
        ("issue", "create"),
        ("issue", "view"),
        ("issue", "close"),
        ("issue", "lock"),
        ("issue", "unlock"),
        ("issue", "delete"),
        ("issue", "edit"),
        ("issue", "reopen"),
        ("issue-template", "list"),
        ("repo", "list"),
        ("repo", "create"),
        ("repo", "clone"),
        ("repo", "view"),
        ("repo", "delete"),
        ("repo", "fork"),
        ("repo", "sync"),
        ("repo", "edit"),
        ("repo", "archive"),
        ("repo", "languages"),
        ("repo", "topics"),
        ("repo", "compare"),
        ("repo", "migrate"),
        ("release", "list"),
        ("release", "create"),
        ("release", "delete"),
        ("release", "view"),
        ("release", "edit"),
        ("release", "asset"),
        ("label", "list"),
        ("label", "create"),
        ("label", "delete"),
        ("label", "edit"),
        ("milestone", "list"),
        ("milestone", "create"),
        ("milestone", "delete"),
        ("milestone", "view"),
        ("milestone", "edit"),
        ("milestone", "close"),
        ("milestone", "reopen"),
        ("pr", "comment"),
        ("issue", "comment"),
        ("pr", "review"),
        ("branch", "view"),
        ("branch", "list"),
        ("branch", "create"),
        ("branch", "delete"),
        ("tag", "view"),
        ("tag", "list"),
        ("tag", "create"),
        ("tag", "delete"),
        ("status", "list"),
        ("status", "create"),
        ("file", "get"),
        ("file", "put"),
        ("file", "delete"),
        ("webhook", "list"),
        ("webhook", "create"),
        ("webhook", "delete"),
        ("webhook", "test"),
        ("webhook", "edit"),
        ("deploy-key", "view"),
        ("deploy-key", "list"),
        ("deploy-key", "create"),
        ("deploy-key", "delete"),
        ("collaborator", "list"),
        ("collaborator", "add"),
        ("collaborator", "remove"),
        ("ci", "list"),
        ("ci", "view"),
        ("ci", "cancel"),
        ("ci", "trigger"),
        ("ci", "retry"),
        ("ci", "logs"),
        ("ci", "watch"),
        ("ci", "download"),
        ("ci", "workflow"),
        ("ci", "artifact"),
        ("user", "whoami"),
        ("search", "repos"),
        ("search", "issues"),
        ("wiki", "list"),
        ("wiki", "view"),
        ("wiki", "create"),
        ("wiki", "edit"),
        ("wiki", "delete"),
        ("issue", "reaction"),
        ("issue", "depends"),
        ("issue", "timeline"),
        ("issue", "subscribe"),
        ("issue", "unsubscribe"),
        ("issue", "pin"),
        ("issue", "unpin"),
        ("issue", "time"),
        ("search", "prs"),
        ("search", "commits"),
        ("label", "clone"),
        ("wiki", "revisions"),
        ("repo", "mirror"),
        ("repo", "transfer"),
        ("repo", "star"),
        ("repo", "unstar"),
        ("package", "list"),
        ("package", "view"),
        ("package", "delete"),
        ("branch-protect", "list"),
        ("branch-protect", "view"),
        ("branch-protect", "set"),
        ("branch-protect", "remove"),
        ("notification", "list"),
        ("notification", "read"),
        ("org", "list"),
        ("org", "view"),
        ("org", "members"),
        ("org", "repos"),
        ("org", "create"),
        ("org", "edit"),
        ("org", "delete"),
        ("secret", "list"),
        ("secret", "set"),
        ("secret", "delete"),
        ("variable", "list"),
        ("variable", "set"),
        ("variable", "get"),
        ("variable", "delete"),
        ("ssh-key", "view"),
        ("ssh-key", "list"),
        ("ssh-key", "create"),
        ("ssh-key", "delete"),
        ("gpg-key", "view"),
        ("gpg-key", "list"),
        ("gpg-key", "create"),
        ("gpg-key", "delete"),
        ("tag-protect", "list"),
        ("tag-protect", "create"),
        ("tag-protect", "edit"),
        ("tag-protect", "delete"),
        ("browse", None),
        ("api", None),
        ("schema", None),
        ("issue", "migrate"),
        ("batch", "pr"),
    }
    assert set(_DISPATCH.keys()) == expected_keys


def test_dispatch_table_all_callable():
    for key, handler in _DISPATCH.items():
        assert callable(handler), f"Handler for {key} is not callable"


# ── main() のテスト ──


def test_main_no_args_returns_1(capsys):
    result = main([])
    assert result == 1
    captured = capsys.readouterr()
    assert "gfo" in captured.out  # help テキストに prog 名が含まれる


def test_main_version(capsys):
    with pytest.raises(SystemExit) as exc:
        main(["--version"])
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "gfo 0.5.0" in captured.out


def test_main_subcommand_only_returns_1(capsys):
    result = main(["pr"])
    assert result == 1


def test_main_normal_dispatch():
    mock_handler = MagicMock()
    with patch.dict(_DISPATCH, {("pr", "list"): mock_handler}):
        with patch("gfo.cli.get_configured_output_format", return_value="table"):
            result = main(["pr", "list"])
    assert result == 0
    mock_handler.assert_called_once()
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "table"


def test_main_format_override():
    mock_handler = MagicMock()
    with patch.dict(_DISPATCH, {("pr", "list"): mock_handler}):
        result = main(["--format", "json", "pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "json"


def test_main_format_plain_override():
    """--format plain が fmt に正しく伝搬する。"""
    mock_handler = MagicMock()
    with patch.dict(_DISPATCH, {("pr", "list"): mock_handler}):
        result = main(["--format", "plain", "pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "plain"


def test_main_default_format_from_config_plain():
    """config.toml の output = "plain" が fmt に伝搬する。"""
    mock_handler = MagicMock()
    with (
        patch.dict(_DISPATCH, {("pr", "list"): mock_handler}),
        patch("gfo.cli.get_configured_output_format", return_value="plain"),
    ):
        result = main(["pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "plain"


def test_main_jq_forces_json():
    """--jq 指定時に fmt が json に強制される。"""
    mock_handler = MagicMock()
    with patch.dict(_DISPATCH, {("pr", "list"): mock_handler}):
        result = main(["--jq", ".[].title", "pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "json"
    assert kwargs["jq"] == ".[].title"


def test_main_jq_overrides_format_table():
    """--jq は --format table を上書きして json にする。"""
    mock_handler = MagicMock()
    with patch.dict(_DISPATCH, {("pr", "list"): mock_handler}):
        result = main(["--format", "table", "--jq", ".", "pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "json"
    assert kwargs["jq"] == "."


def test_main_no_jq_passes_none():
    """--jq なしでは jq=None が渡される。"""
    mock_handler = MagicMock()
    with patch.dict(_DISPATCH, {("pr", "list"): mock_handler}):
        result = main(["pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["jq"] is None


# ── _resolve_format / TTY 検出 ──


def test_resolve_format_tty_returns_table():
    """TTY 時・config 未設定ではデフォルト table。"""
    with (
        patch("gfo.cli.get_configured_output_format", return_value=None),
        patch("gfo.cli.sys.stdout") as mock_stdout,
        patch.dict("os.environ", {}, clear=False),
    ):
        mock_stdout.isatty.return_value = True
        assert _resolve_format(None, None) == "table"


def test_resolve_format_non_tty_returns_json():
    """非 TTY 時に自動で JSON。"""
    with (
        patch("gfo.cli.get_configured_output_format", return_value=None),
        patch("gfo.cli.sys.stdout") as mock_stdout,
        patch.dict("os.environ", {}, clear=False),
    ):
        mock_stdout.isatty.return_value = False
        # GFO_NO_AUTO_JSON が設定されていないことを保証
        import os

        os.environ.pop("GFO_NO_AUTO_JSON", None)
        assert _resolve_format(None, None) == "json"


def test_resolve_format_non_tty_with_opt_out():
    """GFO_NO_AUTO_JSON=1 で TTY 検出を無効化し table にフォールバック。"""
    with (
        patch("gfo.cli.get_configured_output_format", return_value=None),
        patch("gfo.cli.sys.stdout") as mock_stdout,
        patch.dict("os.environ", {"GFO_NO_AUTO_JSON": "1"}, clear=False),
    ):
        mock_stdout.isatty.return_value = False
        assert _resolve_format(None, None) == "table"


def test_resolve_format_explicit_format_overrides_tty():
    """--format 指定が TTY 検出より優先。"""
    with patch("gfo.cli.sys.stdout") as mock_stdout:
        mock_stdout.isatty.return_value = False
        assert _resolve_format("table", None) == "table"


def test_resolve_format_config_overrides_tty():
    """config 設定が TTY 検出より優先。"""
    with (
        patch("gfo.cli.get_configured_output_format", return_value="plain"),
        patch("gfo.cli.sys.stdout") as mock_stdout,
    ):
        mock_stdout.isatty.return_value = False
        assert _resolve_format(None, None) == "plain"


def test_resolve_format_jq_overrides_all():
    """--jq が最優先で json を返す。"""
    assert _resolve_format("table", ".") == "json"


def test_main_non_tty_auto_json():
    """main() E2E: 非 TTY 時に handler に fmt='json' が渡る。"""
    mock_handler = MagicMock()
    with (
        patch.dict(_DISPATCH, {("pr", "list"): mock_handler}),
        patch("gfo.cli.get_configured_output_format", return_value=None),
        patch("gfo.cli.sys.stdout") as mock_stdout,
        patch.dict("os.environ", {}, clear=False),
    ):
        mock_stdout.isatty.return_value = False
        import os

        os.environ.pop("GFO_NO_AUTO_JSON", None)
        result = main(["pr", "list"])
    assert result == 0
    _, kwargs = mock_handler.call_args
    assert kwargs["fmt"] == "json"


def test_main_gfo_error_returns_1(capsys):
    def raise_gfo(args, *, fmt, jq=None):
        raise GfoError("something went wrong")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_gfo}):
        result = main(["pr", "list"])
    assert result == 1
    captured = capsys.readouterr()
    assert "something went wrong" in captured.err


def test_main_not_supported_error_returns_5(capsys):
    def raise_nse(args, *, fmt, jq=None):
        raise NotSupportedError("github", "delete-repo", web_url="https://github.com/settings")

    with (
        patch.dict(_DISPATCH, {("pr", "list"): raise_nse}),
        patch("gfo.cli.get_configured_output_format", return_value="table"),
    ):
        result = main(["pr", "list"])
    assert result == 5
    captured = capsys.readouterr()
    assert "github" in captured.err
    assert "https://github.com/settings" in captured.out


def test_main_not_supported_error_no_web_url(capsys):
    def raise_nse(args, *, fmt, jq=None):
        raise NotSupportedError("gitlab", "milestones")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_nse}):
        result = main(["pr", "list"])
    assert result == 5
    captured = capsys.readouterr()
    assert "gitlab" in captured.err
    assert captured.out == ""


# ── --format json エラー出力のテスト ──


def test_main_gfo_error_json_format(capsys):
    """--format json 指定時に GfoError が JSON で stderr に出力される。"""
    import json

    def raise_gfo(args, *, fmt, jq=None):
        raise GfoError("something went wrong")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_gfo}):
        result = main(["--format", "json", "pr", "list"])
    assert result == 1
    captured = capsys.readouterr()
    parsed = json.loads(captured.err)
    assert parsed["error"] == "general_error"
    assert parsed["message"] == "something went wrong"
    assert "hint" not in parsed


def test_main_auth_error_json_format(capsys):
    """--format json 指定時に AuthError が hint 付き JSON で stderr に出力される。"""
    import json

    def raise_auth(args, *, fmt, jq=None):
        raise AuthError("github.com")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_auth}):
        result = main(["--format", "json", "pr", "list"])
    assert result == 2
    captured = capsys.readouterr()
    parsed = json.loads(captured.err)
    assert parsed["error"] == "auth_failed"
    assert "hint" in parsed


def test_main_not_supported_error_json_format(capsys):
    """--format json 指定時に NotSupportedError が JSON で stderr に出力される（web_url は hint に）。"""
    import json

    def raise_nse(args, *, fmt, jq=None):
        raise NotSupportedError("github", "delete-repo", web_url="https://github.com/settings")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_nse}):
        result = main(["--format", "json", "pr", "list"])
    assert result == 5
    captured = capsys.readouterr()
    parsed = json.loads(captured.err)
    assert parsed["error"] == "not_supported"
    assert parsed["hint"] == "https://github.com/settings"
    assert captured.out == ""  # web_url は stdout に出力されない


def test_main_not_supported_error_text_format_still_prints_web_url(capsys):
    """--format table（デフォルト）では NotSupportedError の web_url は stdout に出力される。"""

    def raise_nse(args, *, fmt, jq=None):
        raise NotSupportedError("github", "delete-repo", web_url="https://github.com/settings")

    with (
        patch.dict(_DISPATCH, {("pr", "list"): raise_nse}),
        patch("gfo.cli.get_configured_output_format", return_value="table"),
    ):
        result = main(["pr", "list"])
    assert result == 5
    captured = capsys.readouterr()
    assert "https://github.com/settings" in captured.out
    assert "github" in captured.err


# ── _ensure_utf8_stdio のテスト ──


def test_ensure_utf8_stdio_reconfigures_non_utf8(monkeypatch):
    """encoding が utf-8 でないストリームは reconfigure される。"""
    called = {}

    class FakeStream:
        encoding = "cp932"

        def reconfigure(self, *, encoding):
            called[encoding] = True

    fake = FakeStream()
    monkeypatch.setattr("sys.stdout", fake)
    monkeypatch.setattr("sys.stderr", fake)
    _ensure_utf8_stdio()
    assert called.get("utf-8") is True


def test_main_auth_error_returns_2(capsys):
    def raise_err(args, *, fmt, jq=None):
        raise AuthError("github.com")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_err}):
        result = main(["pr", "list"])
    assert result == 2
    assert "github.com" in capsys.readouterr().err


def test_main_authentication_error_returns_2(capsys):
    def raise_err(args, *, fmt, jq=None):
        raise AuthenticationError(401)

    with patch.dict(_DISPATCH, {("pr", "list"): raise_err}):
        result = main(["pr", "list"])
    assert result == 2


def test_main_not_found_error_returns_3(capsys):
    def raise_err(args, *, fmt, jq=None):
        raise NotFoundError()

    with patch.dict(_DISPATCH, {("pr", "list"): raise_err}):
        result = main(["pr", "list"])
    assert result == 3


def test_main_rate_limit_error_returns_4(capsys):
    def raise_err(args, *, fmt, jq=None):
        raise RateLimitError(retry_after=60)

    with patch.dict(_DISPATCH, {("pr", "list"): raise_err}):
        result = main(["pr", "list"])
    assert result == 4


def test_main_config_error_returns_6(capsys):
    def raise_err(args, *, fmt, jq=None):
        raise ConfigError("bad config")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_err}):
        result = main(["pr", "list"])
    assert result == 6


def test_main_network_error_returns_7(capsys):
    def raise_err(args, *, fmt, jq=None):
        raise NetworkError("Connection refused")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_err}):
        result = main(["pr", "list"])
    assert result == 7


# ── _ensure_utf8_stdio のテスト ──


def test_ensure_utf8_stdio_skips_utf8_stream(monkeypatch):
    """既に utf-8 のストリームは reconfigure されない。"""
    called = []

    class FakeStream:
        encoding = "utf-8"

        def reconfigure(self, *, encoding):
            called.append(encoding)

    fake = FakeStream()
    monkeypatch.setattr("sys.stdout", fake)
    monkeypatch.setattr("sys.stderr", fake)
    _ensure_utf8_stdio()
    assert called == []


# ── argparse エラーの構造化テスト (M8) ──


def test_main_argparse_error_returns_config_exit_code(capsys):
    """不正な引数で ConfigError が発生し、exit_code 6 を返す。"""
    result = main(["pr", "list", "--unknown-flag"])
    assert result == 6
    err = capsys.readouterr().err
    assert "unrecognized arguments" in err


def test_main_argparse_error_json_format(capsys):
    """--format json 指定時に argparse エラーが JSON 構造化される。"""
    result = main(["--format", "json", "pr", "list", "--unknown-flag"])
    assert result == 6
    err = capsys.readouterr().err
    import json

    data = json.loads(err)
    assert data["error"] == "config_error"


# ── _pre_parse_format のテスト ──


def test_pre_parse_format_none():
    assert _pre_parse_format([]) is None
    assert _pre_parse_format(["pr", "list"]) is None


def test_pre_parse_format_flag():
    assert _pre_parse_format(["--format", "json"]) == "json"
    assert _pre_parse_format(["--format=plain"]) == "plain"


def test_pre_parse_format_jq():
    assert _pre_parse_format(["--jq", ".command"]) == "json"


# ── --remote グローバルオプションのテスト ──


def test_parser_remote_option():
    parser, _ = create_parser()
    args = parser.parse_args(["--remote", "github", "pr", "list"])
    assert args.global_remote == "github"


def test_parser_no_remote_host_defaults():
    """--remote 未指定時のデフォルトは None。"""
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "list"])
    assert args.global_remote is None


def test_main_sets_context_var_remote():
    """main() が --remote で cli_remote ContextVar を設定する。"""
    from gfo._context import cli_remote

    captured_value = None

    def capture_handler(args, *, fmt, jq=None):
        nonlocal captured_value
        captured_value = cli_remote.get()

    with patch.dict(_DISPATCH, {("pr", "list"): capture_handler}):
        main(["--remote", "upstream", "pr", "list"])

    assert captured_value == "upstream"
    # main() 終了後はリセットされている
    assert cli_remote.get() is None


def test_main_resets_context_var_on_error():
    """main() でエラーが発生しても ContextVar はリセットされる。"""
    from gfo._context import cli_remote

    def raise_handler(args, *, fmt, jq=None):
        raise GfoError("test error")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_handler}):
        main(["--remote", "upstream", "pr", "list"])

    assert cli_remote.get() is None


# ── --repo グローバルオプションのテスト ──


def test_parser_repo_option():
    parser, _ = create_parser()
    args = parser.parse_args(["--repo", "github.com/owner/repo", "pr", "list"])
    assert args.global_repo == "github.com/owner/repo"


def test_parser_no_repo_defaults():
    """--repo 未指定時のデフォルトは None。"""
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "list"])
    assert args.global_repo is None


def test_main_sets_context_var_repo():
    """main() が --repo で cli_repo ContextVar を設定する。"""
    from gfo._context import cli_repo

    captured_value = None

    def capture_handler(args, *, fmt, jq=None):
        nonlocal captured_value
        captured_value = cli_repo.get()

    with patch.dict(_DISPATCH, {("pr", "list"): capture_handler}):
        main(["--repo", "github.com/owner/repo", "pr", "list"])

    assert captured_value == "github.com/owner/repo"
    # main() 終了後はリセットされている
    assert cli_repo.get() is None


def test_main_resets_context_var_repo_on_error():
    """main() でエラーが発生しても cli_repo ContextVar はリセットされる。"""
    from gfo._context import cli_repo

    def raise_handler(args, *, fmt, jq=None):
        raise GfoError("test error")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_handler}):
        main(["--repo", "github.com/owner/repo", "pr", "list"])

    assert cli_repo.get() is None


def test_main_repo_and_remote_mutually_exclusive(capsys):
    """--repo と --remote の同時指定はエラーになる。"""
    exit_code = main(["--repo", "github.com/owner/repo", "--remote", "upstream", "pr", "list"])
    assert exit_code != 0
    captured = capsys.readouterr()
    assert "--repo" in captured.err
    assert "--remote" in captured.err


# ── --account グローバルオプションのテスト ──


def test_parser_account_option():
    parser, _ = create_parser()
    args = parser.parse_args(["--account", "work", "pr", "list"])
    assert args.global_account == "work"


def test_parser_no_account_defaults():
    """--account 未指定時のデフォルトは None。"""
    parser, _ = create_parser()
    args = parser.parse_args(["pr", "list"])
    assert args.global_account is None


def test_main_sets_context_var_account():
    """main() が --account で cli_account ContextVar を設定する。"""
    from gfo._context import cli_account

    captured_value = None

    def capture_handler(args, *, fmt, jq=None):
        nonlocal captured_value
        captured_value = cli_account.get()

    with patch.dict(_DISPATCH, {("pr", "list"): capture_handler}):
        main(["--account", "work", "pr", "list"])

    assert captured_value == "work"
    # main() 終了後はリセットされている
    assert cli_account.get() is None


def test_main_resets_context_var_account_on_error():
    """main() でエラーが発生しても cli_account ContextVar はリセットされる。"""
    from gfo._context import cli_account

    def raise_handler(args, *, fmt, jq=None):
        raise GfoError("test error")

    with patch.dict(_DISPATCH, {("pr", "list"): raise_handler}):
        main(["--account", "work", "pr", "list"])

    assert cli_account.get() is None


# ── auth switch パーサーのテスト ──


def test_parser_auth_switch():
    parser, _ = create_parser()
    args = parser.parse_args(["auth", "switch", "work"])
    assert args.command == "auth"
    assert args.subcommand == "switch"
    assert args.account == "work"


def test_parser_auth_switch_with_host():
    parser, _ = create_parser()
    args = parser.parse_args(["auth", "switch", "work", "--host", "github.com"])
    assert args.account == "work"
    assert args.host == "github.com"


# ── auth login --account パーサーのテスト ──


def test_parser_auth_login_account():
    parser, _ = create_parser()
    args = parser.parse_args(["auth", "login", "--host", "github.com", "--account", "work"])
    assert args.account == "work"


def test_parser_auth_login_account_default():
    parser, _ = create_parser()
    args = parser.parse_args(["auth", "login", "--host", "github.com"])
    assert args.account == "default"


class TestSshKeyViewStringId:
    def test_accepts_string_id(self):
        """ssh-key view が文字列 ID を受け付けることを確認。"""
        parser, _ = create_parser()
        args = parser.parse_args(["ssh-key", "view", "abc123"])
        assert args.id == "abc123"


class TestGpgKeyViewStringId:
    def test_accepts_string_id(self):
        """gpg-key view が文字列 ID を受け付けることを確認。"""
        parser, _ = create_parser()
        args = parser.parse_args(["gpg-key", "view", "def456"])
        assert args.id == "def456"
