"""Tests for external-process provider auth status and Accounts-tab wiring.

Covers the copilot-acp fix class:
  * ``get_auth_status()`` dispatches on ``auth_type == "external_process"``
    (not a hardcoded slug), so future ACP-style providers inherit the
    behaviour automatically.
  * ``auth_verified``/``auth_source`` carry positive credential evidence
    (env token or on-disk GitHub Copilot credential store) while remaining
    honest — no evidence means unknown, never "signed out".
  * The Accounts-tab sign-in ``cli_command`` reflects the executable the
    user actually configured (``HERMES_COPILOT_ACP_COMMAND`` /
    ``COPILOT_CLI_PATH``), and its default is a valid Copilot CLI
    invocation (``copilot login`` — ``copilot /login`` is not a command).
"""

import os

import pytest

from hermes_cli.auth import (
    get_auth_status,
    get_external_process_provider_status,
    resolve_external_process_provider_credentials,
)


@pytest.fixture()
def _clean_copilot_env(tmp_path, monkeypatch):
    """Neutralize host state so tests pin behaviour, not this machine."""
    from pathlib import Path
    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    monkeypatch.setenv("USERPROFILE", str(tmp_path))
    for var in (
        "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN",
        "HERMES_COPILOT_ACP_COMMAND", "COPILOT_CLI_PATH",
        "HERMES_COPILOT_ACP_ARGS", "COPILOT_ACP_BASE_URL",
    ):
        monkeypatch.delenv(var, raising=False)


# --- get_auth_status dispatches on auth_type, not slug ----------------------


def test_get_auth_status_dispatches_external_process_by_auth_type(
    tmp_path, monkeypatch, _clean_copilot_env
):
    fake = tmp_path / ("copilot.exe" if os.name == "nt" else "copilot")
    fake.write_text("", encoding="utf-8")
    fake.chmod(0o755)
    monkeypatch.setenv("HERMES_COPILOT_ACP_COMMAND", str(fake))
    # Point HOME somewhere empty so on-disk credential stores don't leak in.
    monkeypatch.setenv("HOME", str(tmp_path))

    status = get_auth_status("copilot-acp")

    # The external_process status shape, not the {"logged_in": False}
    # fallthrough — proves the dispatcher reached the right branch.
    assert status.get("provider") == "copilot-acp"
    assert status.get("configured") is True
    assert status.get("resolved_command") == str(fake)
    assert "auth_verified" in status


def test_external_process_status_finds_user_local_cli_outside_backend_path(
    tmp_path, monkeypatch, _clean_copilot_env
):
    """Desktop SSH inventory must discover a CLI installed in the user's local bin."""
    local_bin = tmp_path / ".local" / "bin"
    local_bin.mkdir(parents=True)
    cli = local_bin / "copilot"
    cli.write_text("", encoding="utf-8")
    cli.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("PATH", str(tmp_path / "backend-bin"))

    status = get_external_process_provider_status("copilot-acp")

    assert status["configured"] is True
    assert status["resolved_command"] == str(cli)
    assert resolve_external_process_provider_credentials("copilot-acp")["command"] == str(cli)


@pytest.mark.platforms("windows")
def test_external_process_status_finds_user_profile_local_cli_as_native_path(
    tmp_path, monkeypatch, _clean_copilot_env
):
    """The Windows user-local hit is one native path, not a mixed-separator join."""
    local_bin = tmp_path / ".local" / "bin"
    local_bin.mkdir(parents=True)
    cli = local_bin / "copilot.exe"
    cli.write_text("", encoding="utf-8")
    monkeypatch.setenv("PATH", str(tmp_path / "backend-bin"))

    status = get_external_process_provider_status("copilot-acp")
    resolved = status["resolved_command"]

    assert status["configured"] is True
    assert "/" not in resolved and os.path.samefile(resolved, cli)
    assert resolve_external_process_provider_credentials("copilot-acp")["command"] == resolved


def test_external_process_status_resolves_relative_configured_path_from_cwd(
    tmp_path, monkeypatch, _clean_copilot_env
):
    """An operator's relative command path keeps resolving against the launch cwd."""
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    cli = bin_dir / ("copilot.exe" if os.name == "nt" else "copilot")
    cli.write_text("", encoding="utf-8")
    cli.chmod(0o755)
    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("PATH", str(tmp_path / "backend-bin"))
    monkeypatch.setenv("HERMES_COPILOT_ACP_COMMAND", os.path.join(".", "bin", cli.name))

    status = get_external_process_provider_status("copilot-acp")

    assert status["configured"] is True
    assert status["resolved_command"] == str(cli)
    assert resolve_external_process_provider_credentials("copilot-acp")["command"] == str(cli)


