"""Real client classes, driven through injected callables (no network).

Covers the Woodpecker/forge/host HTTP+SSH shells, token resolution, error
mapping, ALL_MANAGED resolution, and lazy context construction.
"""

from __future__ import annotations

import json
import urllib.error

import pytest

from mgf.secrets.context import build_context
from mgf.secrets.errors import CredentialError
from mgf.secrets.forge import CodebergForgeClient, resolve_forge_token
from mgf.secrets.hosts import SSHHostClient
from mgf.secrets.resolve import resolve_all_managed
from mgf.secrets.woodpecker import WoodpeckerClient, resolve_token

# -- Woodpecker -------------------------------------------------------------- #


def _wp_api(routes):
    def api(url, token, method="GET", payload=None):
        for frag, data in routes.items():
            if frag in url:
                return data
        raise AssertionError(f"unexpected url {url}")

    return api


def test_woodpecker_client_reads_scopes_and_pages() -> None:
    routes = {
        "/api/orgs/lookup/magogi-admin": {"id": 1},
        "/api/orgs/1/secrets": [{"name": "s", "events": ["push", "tag"]}],
        "/api/repos/9/secrets": [{"name": "r", "events": ["push"]}],
    }
    # pagination: page 1 has a repo, page 2 empty
    pages = {1: [{"id": 9, "full_name": "magogi-admin/x", "active": True}], 2: []}

    def api(url, token, method="GET", payload=None):
        if "/api/repos?all=true" in url:
            page = int(url.split("page=")[1].split("&")[0])
            return pages.get(page, [])
        return _wp_api(routes)(url, token)

    c = WoodpeckerClient(token="t", api=api)
    assert c.org_id() == 1
    assert c.org_secrets() == {"s": frozenset({"push", "tag"})}
    assert c.repo_secrets(9) == {"r": frozenset({"push"})}
    repos = c.activated_repos()
    assert [r.name for r in repos] == ["x"]


def test_woodpecker_401_maps_to_credential_error() -> None:
    def api(url, token, method="GET", payload=None):
        raise urllib.error.HTTPError(url, 401, "unauth", {}, None)

    c = WoodpeckerClient(token="t", api=api)
    with pytest.raises(CredentialError, match="401"):
        c.org_secrets()


def test_woodpecker_unreachable_maps_to_credential_error() -> None:
    def api(url, token, method="GET", payload=None):
        raise urllib.error.URLError("down")

    with pytest.raises(CredentialError, match="unreachable"):
        WoodpeckerClient(token="t", api=api).org_id()


def test_resolve_token_env_then_file(monkeypatch, tmp_path) -> None:
    monkeypatch.setenv("WOODPECKER_TOKEN", "env-tok")
    assert resolve_token(fed_root=tmp_path) == "env-tok"
    monkeypatch.delenv("WOODPECKER_TOKEN")
    stored = tmp_path / "mgf-standard" / "my_stuff"
    stored.mkdir(parents=True)
    (stored / "woodpecker_token.txt").write_text("file-tok\n", encoding="utf-8")
    assert resolve_token(fed_root=tmp_path) == "file-tok"


def test_resolve_token_missing_raises(monkeypatch, tmp_path) -> None:
    monkeypatch.delenv("WOODPECKER_TOKEN", raising=False)
    with pytest.raises(CredentialError, match="no Woodpecker token"):
        resolve_token(fed_root=tmp_path)


# -- Forge ------------------------------------------------------------------- #


def test_forge_client_parses_and_caches(tmp_path) -> None:
    calls = []

    def api(url, token):
        calls.append(url)
        return [{"title": "ci-clone", "fingerprint": "SHA256:a", "read_only": True}]

    c = CodebergForgeClient(token="t", min_interval=0, cache_dir=tmp_path, api=api)
    keys = c.repo_deploy_keys("mgf-standard")
    assert keys[0].title == "ci-clone" and keys[0].read_only is True
    # second call hits the in-memory cache (no extra api call)
    c.repo_deploy_keys("mgf-standard")
    assert len(calls) == 1
    # a fresh client reads the on-disk cache
    c2 = CodebergForgeClient(token="t", min_interval=0, cache_dir=tmp_path, api=api)
    assert c2.repo_deploy_keys("mgf-standard")[0].fingerprint == "SHA256:a"
    assert len(calls) == 1


def test_forge_404_is_empty_and_401_raises() -> None:
    def api_404(url, token):
        raise urllib.error.HTTPError(url, 404, "nf", {}, None)

    assert CodebergForgeClient(token="t", min_interval=0, api=api_404).repo_deploy_keys("r") == []

    def api_401(url, token):
        raise urllib.error.HTTPError(url, 401, "unauth", {}, None)

    with pytest.raises(CredentialError, match="401"):
        CodebergForgeClient(token="t", min_interval=0, api=api_401).repo_deploy_keys("r")


