import json
from unittest.mock import patch

from hermes_cli.codex_models import DEFAULT_CODEX_MODELS, get_codex_model_ids


def test_get_codex_model_ids_prioritizes_default_and_cache(tmp_path, monkeypatch):
    codex_home = tmp_path / "codex-home"
    codex_home.mkdir(parents=True, exist_ok=True)
    (codex_home / "config.toml").write_text('model = "gpt-5.2-codex"\n')
    (codex_home / "models_cache.json").write_text(
        json.dumps(
            {
                "models": [
                    {"slug": "gpt-5.3-codex", "priority": 20, "supported_in_api": True},
                    {"slug": "gpt-5.3-codex-spark", "priority": 6, "supported_in_api": False},
                    {"slug": "gpt-5.1-codex", "priority": 5, "supported_in_api": True},
                    {"slug": "gpt-5.4", "priority": 1, "supported_in_api": True},
                    {"slug": "gpt-5-hidden-codex", "priority": 2, "visibility": "hidden"},
                ]
            }
        )
    )
    monkeypatch.setenv("CODEX_HOME", str(codex_home))

    models = get_codex_model_ids()

    assert models[0] == "gpt-5.2-codex"
    assert "gpt-5.1-codex" in models
    assert "gpt-5.3-codex" in models
    # Codex CLI marks Spark unsupported in the public API, but the Codex
    # backend still accepts it via the OAuth-backed CLI/Hermes route.
    assert "gpt-5.3-codex-spark" in models
    # Non-codex-suffixed models are included when the cache says they're available
    assert "gpt-5.4" in models
    assert "gpt-5.4-mini" in models
    assert "gpt-5-hidden-codex" not in models


def test_setup_wizard_codex_import_resolves():
    """Regression test for #712: setup.py must import the correct function name."""
    # This mirrors the exact import used in hermes_cli/setup.py line 873.
    # A prior bug had 'get_codex_models' (wrong) instead of 'get_codex_model_ids'.
    from hermes_cli.codex_models import get_codex_model_ids as setup_import
    assert callable(setup_import)


def test_get_codex_model_ids_falls_back_to_curated_defaults(tmp_path, monkeypatch):
    codex_home = tmp_path / "codex-home"
    codex_home.mkdir(parents=True, exist_ok=True)
    monkeypatch.setenv("CODEX_HOME", str(codex_home))

    models = get_codex_model_ids()

    assert models[: len(DEFAULT_CODEX_MODELS)] == DEFAULT_CODEX_MODELS
    assert "gpt-5.4" in models
    assert "gpt-5.3-codex-spark" in models


def test_get_codex_model_ids_adds_forward_compat_models_from_templates(monkeypatch):
    monkeypatch.setattr(
        "hermes_cli.codex_models._fetch_models_from_api",
        lambda access_token: ["gpt-5.3-codex"],
    )

    models = get_codex_model_ids(access_token="codex-access-token")

    # When live discovery only returns gpt-5.3-codex, forward-compat synthesis
    # should surface gpt-5.5, gpt-5.4, gpt-5.4-mini, and gpt-5.3-codex-spark
    # (each is templated off gpt-5.3-codex).
    assert models == [
        "gpt-5.3-codex",
        "gpt-5.5",
        "gpt-5.4-mini",
        "gpt-5.4",
        "gpt-5.3-codex-spark",
    ]


def test_fetch_from_api_keeps_supported_in_api_false_models(monkeypatch):
    """Regression: gpt-5.3-codex-spark is returned by the live Codex backend
    with ``supported_in_api: false`` because it isn't in the public OpenAI
    API. The Codex CLI / OAuth route still serves it for ChatGPT Pro
    accounts, so we must not drop it on that flag. visibility=hidden is
    the separate signal that *should* still filter entries out.
    """
    import sys
    from hermes_cli import codex_models

    class _FakeResp:
        status_code = 200

        def json(self):
            return {
                "models": [
                    {"slug": "gpt-5.5", "priority": 0, "supported_in_api": True},
                    {"slug": "gpt-5.3-codex-spark", "priority": 7, "supported_in_api": False},
                    {"slug": "gpt-5-internal", "priority": 99, "visibility": "hidden"},
                ]
            }

    class _FakeHttpx:
        @staticmethod
        def get(url, headers=None, timeout=None):
            return _FakeResp()

    monkeypatch.setitem(sys.modules, "httpx", _FakeHttpx)

    models = codex_models._fetch_models_from_api(access_token="tok")

    assert "gpt-5.5" in models
    assert "gpt-5.3-codex-spark" in models
    assert "gpt-5-internal" not in models