@pytest.mark.platforms("posix")
@pytest.mark.parametrize("preferred", ["claude-prefix", "user-local", "path", "explicit"])
def test_external_process_claude_resolution_preserves_precedence(
    tmp_path, monkeypatch, _clean_copilot_env, preferred
):
    """Generic discovery must retain Claude's extra prefixes and command precedence."""
    candidates = {}
    for name, directory in (
        ("claude-prefix", tmp_path / ".claude" / "local"),
        ("user-local", tmp_path / ".local" / "bin"),
        ("path", tmp_path / "backend-bin"),
        ("explicit", tmp_path / "configured-bin"),
    ):
        directory.mkdir(parents=True)
        executable = directory / "claude"
        executable.write_text("", encoding="utf-8")
        executable.chmod(0o755)
        candidates[name] = executable
        if name == preferred:
            break
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("PATH", str(tmp_path / "backend-bin"))
    monkeypatch.setenv(
        "HERMES_COPILOT_ACP_COMMAND",
        str(candidates[preferred]) if preferred == "explicit" else "claude",
    )

    status = get_external_process_provider_status("copilot-acp")

    assert status["configured"] is True
    assert status["resolved_command"] == str(candidates[preferred])
    assert resolve_external_process_provider_credentials("copilot-acp")["command"] == str(candidates[preferred])


def test_external_process_status_rejects_wrong_auth_type():
    # A provider that exists but is not external_process must be refused —
    # the generic dispatcher relies on this guard.
    assert get_external_process_provider_status("openrouter") == {"configured": False}
    assert get_external_process_provider_status("no-such-provider") == {"configured": False}


# --- auth_verified: positive evidence only ----------------------------------


def test_auth_verified_false_without_evidence(tmp_path, monkeypatch, _clean_copilot_env):
    monkeypatch.setenv("HOME", str(tmp_path))  # no ~/.config/github-copilot
    status = get_external_process_provider_status("copilot-acp")
    assert status["auth_verified"] is False
    assert status["auth_source"] is None


def test_auth_verified_from_supported_env_token(tmp_path, monkeypatch, _clean_copilot_env):
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("GH_TOKEN", "gho_" + "x" * 36)  # supported OAuth prefix

    status = get_external_process_provider_status("copilot-acp")

    assert status["auth_verified"] is True
    assert status["auth_source"] == "env: GH_TOKEN"


def test_classic_pat_is_not_login_evidence(tmp_path, monkeypatch, _clean_copilot_env):
    # ghp_* classic PATs are rejected by the Copilot API — presence of one
    # must not be presented as a working login.
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("GH_TOKEN", "ghp_" + "x" * 36)

    status = get_external_process_provider_status("copilot-acp")

    assert status["auth_verified"] is False


def test_auth_verified_from_on_disk_credential_store(tmp_path, monkeypatch, _clean_copilot_env):
    monkeypatch.setenv("HOME", str(tmp_path))
    store = tmp_path / ".config" / "github-copilot"
    store.mkdir(parents=True)
    (store / "hosts.json").write_text(
        '{"github.com": {"oauth_token": "gho_test"}}', encoding="utf-8"
    )

    status = get_external_process_provider_status("copilot-acp")

    assert status["auth_verified"] is True
    assert status["auth_source"] == "~/.config/github-copilot/hosts.json"


def test_empty_credential_store_is_not_evidence(tmp_path, monkeypatch, _clean_copilot_env):
    monkeypatch.setenv("HOME", str(tmp_path))
    store = tmp_path / ".config" / "github-copilot"
    store.mkdir(parents=True)
    (store / "hosts.json").write_text("{}", encoding="utf-8")  # logged out

    status = get_external_process_provider_status("copilot-acp")

    assert status["auth_verified"] is False


def test_auth_verified_from_copilot_cli_plaintext_store(tmp_path, monkeypatch, _clean_copilot_env):
    # `copilot login` without an OS keychain writes the token into
    # ~/.copilot/config.json (JSONC, with //-comment header lines).
    monkeypatch.setenv("HOME", str(tmp_path))
    cfg_dir = tmp_path / ".copilot"
    cfg_dir.mkdir()
    (cfg_dir / "config.json").write_text(
        "// User settings belong in settings.json.\n"
        "// This file is managed automatically.\n"
        "{\n"
        '  "copilotTokens": {"https://github.com:someuser": "gho_test"},\n'
        '  "lastLoggedInUser": {"host": "https://github.com", "login": "someuser"}\n'
        "}\n",
        encoding="utf-8",
    )

    status = get_external_process_provider_status("copilot-acp")

    assert status["auth_verified"] is True
    assert status["auth_source"] == "~/.copilot/config.json"


def test_copilot_cli_store_without_tokens_is_not_evidence(tmp_path, monkeypatch, _clean_copilot_env):
    # A config.json exists after first launch even before any login —
    # its presence alone must not read as signed-in.
    monkeypatch.setenv("HOME", str(tmp_path))
    cfg_dir = tmp_path / ".copilot"
    cfg_dir.mkdir()
    (cfg_dir / "config.json").write_text(
        '// managed\n{"firstLaunchAt": "2026-01-01T00:00:00Z", "copilotTokens": {}}\n',
        encoding="utf-8",
    )

    status = get_external_process_provider_status("copilot-acp")

    assert status["auth_verified"] is False