def test_resolve_forge_token_env_and_missing(monkeypatch) -> None:
    monkeypatch.setenv("CODEBERG_TOKEN", "ct")
    assert resolve_forge_token() == "ct"
    monkeypatch.delenv("CODEBERG_TOKEN")
    with pytest.raises(CredentialError, match="no Codeberg token"):
        resolve_forge_token()


# -- Hosts ------------------------------------------------------------------- #


def test_ssh_host_client_sends_quoted_format_and_parses_present() -> None:
    """The remote command must be a SINGLE shell-quoted string so '%a %U' is not
    split by SSH's space-join (the F-1 bug that made every file read MISSING)."""
    seen = {}

    def ok_runner(argv):
        assert argv[0] == "ssh"
        assert "BatchMode=yes" in argv and "ConnectTimeout=10" in argv
        seen["cmd"] = argv[-1]
        return 0, "600 root\n", ""

    st = SSHHostClient(key_path="/k", runner=ok_runner).stat("h", "/opt/woodpecker/.env")
    assert st.status == "present" and st.mode == "600" and st.owner == "root"
    # one remote arg, format kept together, path quoted
    assert seen["cmd"] == "stat -c '%a %U' /opt/woodpecker/.env"


def test_ssh_host_client_three_states() -> None:
    def missing_runner(argv):
        return 1, "", "stat: cannot statx '/p': No such file or directory"

    assert SSHHostClient(key_path="/k", runner=missing_runner).stat("h", "/p").status == "missing"

    def down_runner(argv):
        return 255, "", "ssh: connect to host h port 22: Connection timed out"

    assert SSHHostClient(key_path="/k", runner=down_runner).stat("h", "/p").status == "unreachable"

    # permission denied -> sudo fallback succeeds -> present (via sudo)
    calls = []

    def perm_then_sudo(argv):
        calls.append(argv[-1])
        if argv[-1].startswith("sudo -n "):
            return 0, "600 steward\n", ""
        return 1, "", "stat: cannot statx '/opt/steward/x': Permission denied"

    st = SSHHostClient(key_path="/k", runner=perm_then_sudo).stat("h", "/opt/steward/x")
    assert st.status == "present" and st.owner == "steward" and st.detail == "via sudo"
    assert any(c.startswith("sudo -n ") for c in calls)

    # permission denied + sudo also fails -> unreachable (present-but-unverifiable), NOT missing
    def perm_no_sudo(argv):
        if argv[-1].startswith("sudo -n "):
            return 1, "", "sudo: a password is required"
        return 1, "", "stat: cannot statx '/opt/steward/x': Permission denied"

    st = SSHHostClient(key_path="/k", runner=perm_no_sudo).stat("h", "/opt/steward/x")
    assert st.status == "unreachable"


# -- resolve ----------------------------------------------------------------- #


def test_resolve_all_managed_unions_both_registries(tmp_path) -> None:
    (tmp_path / "mgf-standard").mkdir()
    (tmp_path / "sooth-standard").mkdir()
    (tmp_path / "mgf-standard" / "MGF_LIBRARIES.json").write_text(
        json.dumps({"libraries": [{"repo": "mgf-common"}, {"repo": "mgf-http"}]}), encoding="utf-8"
    )
    (tmp_path / "sooth-standard" / "SOOTH_LIBRARIES.json").write_text(
        json.dumps({"repos": [{"name": "soothfast"}, {"slug": "soothrede"}]}), encoding="utf-8"
    )
    managed = resolve_all_managed(fed_root=tmp_path, activated_repos=["new-repo", "mgf-common"])
    assert managed == {"mgf-common", "mgf-http", "soothfast", "soothrede", "new-repo"}


def test_resolve_all_managed_tolerates_missing_files(tmp_path) -> None:
    assert resolve_all_managed(fed_root=tmp_path, activated_repos=["only"]) == {"only"}


# -- context ----------------------------------------------------------------- #


def test_build_context_best_effort_swallows_missing_creds(monkeypatch, tmp_path) -> None:
    monkeypatch.delenv("WOODPECKER_TOKEN", raising=False)
    monkeypatch.delenv("CODEBERG_TOKEN", raising=False)
    monkeypatch.delenv("FORGE_TOKEN", raising=False)
    ctx = build_context({"woodpecker", "forge", "hosts"}, root=tmp_path, best_effort=True)
    assert ctx.woodpecker is None  # no token, tolerated
    assert ctx.forge is None
    assert ctx.hosts is not None  # SSH client needs no upfront credential


def test_build_context_strict_raises_without_token(monkeypatch, tmp_path) -> None:
    monkeypatch.delenv("WOODPECKER_TOKEN", raising=False)
    with pytest.raises(CredentialError):
        build_context({"woodpecker"}, root=tmp_path, best_effort=False)