def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch):
    from hermes_cli.main import _model_flow_openai_codex

    captured = {}
    choices = iter(["1"])

    monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
    monkeypatch.setattr(
        "hermes_cli.auth.get_codex_auth_status",
        lambda: {"logged_in": True},
    )
    monkeypatch.setattr(
        "hermes_cli.auth.resolve_codex_runtime_credentials",
        lambda *args, **kwargs: {"api_key": "codex-access-token"},
    )

    def _fake_get_codex_model_ids(access_token=None):
        captured["access_token"] = access_token
        return ["gpt-5.2-codex", "gpt-5.2"]

    def _fake_prompt_model_selection(model_ids, current_model=""):
        captured["model_ids"] = list(model_ids)
        captured["current_model"] = current_model
        return None

    monkeypatch.setattr(
        "hermes_cli.codex_models.get_codex_model_ids",
        _fake_get_codex_model_ids,
    )
    monkeypatch.setattr(
        "hermes_cli.auth._prompt_model_selection",
        _fake_prompt_model_selection,
    )

    _model_flow_openai_codex({}, current_model="openai/gpt-5.4")

    assert captured["access_token"] == "codex-access-token"
    assert captured["model_ids"] == ["gpt-5.2-codex", "gpt-5.2"]
    assert captured["current_model"] == "openai/gpt-5.4"


def test_model_command_prompts_to_reuse_or_reauthenticate_codex_session(monkeypatch, capsys):
    from hermes_cli.main import _model_flow_openai_codex

    captured = {"login_calls": 0}
    choices = iter(["2"])

    monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
    monkeypatch.setattr(
        "hermes_cli.auth.get_codex_auth_status",
        lambda: {"logged_in": True, "source": "hermes-auth-store"},
    )
    monkeypatch.setattr(
        "hermes_cli.auth.resolve_codex_runtime_credentials",
        lambda *args, **kwargs: {"api_key": "fresh-codex-token"},
    )

    def _fake_login(*args, force_new_login=False, **kwargs):
        captured["login_calls"] += 1
        captured["force_new_login"] = force_new_login

    monkeypatch.setattr("hermes_cli.auth._login_openai_codex", _fake_login)
    monkeypatch.setattr(
        "hermes_cli.codex_models.get_codex_model_ids",
        lambda access_token=None: ["gpt-5.4", "gpt-5.3-codex"],
    )
    monkeypatch.setattr(
        "hermes_cli.auth._prompt_model_selection",
        lambda model_ids, current_model="": None,
    )

    _model_flow_openai_codex({}, current_model="gpt-5.4")

    out = capsys.readouterr().out
    assert "Use existing credentials" in out
    assert "Reauthenticate (new OAuth login)" in out
    assert captured["login_calls"] == 1
    assert captured["force_new_login"] is True


def test_model_command_uses_existing_codex_session_without_relogin(monkeypatch):
    from hermes_cli.main import _model_flow_openai_codex

    choices = iter(["1"])
    captured = {}

    monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
    monkeypatch.setattr(
        "hermes_cli.auth.get_codex_auth_status",
        lambda: {"logged_in": True, "source": "hermes-auth-store"},
    )
    monkeypatch.setattr(
        "hermes_cli.auth.resolve_codex_runtime_credentials",
        lambda *args, **kwargs: {"api_key": "existing-codex-token"},
    )

    def _fake_get_codex_model_ids(access_token=None):
        captured["access_token"] = access_token
        return ["gpt-5.4"]

    monkeypatch.setattr(
        "hermes_cli.codex_models.get_codex_model_ids",
        _fake_get_codex_model_ids,
    )
    monkeypatch.setattr(
        "hermes_cli.auth._prompt_model_selection",
        lambda model_ids, current_model="": None,
    )
    monkeypatch.setattr(
        "hermes_cli.auth._login_openai_codex",
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not reauthenticate")),
    )

    _model_flow_openai_codex({}, current_model="gpt-5.4")

    assert captured["access_token"] == "existing-codex-token"


# ── Tests for _normalize_model_for_provider ──────────────────────────


def _make_cli(model="anthropic/claude-opus-4.6", **kwargs):
    """Create a HermesCLI with minimal mocking."""
    import cli as _cli_mod
    from cli import HermesCLI

    _clean_config = {
        "model": {
            "default": "anthropic/claude-opus-4.6",
            "base_url": "https://openrouter.ai/api/v1",
            "provider": "auto",
        },
        "display": {"compact": False, "tool_progress": "all", "resume_display": "full"},
        "agent": {},
        "terminal": {"env_type": "local"},
    }
    clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
    with (
        patch("cli.get_tool_definitions", return_value=[]),
        patch.dict("os.environ", clean_env, clear=False),
        patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}),
    ):
        cli = HermesCLI(model=model, **kwargs)
    return cli


