from __future__ import annotations

import base64
import json
import os
import re
import signal
import subprocess
import sys
from pathlib import Path
from collections.abc import Mapping
from typing import cast

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import claudex
import model_catalog
from claude_credentials import CredentialAuthority, ResolvedCredentials
from authority_client import AuthorityDataPlane, GatewayStatus


@pytest.fixture(autouse=True)
def proxy_scope(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv(claudex.PROXY_SCOPE_MARKER, "1")


def jwt(exp: int) -> str:
    payload = base64.urlsafe_b64encode(json.dumps({"exp": exp}).encode()).decode().rstrip("=")
    return f"header.{payload}.signature"


def native_auth(exp: int, refresh: str = "native-refresh") -> dict[str, object]:
    return {
        "OPENAI_API_KEY": None,
        "auth_mode": "chatgpt",
        "last_refresh": "2026-07-22T00:00:00Z",
        "tokens": {
            "access_token": jwt(exp),
            "refresh_token": refresh,
            "account_id": "account-1",
            "id_token": "keep-id-token",
        },
    }


def proxy_auth(exp: int, refresh: str = "proxy-refresh") -> dict[str, object]:
    return {
        "access": jwt(exp),
        "refresh": refresh,
        "expires": exp * 1000,
        "accountId": "account-1",
    }


def write_json(path: Path, payload: dict[str, object]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload), encoding="utf-8")