# --- desktop picker explicit-only filter ------------------------------------


def test_explicit_filter_keeps_signed_in_external_process_row(tmp_path, monkeypatch, _clean_copilot_env):
    # A verified CLI login leaves no trace in active_provider/config/env —
    # the explicit-only desktop filter must treat it like the Anthropic OAuth
    # carve-out and keep the row.
    from hermes_cli.inventory import _filter_explicit_provider_rows

    monkeypatch.setenv("HOME", str(tmp_path))
    cfg_dir = tmp_path / ".copilot"
    cfg_dir.mkdir()
    (cfg_dir / "config.json").write_text(
        '{"copilotTokens": {"https://github.com:u": "gho_test"}}', encoding="utf-8"
    )

    class _Ctx:
        current_provider = "nous"

    rows = [{"slug": "copilot-acp", "models": ["gpt-5.4"]}]
    kept = _filter_explicit_provider_rows(rows, _Ctx())

    assert any(r["slug"] == "copilot-acp" for r in kept), \
        "signed-in copilot-acp must survive the explicit-only picker filter"


def test_explicit_filter_drops_unverified_external_process_row(tmp_path, monkeypatch, _clean_copilot_env):
    # Merely having the executable on PATH is ambient discovery, not an
    # explicit configuration — the desktop filter keeps its narrower contract.
    from hermes_cli.inventory import _filter_explicit_provider_rows

    monkeypatch.setenv("HOME", str(tmp_path))  # no credential stores

    class _Ctx:
        current_provider = "nous"

    rows = [{"slug": "copilot-acp", "models": ["gpt-5.4"]}]
    kept = _filter_explicit_provider_rows(rows, _Ctx())

    assert all(r["slug"] != "copilot-acp" for r in kept)


# --- Accounts-tab cli_command ------------------------------------------------




def test_cli_command_reflects_configured_executable(tmp_path, monkeypatch, _clean_copilot_env):
    from hermes_cli.web_server_oauth import _external_process_cli_command

    fake = tmp_path / ("copilot.exe" if os.name == "nt" else "copilot")
    fake.write_text("", encoding="utf-8")
    fake.chmod(0o755)
    monkeypatch.setenv("HERMES_COPILOT_ACP_COMMAND", str(fake))

    rendered = _external_process_cli_command("copilot-acp", "copilot login")

    assert rendered == f"{fake} login"


def test_cli_command_untouched_for_non_external_providers(_clean_copilot_env):
    from hermes_cli.web_server_oauth import _external_process_cli_command

    assert _external_process_cli_command("nous", "hermes auth add nous") == "hermes auth add nous"


def test_cli_command_default_when_no_override(monkeypatch, _clean_copilot_env):
    from hermes_cli.web_server_oauth import _external_process_cli_command

    assert _external_process_cli_command("copilot-acp", "copilot login") == "copilot login"


# --- live catalog key from the Copilot CLI store -----------------------------


def test_catalog_key_resolves_from_copilot_cli_store(tmp_path, monkeypatch, _clean_copilot_env):
    # A user whose ONLY credential is `copilot login` must still get the live
    # model catalog — otherwise the picker silently falls back to the stale
    # curated list (visibly wrong vs. what their subscription serves).
    from unittest.mock import patch as mock_patch

    from hermes_cli import models as models_mod

    monkeypatch.setenv("HOME", str(tmp_path))
    cfg_dir = tmp_path / ".copilot"
    cfg_dir.mkdir()
    (cfg_dir / "config.json").write_text(
        "// managed\n"
        '{"copilotTokens": {"https://github.com:u": "gho_' + "x" * 36 + '"}}\n',
        encoding="utf-8",
    )

    with mock_patch.object(
        models_mod, "_resolve_copilot_catalog_api_key", wraps=models_mod._resolve_copilot_catalog_api_key
    ), mock_patch(
        "hermes_cli.copilot_auth.exchange_copilot_token",
        return_value=("exchanged-api-token", 0.0, None),
    ), mock_patch(
        "hermes_cli.auth.resolve_api_key_provider_credentials",
        side_effect=Exception("no env creds"),
    ), mock_patch(
        "hermes_cli.auth.read_credential_pool", return_value=[]
    ):
        key = models_mod._resolve_copilot_catalog_api_key()

    assert key == "exchanged-api-token"


def test_catalog_key_empty_when_cli_store_absent(tmp_path, monkeypatch, _clean_copilot_env):
    from unittest.mock import patch as mock_patch

    from hermes_cli import models as models_mod

    monkeypatch.setenv("HOME", str(tmp_path))  # no ~/.copilot at all

    with mock_patch(
        "hermes_cli.auth.resolve_api_key_provider_credentials",
        side_effect=Exception("no env creds"),
    ), mock_patch(
        "hermes_cli.auth.read_credential_pool", return_value=[]
    ):
        key = models_mod._resolve_copilot_catalog_api_key()

    assert key == ""
