from __future__ import annotations

import json
import sys
from pathlib import Path

import pytest

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

import account_lock
import command_router
import remote_dispatch
from account_lock import (
    ACCOUNT_LOCK_EXIT_CODE,
    AccountLockedError,
    AccountLockStore,
    HumanOverrideError,
)


@pytest.fixture
def home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    monkeypatch.setattr(command_router.Path, "home", lambda: tmp_path)
    return tmp_path


def _setup_codex(home: Path) -> Path:
    tray = home / ".local/state/overdeck/systray/runtime"
    for slug in ("night", "agent"):
        (tray / "accounts" / slug / "CODEX_HOME").mkdir(parents=True)
    (tray / "accounts.json").write_text(
        json.dumps(
            {
                "accounts": [
                    {"slug": "night", "alias": "Owner"},
                    {"slug": "agent", "alias": "Agent"},
                ]
            }
        ),
        encoding="utf-8",
    )
    (tray / "default_slug").write_text("night\n", encoding="utf-8")
    (tray / "routing_rules.json").write_text(
        json.dumps(
            {
                "projects": {},
                "default": "night",
                "fallback_chain": ["agent"],
                "fallback_trigger": "broken_or_quota_exhausted",
                "quota_exhausted_threshold_pct": 100,
            }
        ),
        encoding="utf-8",
    )
    (tray / "health_cache.json").write_text(
        json.dumps(
            {
                "night": {
                    "status": "ok",
                    "primary_used_pct": 1,
                    "secondary_used_pct": 2,
                },
                "agent": {
                    "status": "ok",
                    "primary_used_pct": 3,
                    "secondary_used_pct": 4,
                },
            }
        ),
        encoding="utf-8",
    )
    return tray


def _setup_legacy_codex(home: Path, slug: str = "owner") -> tuple[Path, Path]:
    current = home / ".local/state/overdeck/systray/runtime"
    legacy = home / ".codex-tray"
    (legacy / "accounts" / slug / "CODEX_HOME").mkdir(parents=True)
    (legacy / "accounts.json").write_text(
        json.dumps({"accounts": [{"slug": slug, "alias": "Owner"}]}),
        encoding="utf-8",
    )
    return current, legacy


def _router() -> command_router.CommandRouter:
    return command_router.CommandRouter(
        command_router.CodexAdapter(),
        detect_project_name=lambda _cwd=None: "unknown",
    )


def test_canonical_lock_policy_blocks_a_legacy_account_copy(home: Path) -> None:
    current, _legacy = _setup_legacy_codex(home)
    AccountLockStore(current).set_locked("codex", "owner", True)

    with pytest.raises(AccountLockedError):
        _router()._resolve_account("owner")