def test_enter_proxy_slice_reexecs_claudex_in_unlimited_slice(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    command: list[str] = []

    def fake_execv(path: str, argv: list[str]) -> None:
        assert path == "/bin/systemd-run"
        command.extend(argv)
        raise RuntimeError("exec")

    monkeypatch.delenv(claudex.PROXY_SCOPE_MARKER)
    monkeypatch.setattr(claudex.shutil, "which", lambda name: "/bin/systemd-run")
    monkeypatch.setattr(claudex.sys, "argv", ["/bin/claudex", "-m", "sol"])
    monkeypatch.setattr(claudex.os, "getpid", lambda: 321)
    monkeypatch.setattr(claudex.os, "execv", fake_execv)

    with pytest.raises(RuntimeError, match="exec"):
        claudex.enter_proxy_slice()

    assert command == [
        "/bin/systemd-run",
        "--user",
        "--scope",
        "--quiet",
        "--collect",
        "--same-dir",
        "--slice=claudex-proxy.slice",
        "--unit=claudex-proxy-321.scope",
        "--setenv=CLAUDEX_PROXY_SCOPE=1",
        "/bin/claudex",
        "-m",
        "sol",
    ]


def test_parent_death_signal_terminates_proxy_with_claudex(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    calls: list[tuple[int, signal.Signals]] = []

    class FakeLibc:
        def prctl(self, option: int, death_signal: signal.Signals) -> int:
            calls.append((option, death_signal))
            return 0

    monkeypatch.setattr(claudex.ctypes, "CDLL", lambda *args, **kwargs: FakeLibc())
    monkeypatch.setattr(claudex.os, "getppid", lambda: 123)

    claudex._set_parent_death_signal(123)

    assert calls == [(1, signal.SIGTERM)]


def test_resolve_selected_account_uses_current_systray_default(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    base = tmp_path / ".local/state/overdeck/systray/runtime"
    auth_path = base / "accounts" / "rafa" / "CODEX_HOME" / "auth.json"
    write_json(auth_path, native_auth(200))
    (base / "default_slug").write_text("rafa\n", encoding="utf-8")
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)

    selected = claudex.resolve_selected_account()

    assert selected.slug == "rafa"
    assert selected.native_auth_path == auth_path
    assert selected.proxy_config_dir == base / "accounts" / "rafa" / "CLAUDEX_HOME"


def test_native_auth_converts_to_proxy_shape() -> None:
    converted = claudex.native_to_proxy_auth(native_auth(200))

    assert converted == {
        "access": jwt(200),
        "refresh": "native-refresh",
        "expires": 200_000,
        "accountId": "account-1",
    }


def test_reconcile_exports_newer_proxy_tokens_without_clobbering_native_fields(
    tmp_path: Path,
) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_path = tmp_path / "CLAUDEX_HOME" / "codex" / "auth.json"
    write_json(native_path, native_auth(100))
    write_json(proxy_path, proxy_auth(200))

    claudex.reconcile_credentials(native_path, proxy_path)

    native = json.loads(native_path.read_text(encoding="utf-8"))
    assert native["tokens"]["access_token"] == jwt(200)
    assert native["tokens"]["refresh_token"] == "proxy-refresh"
    assert native["tokens"]["id_token"] == "keep-id-token"
    assert native["auth_mode"] == "chatgpt"
    assert native_path.stat().st_mode & 0o777 == 0o600


def test_reconcile_imports_newer_native_tokens(tmp_path: Path) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_path = tmp_path / "CLAUDEX_HOME" / "codex" / "auth.json"
    write_json(native_path, native_auth(300))
    write_json(proxy_path, proxy_auth(200))

    claudex.reconcile_credentials(native_path, proxy_path)

    assert json.loads(proxy_path.read_text(encoding="utf-8")) == {
        "access": jwt(300),
        "refresh": "native-refresh",
        "expires": 300_000,
        "accountId": "account-1",
    }
    assert proxy_path.stat().st_mode & 0o777 == 0o600


def test_reconcile_rejects_equal_expiry_divergent_refresh_tokens(tmp_path: Path) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_path = tmp_path / "CLAUDEX_HOME" / "codex" / "auth.json"
    write_json(native_path, native_auth(200))
    write_json(proxy_path, proxy_auth(200))

    with pytest.raises(claudex.CredentialConflictError):
        claudex.reconcile_credentials(native_path, proxy_path)


def test_run_launches_proxy_and_claude_with_requested_sol_model(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_dir = tmp_path / "CLAUDEX_HOME"
    write_json(native_path, native_auth(200))
    selected = claudex.SelectedAccount("rafa", native_path, proxy_dir)
    calls: list[tuple[list[str], dict[str, str]]] = []

    class FakeProxy:
        returncode = None

        def poll(self) -> None:
            return None

        def terminate(self) -> None:
            calls.append((["terminate"], {}))

        def wait(self, timeout: float | None = None) -> int:
            return 0

        def kill(self) -> None:
            raise AssertionError("proxy should terminate cleanly")

    def fake_popen(args: list[str], **kwargs: object) -> FakeProxy:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return FakeProxy()

    def fake_run(args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return subprocess.CompletedProcess(args, 7)

    monkeypatch.setattr(claudex, "resolve_selected_account", lambda slug=None: selected)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
    monkeypatch.setattr(claudex.shutil, "which", lambda name: f"/bin/{name}")
    monkeypatch.setattr(claudex, "find_available_port", lambda: 23456)
    monkeypatch.setattr(claudex, "wait_for_proxy", lambda process, port: None)
    monkeypatch.setattr(claudex.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)
    monkeypatch.setenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "stale-opus")
    monkeypatch.setenv("ANTHROPIC_DEFAULT_SONNET_MODEL", "stale-sonnet")
    monkeypatch.setenv("ANTHROPIC_DEFAULT_HAIKU_MODEL", "stale-haiku")
    monkeypatch.setenv("ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES", "stale")
    monkeypatch.setenv("ANTHROPIC_DEFAULT_FABLE_MODEL", "stale-fable")
    monkeypatch.setenv("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "1")
    monkeypatch.setenv("CLAUDE_CODE_SUBAGENT_MODEL", "stale-subagent")
    monkeypatch.setenv("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1")

    result = claudex.run(["--model", "sol"])

    assert result == 7
    proxy_args, proxy_env = calls[0]
    assert proxy_args == ["/bin/claude-code-proxy", "serve", "--no-monitor"]
    assert proxy_env["CCP_CONFIG_DIR"] == str(proxy_dir)
    assert proxy_env["CCP_BIND_ADDRESS"] == "127.0.0.1"
    assert proxy_env["PORT"] == "23456"
    assert "CCP_CODEX_MODEL" not in proxy_env
    assert proxy_env["CCP_CODEX_SERVICE_TIER"] == "fast"
    claude_args, claude_env = calls[1]
    assert claude_args == [
        "/bin/claude",
        "--dangerously-skip-permissions",
        "--settings",
        str(proxy_dir / "claude-settings.json"),
    ]
    assert json.loads(
        (proxy_dir / "claude-settings.json").read_text(encoding="utf-8")
    ) == {
        "availableModels": [
            "gpt-5.6-sol",
            "gpt-5.6-terra",
            "gpt-5.6-luna",
        ]
    }
    assert claude_env["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:23456"
    assert claude_env["ANTHROPIC_MODEL"] == "gpt-5.6-sol"
    assert claude_env["ANTHROPIC_SMALL_FAST_MODEL"] == "gpt-5.6-luna"
    assert claude_env["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "gpt-5.6-sol"
    assert claude_env["ANTHROPIC_DEFAULT_OPUS_MODEL_NAME"] == "GPT-5.6 Sol"
    assert (
        claude_env["ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION"]
        == "GPT-5.6 Sol through claude-code-proxy"
    )
    assert claude_env["ANTHROPIC_DEFAULT_SONNET_MODEL"] == "gpt-5.6-terra"
    assert claude_env["ANTHROPIC_DEFAULT_SONNET_MODEL_NAME"] == "GPT-5.6 Terra"
    assert claude_env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] == "gpt-5.6-luna"
    assert claude_env["ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME"] == "GPT-5.6 Luna"
    assert "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES" not in claude_env
    assert "ANTHROPIC_DEFAULT_FABLE_MODEL" not in claude_env
    assert "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY" not in claude_env
    assert "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC" not in claude_env
    assert "CLAUDE_CODE_SUBAGENT_MODEL" not in claude_env
    assert claude_env["CLAUDE_CODE_ALWAYS_ENABLE_EFFORT"] == "1"
    assert claude_env["CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY"] == "3"
    assert claude_env["SYSTRAY_CODEX_ACCOUNT_SLUG"] == "rafa"
    assert claude_env["SYSTRAY_CODEX_ACCOUNT_HOME"] == str(native_path.parent)
    assert claude_env["ENABLE_TOOL_SEARCH"] == "false"
    assert "CLAUDE_CODE_AUTO_COMPACT_WINDOW" not in claude_env
    assert calls[2][0] == ["terminate"]


def test_run_with_pinned_account_falls_back_to_codex_only_when_claude_missing(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_dir = tmp_path / "CLAUDEX_HOME"
    write_json(native_path, native_auth(200))
    selected = claudex.SelectedAccount("zync2", native_path, proxy_dir)
    calls: list[tuple[list[str], dict[str, str]]] = []

    class FakeProxy:
        returncode = None

        def poll(self) -> None:
            return None

        def terminate(self) -> None:
            calls.append((["terminate"], {}))

        def wait(self, timeout: float | None = None) -> int:
            return 0

    def fake_popen(args: list[str], **kwargs: object) -> FakeProxy:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return FakeProxy()

    def fake_run(args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return subprocess.CompletedProcess(args, 9)

    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    monkeypatch.setattr(claudex, "resolve_selected_account", lambda slug=None: selected)
    monkeypatch.setattr(claudex, "claude_account_exists", lambda slug: False)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
    monkeypatch.setattr(claudex.shutil, "which", lambda name: f"/bin/{name}")
    monkeypatch.setattr(claudex, "find_available_port", lambda: 23456)
    monkeypatch.setattr(claudex, "wait_for_proxy", lambda process, port: None)
    monkeypatch.setattr(claudex.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)

    result = claudex.run(["--account=zync2"])

    assert result == 9
    settings_path = proxy_dir / "claude-settings.json"
    assert json.loads(settings_path.read_text(encoding="utf-8")) == {
        "availableModels": [
            "gpt-5.6-sol",
            "gpt-5.6-terra",
            "gpt-5.6-luna",
        ]
    }
    claude_args, claude_env = calls[1]
    assert claude_args == [
        "/bin/claude",
        "--dangerously-skip-permissions",
        "--settings",
        str(settings_path),
    ]
    assert claude_env["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:23456"
    assert claude_env["ANTHROPIC_MODEL"] == "gpt-5.6-sol"
    assert "CLAUDE_CONFIG_DIR" not in claude_env
    assert calls[2][0] == ["terminate"]


def test_run_without_model_launches_hybrid_codex_and_anthropic_session(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_dir = tmp_path / "CLAUDEX_HOME"
    claude_home = tmp_path / "CLAUDE_HOME"
    write_json(native_path, native_auth(200))
    selected = claudex.SelectedAccount("rafa", native_path, proxy_dir)
    account = claudex.ClaudeAccount("rafa", claude_home, {"CLAUDE_CONFIG_DIR": str(claude_home)})
    calls: list[tuple[list[str], dict[str, str]]] = []

    class FakeProxy:
        returncode = None

        def poll(self) -> None:
            return None

        def terminate(self) -> None:
            calls.append((["terminate"], {}))

        def wait(self, timeout: float | None = None) -> int:
            return 0

    def fake_popen(args: list[str], **kwargs: object) -> FakeProxy:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return FakeProxy()

    def fake_run(args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return subprocess.CompletedProcess(args, 5)

    monkeypatch.setattr(claudex, "resolve_selected_account", lambda slug=None: selected)
    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: account)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
    monkeypatch.setattr(claudex.shutil, "which", lambda name: f"/bin/{name}")
    monkeypatch.setattr(claudex, "find_available_port", lambda: 23456)
    monkeypatch.setattr(claudex, "wait_for_proxy", lambda process, port: None)
    monkeypatch.setattr(claudex.subprocess, "Popen", fake_popen)
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)
    monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "stale-token")
    monkeypatch.setenv("ANTHROPIC_API_KEY", "stale-key")
    monkeypatch.setenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "stale-opus")
    monkeypatch.setenv("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "1")

    result = claudex.run([])

    assert result == 5
    settings_path = proxy_dir / "claudex-hybrid-settings.json"
    assert json.loads(settings_path.read_text(encoding="utf-8")) == {
        "availableModels": [
            "fable",
            "opus",
            "sonnet",
            "gpt-5.6-sol",
            "gpt-5.6-terra",
            "gpt-5.6-luna",
        ]
    }
    claude_args, claude_env = calls[1]
    assert claude_args == [
        "/bin/claude",
        "--dangerously-skip-permissions",
        "--settings",
        str(settings_path),
    ]
    assert claude_env["CLAUDE_CONFIG_DIR"] == str(claude_home)
    assert claude_env["ANTHROPIC_MODEL"] == "gpt-5.6-sol"
    assert claude_env["ANTHROPIC_SMALL_FAST_MODEL"] == "gpt-5.6-luna"
    assert claude_env["ANTHROPIC_BASE_URL"].startswith("http://127.0.0.1:")
    assert claude_env["ANTHROPIC_BASE_URL"] != "http://127.0.0.1:23456"
    assert "ANTHROPIC_AUTH_TOKEN" not in claude_env
    assert "ANTHROPIC_API_KEY" not in claude_env
    assert "ANTHROPIC_DEFAULT_OPUS_MODEL" not in claude_env
    assert claude_env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] == "1"
    catalog = json.loads(
        (claude_home / "cache" / "gateway-models.json").read_text(encoding="utf-8")
    )
    assert catalog["baseUrl"] == claude_env["ANTHROPIC_BASE_URL"]
    assert catalog["models"] == [
        {"id": "gpt-5.6-terra", "display_name": "GPT-5.6 Terra"},
        {"id": "gpt-5.6-luna", "display_name": "GPT-5.6 Luna"},
        {"id": "gpt-5.6-sol", "display_name": "GPT-5.6 Sol"},
    ]
    assert claude_env["ENABLE_TOOL_SEARCH"] == "false"
    assert claude_env["SYSTRAY_CODEX_ACCOUNT_SLUG"] == "rafa"
    assert calls[2][0] == ["terminate"]


def test_run_native_launches_once_with_resolved_account_environment(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    account_home = tmp_path / "CLAUDE_HOME"
    secure_storage = tmp_path / "credentials"
    account_home.mkdir()
    account = claudex.ClaudeAccount(
        "rafa",
        account_home,
        {
            "CLAUDE_CONFIG_DIR": str(account_home),
            "CLAUDE_SECURESTORAGE_CONFIG_DIR": str(secure_storage),
        },
    )
    calls: list[tuple[list[str], dict[str, str]]] = []

    def fake_run(args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return subprocess.CompletedProcess(args, 0)

    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: account)
    monkeypatch.setattr(claudex, "_require_binary", lambda name: "/bin/claude")
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)

    result = claudex.run_native(
        "claude-opus-5",
        ["--version"],
        claudex.AccountOverrides(None, "rafa"),
    )

    assert result == 0
    assert len(calls) == 1
    args, env = calls[0]
    assert args[0:3] == ["/bin/claude", "--model", "claude-opus-5"]
    assert env["CLAUDE_CONFIG_DIR"] == str(account_home)
    assert env["CLAUDE_SECURESTORAGE_CONFIG_DIR"] == str(secure_storage)


def test_default_claude_args_do_not_duplicate_dangerous_permissions_flag() -> None:
    assert claudex.default_claude_args(["--dangerously-skip-permissions", "chat"]) == [
        "--dangerously-skip-permissions",
        "chat",
    ]


@pytest.mark.parametrize(
    ("argv", "expected_model", "forwarded"),
    [
        (["--model", "sol", "chat"], "gpt-5.6-sol", ["chat"]),
        (["--model=terra"], "gpt-5.6-terra", []),
        (["--model", "luna"], "gpt-5.6-luna", []),
        (["--model", "fable"], "claude-fable-5", []),
        (["--model", "opus"], "claude-opus-5", []),
        (["--model", "sonnet"], "claude-sonnet-5", []),
        (["chat"], None, ["chat"]),
    ],
)
def test_parse_model_accepts_claudex_model_names(
    argv: list[str], expected_model: str | None, forwarded: list[str]
) -> None:
    assert claudex.parse_model(argv) == (expected_model, forwarded)


@pytest.mark.parametrize(
    ("alias", "full_model"),
    [
        ("sol", "gpt-5.6-sol"),
        ("terra", "gpt-5.6-terra"),
        ("luna", "gpt-5.6-luna"),
    ],
)
def test_parse_model_resolves_codex_aliases_in_all_supported_flag_forms(
    alias: str, full_model: str
) -> None:
    cases = [
        (["-m", alias, "chat"], ["chat"]),
        ([f"-m={alias}", "chat"], ["chat"]),
        (["--model", alias, "chat"], ["chat"]),
        ([f"--model={alias}", "chat"], ["chat"]),
    ]

    for argv, forwarded in cases:
        assert claudex.parse_model(argv) == (full_model, forwarded)


@pytest.mark.parametrize("model", ["gpt-5.6-sol", "claude-opus-5"])
def test_parse_model_captures_full_names_of_routable_models(model: str) -> None:
    assert claudex.parse_model(["-m", model, "chat"]) == (model, ["chat"])
    assert claudex.parse_model(["--model", model, "chat"]) == (model, ["chat"])


@pytest.mark.parametrize(
    "model", ["claude-opus-4-1-20250805", "gpt-5.5", "codex-auto-review"]
)
def test_parse_model_forwards_unroutable_full_names_to_claude(model: str) -> None:
    """A model claudex cannot route itself stays a claude CLI argument.

    Capturing it would send a `claude-*` id to run_proxy, i.e. a Claude model
    dispatched through the GPT proxy.
    """
    assert claudex.parse_model(["--model", model, "chat"]) == (
        None,
        ["--model", model, "chat"],
    )
    assert claudex.parse_model([f"--model={model}", "chat"]) == (
        None,
        [f"--model={model}", "chat"],
    )
    assert claudex.parse_model(["-m", model, "chat"]) == (
        None,
        ["--model", model, "chat"],
    )


def test_parse_model_rejects_unknown_bare_alias_and_names_all_valid_aliases() -> None:
    with pytest.raises(claudex.ClaudexError) as error:
        claudex.parse_model(["-m", "banana", "chat"])

    assert "banana" in str(error.value)
    for alias in (*model_catalog.MODEL_ALIASES, *claudex.NATIVE_MODEL_ALIASES):
        assert alias in str(error.value)


def test_claudex_main_surfaces_unknown_alias_without_traceback(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    monkeypatch.setattr(
        claudex,
        "run_proxy",
        lambda *args, **kwargs: pytest.fail("proxy dispatch must not run"),
    )
    monkeypatch.setattr(
        claudex,
        "run_native",
        lambda *args, **kwargs: pytest.fail("native dispatch must not run"),
    )
    monkeypatch.setattr(
        claudex,
        "run_hybrid",
        lambda *args, **kwargs: pytest.fail("hybrid dispatch must not run"),
    )

    exit_code = claudex.main(["claudex", "-m", "banana", "chat"])
    captured = capsys.readouterr()

    assert exit_code != 0
    assert captured.out == ""
    assert captured.err == (
        "claudex: unknown model alias 'banana' "
        "(valid aliases: sol, terra, luna, fable, opus, sonnet)\n"
    )
    assert "Traceback" not in captured.err


def test_claudex_uses_shared_model_alias_table() -> None:
    assert claudex.MODEL_ALIASES is model_catalog.MODEL_ALIASES


def test_require_binary_launches_through_the_tmpjail_shim(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    shim_dir = tmp_path / ".claude" / "bin"
    real_dir = tmp_path / "real"
    shim_dir.mkdir(parents=True)
    real_dir.mkdir()
    shim_body = shim_dir / "_tmpjail-shim.sh"
    shim_body.write_text("#!/bin/sh\n", encoding="utf-8")
    shim_body.chmod(0o755)
    shim = shim_dir / "claude"
    shim.symlink_to(shim_body)
    real = real_dir / "claude"
    real.write_text("#!/bin/sh\n", encoding="utf-8")
    real.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
    # PATH omits the shim dir: an inherited PATH from a launcher that never had it is
    # exactly how a session starts uncapped.
    monkeypatch.setenv("PATH", str(real_dir))
    monkeypatch.setattr(claudex.shutil, "which", lambda name: str(real_dir / name))

    assert claudex._require_binary("claude") == str(shim)


def test_require_binary_falls_back_to_path_when_no_shim_is_installed(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    real_dir = tmp_path / "real"
    real_dir.mkdir()
    real = real_dir / "claude"
    real.write_text("#!/bin/sh\n", encoding="utf-8")
    real.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
    monkeypatch.setenv("PATH", str(real_dir))
    monkeypatch.setattr(claudex.shutil, "which", lambda name: str(real_dir / name))

    assert claudex._require_binary("claude") == str(real)


def test_resolve_claude_account_points_storage_at_the_credential_owner(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    account_home = tmp_path / "account" / "CLAUDE_HOME"
    live_home = tmp_path / "live"
    account_home.mkdir(parents=True)
    live_home.mkdir()

    class FakeRouter:
        def __init__(self, adapter: object) -> None:
            self.adapter = adapter

        def _resolve_account(self, slug: object, notify_fallback: bool = False) -> object:
            return tmp_path, "account", account_home

    monkeypatch.setattr(claudex, "CommandRouter", FakeRouter)
    monkeypatch.setattr(claudex.ClaudeAdapter, "sync_before_exec", lambda self, base: None)
    monkeypatch.setattr(
        claudex,
        "resolve_credentials",
        lambda home: ResolvedCredentials(
            live_home / ".credentials.json", CredentialAuthority.VENDOR_MANAGED
        ),
    )

    account = claudex.resolve_claude_account()

    assert account.slug == "account"
    assert account.home == account_home
    assert account.env["CLAUDE_CONFIG_DIR"] == str(account_home)
    assert account.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"] == str(live_home)


@pytest.mark.parametrize(
    ("argv", "codex", "claude", "forwarded"),
    [
        (["--codex-account", "zync", "chat"], "zync", None, ["chat"]),
        (["--codex-profile=zync"], "zync", None, []),
        (["--claude-account", "zync"], None, "zync", []),
        (["--claude-profile=zync", "--codex-account=multideal"], "multideal", "zync", []),
        (["--model", "opus"], None, None, ["--model", "opus"]),
        (["--profile", "zync"], "zync", "zync", []),
        (["--account=zync", "--claude-account", "multideal"], "zync", "multideal", []),
    ],
)
def test_parse_account_overrides_selects_each_provider(
    argv: list[str], codex: str | None, claude: str | None, forwarded: list[str]
) -> None:
    accounts, remaining = claudex.parse_account_overrides(argv)

    assert accounts == claudex.AccountOverrides(codex, claude)
    assert remaining == forwarded


@pytest.mark.parametrize("argv", [["--codex-account"], ["--claude-account="]])
def test_parse_account_overrides_rejects_a_missing_account(argv: list[str]) -> None:
    with pytest.raises(claudex.ClaudexError):
        claudex.parse_account_overrides(argv)


def test_resolve_selected_account_honors_the_requested_codex_account(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    accounts_dir = tmp_path / ".local/state/overdeck/systray/runtime" / "accounts"
    for slug in ("multideal", "zync"):
        write_json(accounts_dir / slug / "CODEX_HOME" / "auth.json", native_auth(200))
    (tmp_path / ".local/state/overdeck/systray/runtime" / "default_slug").write_text("multideal", encoding="utf-8")

    selected = claudex.resolve_selected_account("zync")

    assert selected.slug == "zync"
    assert selected.native_auth_path == accounts_dir / "zync" / "CODEX_HOME" / "auth.json"
    assert selected.proxy_config_dir == accounts_dir / "zync" / "CLAUDEX_HOME"
    with pytest.raises(claudex.ClaudexError):
        claudex.resolve_selected_account("absent")


def test_resolve_selected_account_accepts_a_registry_alias(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    accounts_dir = tmp_path / ".local/state/overdeck/systray/runtime" / "accounts"
    write_json(accounts_dir / "new-account" / "CODEX_HOME" / "auth.json", native_auth(200))
    write_json(
        tmp_path / ".local/state/overdeck/systray/runtime" / "accounts.json",
        {"accounts": [{"slug": "new-account", "alias": "zync2"}], "deleted_legacy_slugs": []},
    )

    selected = claudex.resolve_selected_account("zync2")

    assert selected.slug == "new-account"
    assert selected.native_auth_path == accounts_dir / "new-account" / "CODEX_HOME" / "auth.json"


CAP_RESUME_AT_EPOCH = 1786365653.0
CAP_RESUME_AT = "2026-08-10T12:40:53Z"


def write_codex_account(tray: Path, slug: str) -> None:
    write_json(tray / "accounts" / slug / "CODEX_HOME" / "auth.json", native_auth(200))


def write_routing_rules(tray: Path, account_caps: dict[str, object] | None) -> None:
    rules: dict[str, object] = {
        "version": "routing/v2",
        "projects": {},
        "default": "multideal",
        "fallback_chain": [],
        "fallback_trigger": "broken_or_quota_exhausted",
        "quota_exhausted_threshold_pct": 100,
    }
    if account_caps is not None:
        rules["account_caps"] = account_caps
    write_json(tray / "routing_rules.json", rules)


def write_health_cache(tray: Path, snapshots: dict[str, object]) -> None:
    write_json(tray / "health_cache.json", snapshots)


def test_resolve_selected_account_refuses_a_capped_requested_codex_account(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    tray = tmp_path / ".local/state/overdeck/systray/runtime"
    for slug in ("multideal", "zync"):
        write_codex_account(tray, slug)
    (tray / "default_slug").write_text("multideal", encoding="utf-8")
    write_routing_rules(tray, {"zync": {"7d": 10}})
    write_health_cache(
        tray,
        {
            "multideal": {"status": "ok", "primary_used_pct": 10, "secondary_used_pct": 10},
            "zync": {
                "status": "ok",
                "primary_used_pct": 10,
                "secondary_used_pct": 91,
                "secondary_reset_at": CAP_RESUME_AT_EPOCH,
            },
        },
    )

    with pytest.raises(claudex.ClaudexCappedError) as raised:
        claudex.resolve_selected_account("zync")

    assert raised.value.exit_code == 75
    assert raised.value.resume_at == CAP_RESUME_AT

    exit_code = claudex.main(["claudex", "--codex-account", "zync"])

    assert exit_code == 75
    assert capsys.readouterr().out == (
        '{"ok":false,"detail":"rate-limited","reason":"all-accounts-capped",'
        '"resume_at":"2026-08-10T12:40:53Z"}\n'
    )


def test_resolve_selected_account_refuses_a_capped_default_codex_account(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    tray = tmp_path / ".local/state/overdeck/systray/runtime"
    write_codex_account(tray, "multideal")
    (tray / "default_slug").write_text("multideal", encoding="utf-8")
    write_routing_rules(tray, {"multideal": {"5h": 15}})
    write_health_cache(
        tray,
        {"multideal": {"status": "ok", "primary_used_pct": 90, "secondary_used_pct": 10}},
    )

    with pytest.raises(claudex.ClaudexCappedError) as raised:
        claudex.resolve_selected_account()

    assert raised.value.exit_code == 75
    assert raised.value.resume_at is None

    exit_code = claudex.main(["claudex"])

    assert exit_code == 75
    assert capsys.readouterr().out == (
        '{"ok":false,"detail":"rate-limited","reason":"all-accounts-capped"}\n'
    )


def test_resolve_selected_account_keeps_an_account_below_its_cap(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    tray = tmp_path / ".local/state/overdeck/systray/runtime"
    write_codex_account(tray, "multideal")
    (tray / "default_slug").write_text("multideal", encoding="utf-8")
    write_routing_rules(tray, {"multideal": {"7d": 10}})
    write_health_cache(
        tray,
        {"multideal": {"status": "ok", "primary_used_pct": 10, "secondary_used_pct": 80}},
    )

    for selected in (
        claudex.resolve_selected_account(),
        claudex.resolve_selected_account("multideal"),
    ):
        assert selected.slug == "multideal"
        assert (
            selected.native_auth_path
            == tray / "accounts" / "multideal" / "CODEX_HOME" / "auth.json"
        )
        assert selected.proxy_config_dir == tray / "accounts" / "multideal" / "CLAUDEX_HOME"


def test_resolve_selected_account_ignores_usage_when_no_cap_is_configured(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    tray = tmp_path / ".local/state/overdeck/systray/runtime"
    write_codex_account(tray, "multideal")
    (tray / "default_slug").write_text("multideal", encoding="utf-8")
    write_routing_rules(tray, None)
    write_health_cache(
        tray,
        {"multideal": {"status": "ok", "primary_used_pct": 100, "secondary_used_pct": 100}},
    )

    for selected in (
        claudex.resolve_selected_account(),
        claudex.resolve_selected_account("multideal"),
    ):
        assert selected.slug == "multideal"
        assert selected.proxy_config_dir == tray / "accounts" / "multideal" / "CLAUDEX_HOME"


def write_claude_account(tray: Path, slug: str) -> None:
    (tray / "claude-accounts" / slug / "CLAUDE_HOME").mkdir(parents=True, exist_ok=True)


def write_claude_routing_rules(tray: Path, account_caps: dict[str, object] | None) -> None:
    rules: dict[str, object] = {
        "version": "routing/v2",
        "projects": {},
        "default": "multideal",
        "fallback_chain": [],
        "fallback_trigger": "broken_or_quota_exhausted",
        "quota_exhausted_threshold_pct": 100,
    }
    if account_caps is not None:
        rules["account_caps"] = account_caps
    write_json(tray / "claude_routing_rules.json", rules)


def test_resolve_claude_account_refuses_a_capped_claude_account(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    tray = tmp_path / ".local/state/overdeck/systray/runtime"
    for slug in ("multideal", "zync"):
        write_claude_account(tray, slug)
    (tray / "claude_default_slug").write_text("multideal", encoding="utf-8")
    write_claude_routing_rules(tray, {"zync": {"7d": 10}})
    write_json(
        tray / "claude_health_cache.json",
        {
            "multideal": {"status": "ok", "primary_used_pct": 10, "secondary_used_pct": 10},
            "zync": {
                "status": "ok",
                "primary_used_pct": 10,
                "secondary_used_pct": 92,
                "secondary_reset_at": CAP_RESUME_AT_EPOCH,
            },
        },
    )

    with pytest.raises(claudex.ClaudexCappedError) as raised:
        claudex.resolve_claude_account("zync")

    assert raised.value.exit_code == 75
    assert raised.value.resume_at == CAP_RESUME_AT
    assert raised.value.refusal_json() == (
        '{"ok":false,"detail":"rate-limited","reason":"all-accounts-capped",'
        '"resume_at":"2026-08-10T12:40:53Z"}'
    )


def write_transcript(projects_dir: Path, project: str, session_id: str, cwd: Path) -> Path:
    transcript = projects_dir / project / f"{session_id}.jsonl"
    transcript.parent.mkdir(parents=True, exist_ok=True)
    transcript.write_text(
        json.dumps({"type": "summary"}) + "\n" + json.dumps({"cwd": str(cwd)}) + "\n",
        encoding="utf-8",
    )
    return transcript


def test_resumed_session_id_reads_separate_and_inline_forms() -> None:
    session_id = "41481840-0bdc-4377-9cec-d0a8f6778f85"
    assert claudex.resumed_session_id(["--resume", session_id]) == session_id
    assert claudex.resumed_session_id([f"--resume={session_id}"]) == session_id
    assert claudex.resumed_session_id(["-r", session_id]) == session_id
    assert claudex.resumed_session_id(["--resume"]) is None
    assert claudex.resumed_session_id(["--print", "hello"]) is None


def test_relocate_to_resumed_session_moves_to_recorded_directory(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    session_id = "41481840-0bdc-4377-9cec-d0a8f6778f85"
    projects_dir = tmp_path / ".claude" / "projects"
    owner = tmp_path / "workspace" / "plugin"
    elsewhere = tmp_path / "workspace" / "backend"
    owner.mkdir(parents=True)
    elsewhere.mkdir(parents=True)
    write_transcript(projects_dir, "-workspace-plugin", session_id, owner)
    monkeypatch.setattr(claudex, "session_projects_dirs", lambda: [projects_dir])
    monkeypatch.chdir(elsewhere)

    claudex.relocate_to_resumed_session(["--resume", session_id])

    assert Path.cwd() == owner.resolve()


def test_relocate_to_resumed_session_keeps_cwd_when_session_is_unknown(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    projects_dir = tmp_path / ".claude" / "projects"
    projects_dir.mkdir(parents=True)
    elsewhere = tmp_path / "workspace" / "backend"
    elsewhere.mkdir(parents=True)
    monkeypatch.setattr(claudex, "session_projects_dirs", lambda: [projects_dir])
    monkeypatch.chdir(elsewhere)

    claudex.relocate_to_resumed_session(["--resume", "41481840-0bdc-4377-9cec-d0a8f6778f85"])

    assert Path.cwd() == elsewhere.resolve()


def test_relocate_to_resumed_session_keeps_cwd_when_recorded_directory_is_gone(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    session_id = "41481840-0bdc-4377-9cec-d0a8f6778f85"
    projects_dir = tmp_path / ".claude" / "projects"
    elsewhere = tmp_path / "workspace" / "backend"
    elsewhere.mkdir(parents=True)
    write_transcript(projects_dir, "-gone", session_id, tmp_path / "deleted")
    monkeypatch.setattr(claudex, "session_projects_dirs", lambda: [projects_dir])
    monkeypatch.chdir(elsewhere)

    claudex.relocate_to_resumed_session(["--resume", session_id])

    assert Path.cwd() == elsewhere.resolve()


def _runtime_tray(tmp_path: Path) -> Path:
    return tmp_path / ".local/state/overdeck/systray/runtime"


def _seat_binding(
    *,
    account: str = "roy",
    seat_id: str = "seat-1",
    host: str = "debian1",
    model: str = "gpt-5.6-terra",
    fingerprint: str = "abc",
) -> dict[str, str]:
    return {
        "account": account,
        "seatId": seat_id,
        "host": host,
        "model": model,
        "credentialFingerprint": fingerprint,
        "boundAt": "2026-08-06T00:00:00Z",
    }


def _write_binding(path: Path, payload: Mapping[str, object]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload), encoding="utf-8")


def _ensure_claude_account(tmp_path: Path, slug: str = "roy") -> Path:
    account_home = _runtime_tray(tmp_path) / "claude-accounts" / slug / "CLAUDE_HOME"
    account_home.mkdir(parents=True)
    return account_home


def _ensure_claude_default(tmp_path: Path, slug: str = "roy") -> Path:
    account_home = _ensure_claude_account(tmp_path, slug)
    (_runtime_tray(tmp_path) / "claude_default_slug").write_text(slug, encoding="utf-8")
    return account_home


def _fake_claude_account(slug: str = "roy", home: Path | None = None) -> claudex.ClaudeAccount:
    account_home = home or Path(f"/tmp/{slug}/CLAUDE_HOME")
    return claudex.ClaudeAccount(
        slug,
        account_home,
        {"CLAUDE_CONFIG_DIR": str(account_home)},
    )


def test_parse_seat_account_overrides_forwards_after_separator() -> None:
    accounts, remaining = claudex.parse_seat_account_overrides(
        ["--claude-account", "roy", "--model", "terra", "--", "--account", "forwarded", "chat"]
    )

    assert accounts == claudex.AccountOverrides(None, "roy")
    assert remaining == ["--model", "terra", "--", "--account", "forwarded", "chat"]


def test_run_seat_delegates_exact_argv(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[list[str]] = []

    def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append(command)
        return subprocess.CompletedProcess(command, 11)

    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(claudex.Path, "cwd", classmethod(lambda cls: Path("/repo")))
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)

    result = claudex.run(["--seat", "--model", "terra", "--", "chat", "-p", "hi"])

    assert result == 11
    assert calls == [[
        "/bin/seat-remote",
        "launch",
        "--host",
        "auto",
        "--account",
        "roy",
        "--project",
        "/repo",
        "--model",
        "gpt-5.6-terra",
        "--",
        "chat",
        "-p",
        "hi",
    ]]


def test_run_seat_forwards_account_flag_after_separator(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[list[str]] = []

    def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append(command)
        return subprocess.CompletedProcess(command, 0)

    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(claudex.Path, "cwd", classmethod(lambda cls: Path("/repo")))
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)

    claudex.run(
        ["--seat", "--model", "terra", "--", "--account", "forwarded", "chat", "-p", "hi"]
    )

    separator = calls[0].index("--")
    assert calls[0][separator + 1 :] == ["--account", "forwarded", "chat", "-p", "hi"]


@pytest.mark.parametrize(
    ("token", "expected"),
    [
        ("sol", "gpt-5.6-sol"),
        ("terra", "gpt-5.6-terra"),
        ("luna", "gpt-5.6-luna"),
        ("gpt-5.6-sol", "gpt-5.6-sol"),
        ("gpt-custom.model_1", "gpt-custom.model_1"),
    ],
)
def test_resolve_seat_model_token_accepts_aliases_and_exact_ids(
    token: str, expected: str
) -> None:
    assert claudex.resolve_seat_model_token(token) == expected


@pytest.mark.parametrize(
    ("argv", "expected_model"),
    [
        (["-m", "sol", "chat"], "gpt-5.6-sol"),
        (["-m=terra", "chat"], "gpt-5.6-terra"),
        (["--model", "luna", "chat"], "gpt-5.6-luna"),
        (["--model=sol", "chat"], "gpt-5.6-sol"),
    ],
)
def test_parse_seat_wrapper_model_accepts_short_and_long_model_flags(
    argv: list[str], expected_model: str
) -> None:
    assert claudex.parse_seat_wrapper_model(argv) == claudex.SeatWrapperModel(
        expected_model, ["chat"]
    )


@pytest.mark.parametrize(
    ("token", "message"),
    [
        ("sonnet", claudex.SEAT_UNSUPPORTED_BOUNDARY),
        ("fable", claudex.SEAT_UNSUPPORTED_BOUNDARY),
        ("claude-sonnet-5", claudex.SEAT_UNSUPPORTED_BOUNDARY),
        ("unknown-model", "seat-remote: unknown seat model unknown-model"),
    ],
)
def test_resolve_seat_model_token_rejects_native_and_unknown(token: str, message: str) -> None:
    with pytest.raises(claudex.ClaudexError, match=re.escape(message)):
        claudex.resolve_seat_model_token(token)


@pytest.mark.parametrize(
    ("argv", "pattern"),
    [
        (["--seat"], "seat-remote: --model is required"),
        (["--seat", "--model"], "seat-remote: --model is required"),
        (["--seat", "--model", "terra", "--model", "sol"], "duplicate wrapper --model"),
        (["--seat", "--model", "terra", "--fallback-model", "gpt-5.6-sol"], "fallback-model"),
        (["--seat", "--model", "terra", "--", "--model", "gpt-5.6-sol"], "forwarded security arg denied"),
        (["--seat", "--model", "terra", "--", "-m", "gpt-5.6-sol"], "forwarded security arg denied"),
        (["--seat", "--model", "terra", "--", "-m=gpt-5.6-sol"], "forwarded security arg denied"),
        (["--seat", "--model", "sonnet"], re.escape(claudex.SEAT_UNSUPPORTED_BOUNDARY)),
    ],
)
def test_run_seat_rejects_invalid_model_before_subprocess(
    argv: list[str], pattern: str, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match=pattern):
        claudex.run(argv)


def test_run_seat_honors_claude_account_precedence(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[list[str]] = []

    def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append(command)
        return subprocess.CompletedProcess(command, 0)

    monkeypatch.setattr(
        claudex,
        "resolve_claude_account",
        lambda slug=None: _fake_claude_account("multideal"),
    )
    monkeypatch.setattr(claudex.Path, "cwd", classmethod(lambda cls: Path("/repo")))
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)

    claudex.run(["--seat", "--claude-account", "multideal", "--model", "sol"])

    assert calls[0][calls[0].index("--account") + 1] == "multideal"


def test_run_seat_uses_resume_relocated_cwd(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    work = tmp_path / "cwd"
    work.mkdir()
    monkeypatch.chdir(work)
    calls: list[list[str]] = []

    def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
        calls.append(command)
        return subprocess.CompletedProcess(command, 0)

    monkeypatch.setattr(
        claudex,
        "resolve_claude_account",
        lambda slug=None: _fake_claude_account("roy"),
    )
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(claudex.subprocess, "run", fake_run)

    claudex.run(["--seat", "--model", "luna", "resume", "session-abc"])

    assert calls[0][calls[0].index("--project") + 1] == str(work)


def test_run_seat_missing_binary_exits_127(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(
        claudex,
        "resolve_seat_remote_binary",
        lambda: (_ for _ in ()).throw(
            claudex.MissingBinaryError("claudex: required binary not found: seat-remote")
        ),
    )

    exit_code = claudex.main(["claudex", "--seat", "--model", "sol"])

    assert exit_code == 127
    assert "seat-remote" in capsys.readouterr().err


def test_run_seat_propagates_child_exit_status(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(claudex.Path, "cwd", classmethod(lambda cls: Path("/repo")))
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(
        claudex.subprocess,
        "run",
        lambda command, **kwargs: subprocess.CompletedProcess(command, 42),
    )

    assert claudex.run(["--seat", "--model", "sol"]) == 42


def test_bound_account_blocks_non_seat_local_execution(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _ensure_claude_default(tmp_path, "roy")
    binding_path = claudex.seat_binding_path("roy", tmp_path)
    _write_binding(binding_path, _seat_binding())
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match="use claudex --seat"):
        claudex.run(["--model", "sol"])


def test_unbound_account_allows_non_seat_local_execution(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_dir = tmp_path / "CLAUDEX_HOME"
    write_json(native_path, native_auth(200))
    selected = claudex.SelectedAccount("roy", native_path, proxy_dir)
    monkeypatch.setattr(claudex, "resolve_selected_account", lambda slug=None: selected)
    monkeypatch.setattr(claudex.shutil, "which", lambda name: f"/bin/{name}")
    monkeypatch.setattr(claudex, "find_available_port", lambda: 12345)
    monkeypatch.setattr(claudex, "wait_for_proxy", lambda process, port: None)
    monkeypatch.setattr(
        claudex.subprocess,
        "run",
        lambda command, **kwargs: subprocess.CompletedProcess(command, 0),
    )
    monkeypatch.setattr(
        claudex.subprocess,
        "Popen",
        lambda *args, **kwargs: type(
            "Proxy",
            (),
            {
                "poll": lambda self: None,
                "terminate": lambda self: None,
                "wait": lambda self, timeout=None: 0,
            },
        )(),
    )

    assert claudex.run(["--model", "sol"]) == 0


def test_bound_account_allows_seat_execution(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _write_binding(claudex.seat_binding_path("roy", tmp_path), _seat_binding())
    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(claudex.Path, "cwd", classmethod(lambda cls: Path("/repo")))
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(
        claudex.subprocess,
        "run",
        lambda command, **kwargs: subprocess.CompletedProcess(command, 0),
    )

    assert claudex.run(["--seat", "--model", "sol"]) == 0


def test_resolve_seat_remote_binary_prefers_path_then_canonical(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    path_binary = tmp_path / "path-seat-remote"
    path_binary.write_text("#!/bin/sh\n", encoding="utf-8")
    path_binary.chmod(0o755)
    canonical = tmp_path / ".claude" / "bin" / "seat-remote"
    canonical.parent.mkdir(parents=True)
    canonical.write_text("#!/bin/sh\n", encoding="utf-8")
    canonical.chmod(0o755)

    monkeypatch.setattr(claudex.shutil, "which", lambda name: str(path_binary))
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    assert claudex.resolve_seat_remote_binary() == str(path_binary)

    monkeypatch.setattr(claudex.shutil, "which", lambda name: None)
    assert claudex.resolve_seat_remote_binary() == str(canonical)


def test_malformed_seat_binding_fails_closed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _ensure_claude_default(tmp_path, "roy")
    binding_path = claudex.seat_binding_path("roy", tmp_path)
    _write_binding(binding_path, {"account": "roy"})
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match="invalid seat binding record"):
        claudex.run(["--model", "sol"])


def test_symlink_seat_binding_fails_closed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _ensure_claude_default(tmp_path, "roy")
    target = tmp_path / "target.json"
    _write_binding(target, _seat_binding())
    binding_path = claudex.seat_binding_path("roy", tmp_path)
    binding_path.parent.mkdir(parents=True, exist_ok=True)
    binding_path.symlink_to(target)
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match="invalid seat binding record"):
        claudex.run(["--model", "sol"])


def test_oversized_seat_binding_fails_closed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _ensure_claude_default(tmp_path, "roy")
    binding_path = claudex.seat_binding_path("roy", tmp_path)
    binding_path.parent.mkdir(parents=True, exist_ok=True)
    binding_path.write_text("x" * (claudex.SEAT_BINDING_MAX_BYTES + 1), encoding="utf-8")
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match="invalid seat binding record"):
        claudex.run(["--model", "sol"])


def test_run_seat_does_not_invoke_native_or_hybrid_paths(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(claudex, "resolve_claude_account", lambda slug=None: _fake_claude_account())
    monkeypatch.setattr(claudex.Path, "cwd", classmethod(lambda cls: Path("/repo")))
    monkeypatch.setattr(claudex, "resolve_seat_remote_binary", lambda: "/bin/seat-remote")
    monkeypatch.setattr(claudex, "run_native", lambda *args, **kwargs: pytest.fail("native"))
    monkeypatch.setattr(claudex, "run_hybrid", lambda *args, **kwargs: pytest.fail("hybrid"))
    monkeypatch.setattr(claudex, "run_proxy", lambda *args, **kwargs: pytest.fail("proxy"))
    monkeypatch.setattr(
        claudex.subprocess,
        "run",
        lambda command, **kwargs: subprocess.CompletedProcess(command, 0),
    )

    claudex.run(["--seat", "--model", "sol"])


def test_load_seat_binding_unreadable_file_fails_closed(tmp_path: Path) -> None:
    binding_path = claudex.seat_binding_path("roy", tmp_path)
    _write_binding(binding_path, _seat_binding())
    binding_path.chmod(0o000)

    try:
        assert claudex.load_seat_binding("roy", tmp_path) == "invalid"
    finally:
        binding_path.chmod(0o600)


def test_load_seat_binding_directory_fails_closed(tmp_path: Path) -> None:
    binding_path = claudex.seat_binding_path("roy", tmp_path)
    binding_path.mkdir(parents=True)

    assert claudex.load_seat_binding("roy", tmp_path) == "invalid"


def test_load_seat_binding_missing_o_nofollow_fails_closed(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.delattr(os, "O_NOFOLLOW", raising=False)

    assert claudex.load_seat_binding("roy", Path("/tmp")) == "invalid"


def test_non_seat_gate_skips_without_claude_account(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    native_path = tmp_path / "CODEX_HOME" / "auth.json"
    proxy_dir = tmp_path / "CLAUDEX_HOME"
    write_json(native_path, native_auth(200))
    selected = claudex.SelectedAccount("zync2", native_path, proxy_dir)
    gate_calls: list[str | None] = []

    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    monkeypatch.setattr(claudex, "resolve_selected_account", lambda slug=None: selected)
    monkeypatch.setattr(claudex, "claude_account_exists", lambda slug: False)
    monkeypatch.setattr(claudex.shutil, "which", lambda name: f"/bin/{name}")
    monkeypatch.setattr(claudex, "find_available_port", lambda: 23456)
    monkeypatch.setattr(claudex, "wait_for_proxy", lambda process, port: None)
    monkeypatch.setattr(
        claudex.subprocess,
        "Popen",
        lambda *args, **kwargs: type(
            "Proxy",
            (),
            {
                "poll": lambda self: None,
                "terminate": lambda self: None,
                "wait": lambda self, timeout=None: 0,
            },
        )(),
    )
    monkeypatch.setattr(
        claudex.subprocess,
        "run",
        lambda command, **kwargs: subprocess.CompletedProcess(command, 0),
    )
    monkeypatch.setattr(
        claudex,
        "enforce_local_seat_binding_gate",
        lambda slug, home=None: gate_calls.append(slug),
    )

    assert claudex.run(["--account=zync2"]) == 0
    assert gate_calls == [None]


def test_native_bound_account_blocks_local_execution(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _ensure_claude_default(tmp_path, "roy")
    _write_binding(claudex.seat_binding_path("roy", tmp_path), _seat_binding())
    monkeypatch.setattr(
        claudex,
        "resolve_claude_account",
        lambda slug=None: _fake_claude_account("roy", tmp_path / ".systray-ai" / "claude-accounts" / "roy" / "CLAUDE_HOME"),
    )
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match="use claudex --seat"):
        claudex.run(["--model", "opus", "--claude-account", "roy"])


def test_hybrid_bound_account_blocks_local_execution(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)
    _ensure_claude_default(tmp_path, "roy")
    _write_binding(claudex.seat_binding_path("roy", tmp_path), _seat_binding())
    monkeypatch.setattr(claudex.subprocess, "run", lambda *args, **kwargs: pytest.fail("no subprocess"))

    with pytest.raises(claudex.ClaudexError, match="use claudex --seat"):
        claudex.run(["--claude-account", "roy"])


def test_migrated_selected_account_resolves_without_native_auth_file(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    base = tmp_path / ".local/state/overdeck/systray/runtime"
    registry = claudex.AccountRegistry(base_dir=base, legacy_codex_home=tmp_path / ".codex")
    account_home = registry.add_dir("migrated", "Migrated")
    native_path = account_home / "auth.json"
    assert not native_path.exists()
    grant = tmp_path / "migrated.grant"
    registry.set_authority_binding(
        "migrated",
        claudex.AuthorityBinding(
            mode=claudex.AuthorityMode.SUBROUTER,
            authority_name="workstation",
            route_id="routefixture1234567890",
            provider="codex",
            proxy_grant_ref=grant.resolve(),
        ),
    )
    registry.default_path.write_text("migrated\n", encoding="utf-8")
    monkeypatch.setattr(claudex.Path, "home", lambda: tmp_path)

    selected = claudex.resolve_selected_account()

    assert selected.slug == "migrated"
    assert selected.native_auth_path == native_path
    assert not selected.native_auth_path.exists()
    assert selected.authority_binding is not None
    assert selected.authority_binding.mode == claudex.AuthorityMode.SUBROUTER


def test_authority_codex_proxy_uses_ephemeral_gateway_profile_without_reconciliation(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    native_path = tmp_path / "accounts/migrated/CODEX_HOME/auth.json"
    assert not native_path.exists()
    binding = claudex.AuthorityBinding(
        mode=claudex.AuthorityMode.SUBROUTER,
        authority_name="workstation",
        route_id="routefixture1234567890",
        provider="codex",
        proxy_grant_ref=(tmp_path / "migrated.grant").resolve(),
    )
    selected = claudex.SelectedAccount(
        "migrated",
        native_path,
        tmp_path / "accounts/migrated/CLAUDEX_HOME",
        tmp_path / "runtime",
        None,
        binding,
    )
    calls: list[tuple[list[str], dict[str, str]]] = []

    class FakeProxy:
        returncode = None

        def poll(self) -> None:
            return None

        def terminate(self) -> None:
            calls.append((["terminate"], {}))

        def wait(self, timeout: float | None = None) -> int:
            return 0

        def kill(self) -> None:
            raise AssertionError("authority proxy should terminate cleanly")

    def fake_popen(args: list[str], **kwargs: object) -> FakeProxy:
        calls.append((args, cast(dict[str, str], kwargs["env"])))
        return FakeProxy()

    monkeypatch.setattr(
        claudex,
        "resolve_authority_data_plane",
        lambda *_args, **_kwargs: AuthorityDataPlane(
            responses_url="http://100.126.128.50:31416/r/routefixture1234567890/v1/responses",
            proxy_key="fixture-subrouter-grant",
            grant_expires_at_ms=1_787_370_000_000,
            sanitized_status=GatewayStatus(
                "ready", "Gateway: ready", 1.0, 1_787_370_000_000
            ),
        ),
    )
    monkeypatch.setattr(
        claudex,
        "reconcile_credentials",
        lambda *_args, **_kwargs: pytest.fail("migrated Claudex reconciled native credentials"),
    )
    monkeypatch.setattr(claudex, "_require_binary", lambda _name: "/bin/claude-code-proxy")
    monkeypatch.setattr(claudex, "find_available_port", lambda: 24567)
    monkeypatch.setattr(claudex, "wait_for_proxy", lambda _process, _port: None)
    monkeypatch.setattr(claudex.subprocess, "Popen", fake_popen)

    with claudex.codex_proxy(selected) as port:
        assert port == 24567
        proxy_args, proxy_env = calls[0]
        assert proxy_args == ["/bin/claude-code-proxy", "serve", "--no-monitor"]
        assert proxy_env["CCP_CODEX_BASE_URL"] == (
            "http://100.126.128.50:31416/r/routefixture1234567890/v1/responses"
        )
        assert proxy_env["CCP_BIND_ADDRESS"] == "127.0.0.1"
        assert proxy_env["CCP_CODEX_SERVICE_TIER"] == "fast"
        config_dir = Path(proxy_env["CCP_CONFIG_DIR"])
        assert config_dir != selected.proxy_config_dir
        assert config_dir.stat().st_mode & 0o777 == 0o700
        proxy_auth_path = config_dir / "codex/auth.json"
        payload = json.loads(proxy_auth_path.read_text(encoding="utf-8"))
        assert payload == {
            "access": "fixture-subrouter-grant",
            "refresh": "",
            "expires": 1_787_370_000_000,
        }
        assert proxy_auth_path.stat().st_mode & 0o777 == 0o600
        assert "accountId" not in payload
        assert "native-refresh" not in json.dumps(payload)
        assert str(native_path) not in json.dumps(proxy_env)
    assert calls[-1][0] == ["terminate"]
    assert not config_dir.exists()
    assert not native_path.exists()


def test_authority_codex_proxy_requires_ready_gateway_and_never_falls_back_native(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    selected = claudex.SelectedAccount(
        "migrated",
        tmp_path / "missing/CODEX_HOME/auth.json",
        tmp_path / "missing/CLAUDEX_HOME",
        tmp_path / "runtime",
        None,
        claudex.AuthorityBinding(
            mode=claudex.AuthorityMode.SUBROUTER,
            authority_name="workstation",
            route_id="routefixture1234567890",
            provider="codex",
            proxy_grant_ref=(tmp_path / "grant").resolve(),
        ),
    )
    monkeypatch.setattr(
        claudex,
        "resolve_authority_data_plane",
        lambda *_args, **_kwargs: AuthorityDataPlane(
            responses_url="http://100.126.128.50:31416/r/routefixture1234567890/v1/responses",
            proxy_key="fixture-subrouter-grant",
            grant_expires_at_ms=1_787_370_000_000,
            sanitized_status=GatewayStatus(
                "migration-required", "Gateway: migration required", 1.0, 1_787_370_000_000
            ),
        ),
    )
    monkeypatch.setattr(
        claudex,
        "reconcile_credentials",
        lambda *_args, **_kwargs: pytest.fail("non-ready gateway fell back to native credentials"),
    )
    monkeypatch.setattr(claudex, "_require_binary", lambda _name: "/bin/claude-code-proxy")
    monkeypatch.setattr(
        claudex.subprocess,
        "Popen",
        lambda *_args, **_kwargs: pytest.fail("non-ready gateway started proxy"),
    )

    with pytest.raises(claudex.ClaudexError, match="Gateway: migration required"):
        with claudex.codex_proxy(selected):
            pytest.fail("non-ready gateway yielded a proxy")


def test_migrated_shared_environment_omits_native_codex_home(tmp_path: Path) -> None:
    selected = claudex.SelectedAccount(
        "migrated",
        tmp_path / "missing/CODEX_HOME/auth.json",
        tmp_path / "missing/CLAUDEX_HOME",
        tmp_path / "runtime",
        None,
        claudex.AuthorityBinding(
            mode=claudex.AuthorityMode.SUBROUTER,
            authority_name="workstation",
            route_id="routefixture1234567890",
            provider="codex",
            proxy_grant_ref=(tmp_path / "grant").resolve(),
        ),
    )

    environment = claudex._shared_claude_env(selected)

    assert environment["SYSTRAY_CODEX_ACCOUNT_SLUG"] == "migrated"
    assert "SYSTRAY_CODEX_ACCOUNT_HOME" not in environment


def test_resolve_claude_account_uses_systray_gateway_binding_without_native_credentials(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    from types import SimpleNamespace

    native_home = tmp_path / "runtime/claude-accounts/migrated/CLAUDE_HOME"
    native_home.mkdir(parents=True)
    gateway_home = tmp_path / "runtime/gateway-homes/claude/fixture"
    gateway_home.mkdir(parents=True)
    binding = claudex.AuthorityBinding(
        mode=claudex.AuthorityMode.SUBROUTER,
        authority_name="subrouter-primary",
        route_id="routefixture1234567890",
        provider="claude",
        proxy_grant_ref=(tmp_path / "grant.key").resolve(),
    )
    account_record = SimpleNamespace(authority_binding=binding)

    class FakeAdapter:
        def account(self, _base: Path, slug: str):
            assert slug == "migrated"
            return account_record

        def lock_policy_dir(self):
            return tmp_path / "runtime"

        def sync_before_exec(self, _base: Path):
            pytest.fail("migrated Claude synchronized native account state")

        def build_env(self, _home: Path):
            pytest.fail("migrated Claude built native environment")

    class FakeRouter:
        def __init__(self, _adapter: object) -> None:
            self.adapter = FakeAdapter()

        def _resolve_account(self, *_args: object, **_kwargs: object):
            return tmp_path / "runtime", "migrated", native_home

    monkeypatch.setattr(claudex, "CommandRouter", FakeRouter)
    monkeypatch.setattr(claudex, "authorize_account_use", lambda *_args, **_kwargs: object())
    monkeypatch.setattr(claudex, "revalidate_account_use", lambda *_args, **_kwargs: None)
    monkeypatch.setattr(
        claudex,
        "resolve_credentials",
        lambda *_args, **_kwargs: pytest.fail("migrated Claude opened native credentials"),
    )
    monkeypatch.setattr(
        claudex,
        "build_authority_launch",
        lambda *_args, **_kwargs: SimpleNamespace(
            environment={
                "HOME": str(gateway_home),
                "CLAUDE_CONFIG_DIR": str(gateway_home),
                "ANTHROPIC_BASE_URL": "http://127.0.0.1:31415/r/routefixture1234567890",
                "ANTHROPIC_AUTH_TOKEN": "fixture-gateway-grant",
            },
            gateway_home=gateway_home,
            sanitized_status=GatewayStatus("ready", "Gateway: ready", 1.0),
        ),
    )

    account = claudex.resolve_claude_account("migrated")

    assert account.slug == "migrated"
    assert account.authority_binding == binding
    assert account.home == gateway_home
    assert account.settings_home == native_home.parent / "CLAUDEX_HOME"
    assert account.env["ANTHROPIC_BASE_URL"].endswith("/r/routefixture1234567890")
    assert account.env["ANTHROPIC_AUTH_TOKEN"] == "fixture-gateway-grant"
    assert account.env["CLAUDE_CONFIG_DIR"] == str(native_home.parent / "CLAUDEX_HOME")
    assert account.env["SYSTRAY_CLAUDE_ACCOUNT_SLUG"] == "migrated"
    assert "SYSTRAY_CLAUDE_ACCOUNT_HOME" not in account.env
    assert "CLAUDE_SECURESTORAGE_CONFIG_DIR" not in account.env