class TestNormalizeModelForProvider:
    """_normalize_model_for_provider() trusts user-selected models.

    Only two things happen:
    1. Provider prefixes are stripped (API needs bare slugs)
    2. The *untouched default* model is swapped for a Codex model
    Everything else passes through — the API is the judge.
    """

    def test_non_codex_provider_is_noop(self):
        cli = _make_cli(model="gpt-5.4")
        changed = cli._normalize_model_for_provider("openrouter")
        assert changed is False
        assert cli.model == "gpt-5.4"

    def test_native_provider_prefix_is_stripped_before_agent_startup(self):
        cli = _make_cli(model="zai/glm-5.1")
        changed = cli._normalize_model_for_provider("zai")
        assert changed is True
        assert cli.model == "glm-5.1"

    def test_bare_codex_model_passes_through(self):
        cli = _make_cli(model="gpt-5.3-codex")
        changed = cli._normalize_model_for_provider("openai-codex")
        assert changed is False
        assert cli.model == "gpt-5.3-codex"

    def test_bare_non_codex_model_passes_through(self):
        """gpt-5.4 (no 'codex' suffix) passes through — user chose it."""
        cli = _make_cli(model="gpt-5.4")
        changed = cli._normalize_model_for_provider("openai-codex")
        assert changed is False
        assert cli.model == "gpt-5.4"

    def test_any_bare_model_trusted(self):
        """Even a non-OpenAI bare model passes through — user explicitly set it."""
        cli = _make_cli(model="claude-opus-4-6")
        changed = cli._normalize_model_for_provider("openai-codex")
        # User explicitly chose this model — we trust them, API will error if wrong
        assert changed is False
        assert cli.model == "claude-opus-4-6"

    def test_provider_prefix_stripped(self):
        """openai/gpt-5.4 → gpt-5.4 (strip prefix, keep model)."""
        cli = _make_cli(model="openai/gpt-5.4")
        changed = cli._normalize_model_for_provider("openai-codex")
        assert changed is True
        assert cli.model == "gpt-5.4"

    def test_any_provider_prefix_stripped(self):
        """anthropic/claude-opus-4.6 → claude-opus-4.6 (strip prefix only).
        User explicitly chose this — let the API decide if it works."""
        cli = _make_cli(model="anthropic/claude-opus-4.6")
        changed = cli._normalize_model_for_provider("openai-codex")
        assert changed is True
        assert cli.model == "claude-opus-4.6"

    def test_opencode_go_prefix_stripped(self):
        cli = _make_cli(model="opencode-go/kimi-k2.5")
        cli.api_mode = "chat_completions"
        changed = cli._normalize_model_for_provider("opencode-go")
        assert changed is True
        assert cli.model == "kimi-k2.5"
        assert cli.api_mode == "chat_completions"

    def test_opencode_zen_claude_sets_messages_mode(self):
        cli = _make_cli(model="opencode-zen/claude-sonnet-4-6")
        cli.api_mode = "chat_completions"
        changed = cli._normalize_model_for_provider("opencode-zen")
        assert changed is True
        assert cli.model == "claude-sonnet-4-6"
        assert cli.api_mode == "anthropic_messages"

    def test_default_model_replaced(self):
        """No model configured (empty default) gets swapped for codex."""
        import cli as _cli_mod
        _clean_config = {
            "model": {
                "default": "",
                "base_url": "",
                "provider": "auto",
            },
            "display": {"compact": False, "tool_progress": "all", "resume_display": "full"},
            "agent": {},
            "terminal": {"env_type": "local"},
        }
        # Don't pass model= so _model_is_default is True
        with (
            patch("cli.get_tool_definitions", return_value=[]),
            patch.dict("os.environ", {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}, clear=False),
            patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}),
        ):
            from cli import HermesCLI
            cli = HermesCLI()

        assert cli._model_is_default is True
        with patch(
            "hermes_cli.codex_models.get_codex_model_ids",
            return_value=["gpt-5.3-codex", "gpt-5.4"],
        ):
            changed = cli._normalize_model_for_provider("openai-codex")
        assert changed is True
        # Uses first from available list
        assert cli.model == "gpt-5.3-codex"

    def test_default_fallback_when_api_fails(self):
        """No model configured falls back to gpt-5.3-codex when API unreachable."""
        import cli as _cli_mod
        _clean_config = {
            "model": {
                "default": "",
                "base_url": "",
                "provider": "auto",
            },
            "display": {"compact": False, "tool_progress": "all", "resume_display": "full"},
            "agent": {},
            "terminal": {"env_type": "local"},
        }
        with (
            patch("cli.get_tool_definitions", return_value=[]),
            patch.dict("os.environ", {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}, clear=False),
            patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}),
        ):
            from cli import HermesCLI
            cli = HermesCLI()

        with patch(
            "hermes_cli.codex_models.get_codex_model_ids",
            side_effect=Exception("offline"),
        ):
            changed = cli._normalize_model_for_provider("openai-codex")
        assert changed is True
        assert cli.model == "gpt-5.3-codex"