def test_human_override_for_legacy_account_uses_canonical_lock_policy(
    home: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    current, _legacy = _setup_legacy_codex(home)
    store = AccountLockStore(current)
    store.set_locked("codex", "owner", True)
    confirmations: list[tuple[str, str, str]] = []
    monkeypatch.setattr(
        account_lock,
        "confirm_human_action",
        lambda action, tool, slug: confirmations.append((action, tool, slug)),
    )
    monkeypatch.setattr(command_router.CodexAdapter, "sync_before_exec", lambda *_args: None)
    monkeypatch.setattr(command_router.CodexAdapter, "verify_env_support", lambda *_args: True)
    monkeypatch.setattr(command_router.CodexAdapter, "capture_log_path", lambda *_args: None)
    monkeypatch.setattr(command_router.remote_dispatch, "in_container", lambda: False)

    exit_code = _router().main(
        ["cdx", "--account", "owner", "--human-override-lock", "chat"]
    )

    assert exit_code == remote_dispatch.EXIT_UNCONTAINABLE
    assert confirmations == [("use-once", "codex", "owner")]
    assert store.is_locked("codex", "owner") is True


def test_automatic_routing_skips_locked_default(home: Path) -> None:
    tray = _setup_codex(home)
    AccountLockStore(tray).set_locked("codex", "night", True)
    router = _router()

    resolved = router._resolve_account(None)

    assert resolved == (tray, "agent", tray / "accounts/agent/CODEX_HOME")
    assert router._last_route.fallback_from == "night"


def test_explicit_chain_skips_locked_candidates(home: Path) -> None:
    tray = _setup_codex(home)
    AccountLockStore(tray).set_locked("codex", "night", True)
    router = _router()

    resolved = router._resolve_account("Owner,Agent")

    assert resolved == (tray, "agent", tray / "accounts/agent/CODEX_HOME")
    assert router._last_route.chain == ("night", "agent")


def test_explicit_locked_account_refuses_before_launch(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    tray = _setup_codex(home)
    AccountLockStore(tray).set_locked("codex", "night", True)
    monkeypatch.setattr(
        command_router.os,
        "execvpe",
        lambda *_args, **_kwargs: pytest.fail("locked account must not launch"),
    )

    exit_code = _router().main(["cdx", "--account", "night", "chat"])
    captured = capsys.readouterr()

    assert exit_code == ACCOUNT_LOCK_EXIT_CODE
    assert "account lock blocks codex:night" in captured.err
    assert json.loads(captured.out)["reason"] == "account-locked"


def test_all_locked_automatic_route_refuses_with_machine_readable_reason(
    home: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    tray = _setup_codex(home)
    store = AccountLockStore(tray)
    store.set_locked("codex", "night", True)
    store.set_locked("codex", "agent", True)

    exit_code = _router().main(["cdx", "route"])
    captured = capsys.readouterr()

    assert exit_code == ACCOUNT_LOCK_EXIT_CODE
    payload = json.loads(captured.out)
    assert payload["reason"] == "account-locked"
    assert payload["accounts"] == ["codex:night", "codex:agent"]


def test_human_override_is_one_shot_explicit_and_keeps_persistent_lock(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    tray = _setup_codex(home)
    store = AccountLockStore(tray)
    store.set_locked("codex", "night", True)
    confirmations: list[tuple[str, str, str]] = []
    monkeypatch.setattr(
        account_lock,
        "confirm_human_action",
        lambda action, tool, slug: confirmations.append((action, tool, slug)),
    )
    monkeypatch.setattr(command_router.CodexAdapter, "sync_before_exec", lambda *_args: None)
    monkeypatch.setattr(command_router.CodexAdapter, "verify_env_support", lambda *_args: True)
    monkeypatch.setattr(command_router.CodexAdapter, "capture_log_path", lambda *_args: None)
    monkeypatch.setattr(command_router.remote_dispatch, "in_container", lambda: False)
    monkeypatch.setattr(
        command_router.os,
        "execvpe",
        lambda *_args, **_kwargs: pytest.fail("uncontained launch must abort"),
    )

    exit_code = _router().main(
        ["cdx", "--account", "night", "--human-override-lock", "chat"]
    )

    assert exit_code == remote_dispatch.EXIT_UNCONTAINABLE
    assert confirmations == [("use-once", "codex", "night")]
    assert store.is_locked("codex", "night") is True


def test_human_override_denial_propagates_without_launch(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    tray = _setup_codex(home)
    AccountLockStore(tray).set_locked("codex", "night", True)
    monkeypatch.setattr(
        account_lock,
        "confirm_human_action",
        lambda *_args: (_ for _ in ()).throw(
            HumanOverrideError("account lock action requires a human terminal")
        ),
    )

    exit_code = _router().main(
        ["cdx", "--account", "night", "--human-override-lock", "chat"]
    )

    assert exit_code == ACCOUNT_LOCK_EXIT_CODE
    assert "human terminal" in capsys.readouterr().err


def test_human_override_is_rejected_for_route_queries_and_implicit_accounts(
    home: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    _setup_codex(home)

    assert _router().main(["cdx", "route", "--account", "night", "--human-override-lock"]) == 1
    assert "not valid for route queries" in capsys.readouterr().err

    assert _router().main(["cdx", "--human-override-lock", "chat"]) == ACCOUNT_LOCK_EXIT_CODE
    payload = json.loads(capsys.readouterr().out)
    assert payload["reason"] == "human-override-denied"

    assert (
        _router().main(
            [
                "cdx",
                "--account",
                "night,agent",
                "--human-override-lock",
                "chat",
            ]
        )
        == ACCOUNT_LOCK_EXIT_CODE
    )
    captured = capsys.readouterr()
    assert "exactly one explicit --account" in captured.err
    assert json.loads(captured.out)["reason"] == "human-override-denied"


def test_info_only_invocation_does_not_use_or_override_locked_credentials(
    home: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    tray = _setup_codex(home)
    AccountLockStore(tray).set_locked("codex", "night", True)
    launched: list[list[str]] = []
    monkeypatch.setattr(command_router.CodexAdapter, "sync_before_exec", lambda *_args: None)
    monkeypatch.setattr(command_router.CodexAdapter, "verify_env_support", lambda *_args: True)
    monkeypatch.setattr(
        command_router.os,
        "execvpe",
        lambda _file, argv, _env: launched.append(argv),
    )

    exit_code = _router().main(["cdx", "--account", "night", "--help"])

    assert exit_code == 0
    assert launched == [["codex", "--help"]]


def test_invalid_lock_state_fails_closed(home: Path, capsys: pytest.CaptureFixture[str]) -> None:
    tray = _setup_codex(home)
    (tray / "account_locks.json").write_text("not-json", encoding="utf-8")

    exit_code = _router().main(["cdx", "--account", "night", "chat"])

    assert exit_code == ACCOUNT_LOCK_EXIT_CODE
    assert json.loads(capsys.readouterr().out)["reason"] == "account-lock-state-invalid"
