"""Tests for the local-release runner."""

from __future__ import annotations

import json
import zipfile
from pathlib import Path

import pytest

from mgf.fed import _release
from mgf.fed._release import (
    ReleaseResult,
    _validate_wheel,
    _version,
    release_local,
    release_remote,
)


def test_version_reads_pyproject(tmp_path: Path) -> None:
    (tmp_path / "pyproject.toml").write_text('[project]\nname = "x"\nversion = "1.2.3"\n')
    assert _version(tmp_path) == "1.2.3"


def test_validate_wheel(tmp_path: Path) -> None:
    good = tmp_path / "mgf_x-1.2.3-py3-none-any.whl"
    with zipfile.ZipFile(good, "w") as z:
        z.writestr("mgf/x/py.typed", "")
    assert _validate_wheel(good, "1.2.3") == (True, "")
    ok, detail = _validate_wheel(good, "9.9.9")
    assert not ok and "version" in detail

    no_typed = tmp_path / "mgf_y-1.0.0-py3-none-any.whl"
    with zipfile.ZipFile(no_typed, "w") as z:
        z.writestr("mgf/y/__init__.py", "")
    ok, detail = _validate_wheel(no_typed, "1.0.0")
    assert not ok and "py.typed" in detail


def _root(tmp_path: Path) -> Path:
    reg = {
        "release_waves": [["mgf-a"], ["mgf-b"]],
        "libraries": [
            {"repo": "mgf-a", "lang": "python", "version": "1.0.0"},
            {"repo": "mgf-b", "lang": "python", "version": "2.0.0"},
        ],
    }
    (tmp_path / "mgf-standard").mkdir()
    (tmp_path / "mgf-standard" / "MGF_LIBRARIES.json").write_text(json.dumps(reg), encoding="utf-8")
    for r in ("mgf-a", "mgf-b"):
        (tmp_path / r).mkdir()
    return tmp_path


def test_release_local_runs_in_wave_order(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(_release.shutil, "which", lambda _n: "/usr/bin/uv")
    monkeypatch.setattr(_release, "_find_ruff", lambda: None)
    seen: list[str] = []

    def _fake_one(m: object, uv: str, ruff: str | None) -> ReleaseResult:
        repo = m.repo  # type: ignore[attr-defined]
        seen.append(repo)
        return ReleaseResult(repo, "1.0.0", "publish", True, "ok")

    monkeypatch.setattr(_release, "_release_one", _fake_one)
    rc = release_local(["mgf-b", "mgf-a"], root=_root(tmp_path))  # arg order reversed
    assert rc == 0
    assert seen == ["mgf-a", "mgf-b"]  # processed in WAVE order, not arg order
    assert "2/2 published" in capsys.readouterr().out


def test_release_local_no_uv(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(_release.shutil, "which", lambda _n: None)
    assert release_local(["mgf-a"]) == 2


def test_release_remote_dry_run(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(_release.shutil, "which", lambda _n: "/usr/bin/uv")
    monkeypatch.setattr(_release, "_local_wheel", lambda repo, ver: tmp_path / f"{repo}.whl")
    monkeypatch.setattr(_release, "_git", lambda args, cwd: (True, ""))
    monkeypatch.setattr(_release, "_devpi_has", lambda pkg, ver: False)
    rc = release_remote(["mgf-a"], dry_run=True, root=_root(tmp_path))
    out = capsys.readouterr().out
    assert rc == 0 and "would tag" in out and "on devpi" in out


def test_release_remote_already_published_is_noop(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(_release.shutil, "which", lambda _n: "/usr/bin/uv")
    monkeypatch.setattr(_release, "_local_wheel", lambda repo, ver: tmp_path / f"{repo}.whl")
    monkeypatch.setattr(_release, "_git", lambda args, cwd: (True, ""))
    monkeypatch.setattr(_release, "_devpi_has", lambda pkg, ver: True)
    monkeypatch.setenv(_release.DEVPI_PASSWORD_ENV, "pw")
    rc = release_remote(["mgf-a"], root=_root(tmp_path))
    assert rc == 0 and "already published" in capsys.readouterr().out


def test_release_remote_requires_local_release_first(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(_release.shutil, "which", lambda _n: "/usr/bin/uv")
    monkeypatch.setattr(_release, "_local_wheel", lambda repo, ver: None)
    monkeypatch.setenv(_release.DEVPI_PASSWORD_ENV, "pw")
    rc = release_remote(["mgf-a"], root=_root(tmp_path))
    assert rc == 1 and "run `release-local" in capsys.readouterr().out


def test_release_remote_no_password_no_dry_run(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    monkeypatch.setattr(_release.shutil, "which", lambda _n: "/usr/bin/uv")
    monkeypatch.delenv(_release.DEVPI_PASSWORD_ENV, raising=False)
    assert release_remote(["mgf-a"], dry_run=False, root=_root(tmp_path)) == 2
    assert "DEVPI_PASSWORD" in capsys.readouterr().out


def _member(tmp_path: Path, repo: str = "mgf-x", version: str = "1.0.0"):
    from mgf.fed._roster import Member

    d = tmp_path / repo
    d.mkdir()
    (d / "pyproject.toml").write_text(f'[project]\nname = "{repo}"\nversion = "{version}"\n')
    return Member(repo=repo, lang="python", registry_version=version, root=tmp_path)


def test_release_one_publishes(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    m = _member(tmp_path)
    monkeypatch.setattr(_release, "_run", lambda *a, **k: (True, ""))
    monkeypatch.setattr(
        _release, "_newest_wheel", lambda d, v: tmp_path / "mgf_x-1.0.0-py3-none-any.whl"
    )
    monkeypatch.setattr(_release, "_validate_wheel", lambda w, v: (True, ""))
    monkeypatch.setattr(_release._index, "publish", lambda w: tmp_path / "idx" / Path(w).name)
    res = _release._release_one(m, "/usr/bin/uv", None)  # ruff=None → gate skipped
    assert res.ok and res.reached == "publish"


def test_release_one_build_fail(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    m = _member(tmp_path)
    monkeypatch.setattr(
        _release, "_run", lambda args, cwd=None: (False, "boom") if "build" in args else (True, "")
    )
    res = _release._release_one(m, "/usr/bin/uv", None)
    assert not res.ok and res.reached == "build"


def test_release_remote_one_publishes_and_verifies(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    m = _member(tmp_path)
    monkeypatch.setattr(
        _release, "_local_wheel", lambda r, v: tmp_path / "mgf_x-1.0.0-py3-none-any.whl"
    )
    monkeypatch.setattr(_release, "_git", lambda a, c: (True, ""))
    seen: list[int] = []

    def _has(_p: str, _v: str) -> bool:
        seen.append(1)
        return len(seen) > 1  # 1st (pre-req) absent → publish; 2nd (verify) present

    monkeypatch.setattr(_release, "_devpi_has", _has)
    monkeypatch.setattr(_release, "_run", lambda *a, **k: (True, ""))  # uv publish
    res = _release._release_remote_one(m, "pw", "/usr/bin/uv", dry_run=False)
    assert res.ok and res.reached == "publish"


# --- §3.5: the publish target + auth shape derive from $DEVPI_URL ---------------


def test_upload_url_and_user_derived_from_devpi_url(monkeypatch: pytest.MonkeyPatch) -> None:
    """URL/auth derivation (re-pinned target contract): the upload endpoint is the
    index root ($DEVPI_URL minus the `+simple` tail — the CI publish step's
    ${DEVPI_URL%%+simple*} derivation) and the upload account is the index's owner
    segment (magogi/prod → magogi)."""
    monkeypatch.setenv("DEVPI_URL", "https://example.test/alice/dev/+simple/")
    assert _release._upload_url() == "https://example.test/alice/dev/"
    assert _release._upload_user() == "alice"
    monkeypatch.delenv("DEVPI_URL", raising=False)  # the documented default
    assert _release._upload_url().endswith("/magogi/prod/")
    assert "+simple" not in _release._upload_url()
    assert _release._upload_user() == "magogi"


def test_release_remote_one_uploads_to_devpi_index_root_with_env_auth(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """The upload is `uv publish --publish-url <index root>` with the account/password
    passed via UV_PUBLISH_* env — the password NEVER appears in argv (SC-11)."""
    monkeypatch.setenv("DEVPI_URL", "https://example.test/magogi/prod/+simple/")
    m = _member(tmp_path)
    monkeypatch.setattr(
        _release, "_local_wheel", lambda r, v: tmp_path / "mgf_x-1.0.0-py3-none-any.whl"
    )
    monkeypatch.setattr(_release, "_git", lambda a, c: (True, ""))
    monkeypatch.setattr(_release, "_devpi_has", lambda p, v: False)
    calls: list[tuple[list[str], dict[str, str] | None]] = []

    def _run(args: list[str], cwd: str | None = None, env: dict[str, str] | None = None):
        calls.append((args, env))
        return True, ""

    monkeypatch.setattr(_release, "_run", _run)
    _release._release_remote_one(m, "s3cret", "/usr/bin/uv", dry_run=False)
    publish = next(c for c in calls if c[0][:2] == ["/usr/bin/uv", "publish"])
    args, env = publish
    assert args[args.index("--publish-url") + 1] == "https://example.test/magogi/prod/"
    assert env is not None
    assert env["UV_PUBLISH_USERNAME"] == "magogi"
    assert env["UV_PUBLISH_PASSWORD"] == "s3cret"  # noqa: S105 — test fixture value
    assert "s3cret" not in " ".join(args)  # never argv (SC-11)


def test_devpi_has_reads_the_simple_listing(monkeypatch: pytest.MonkeyPatch) -> None:
    """PKG-05: presence == the +simple listing serves the version. The match is the
    wheel-name shape `-<version>-`, so 0.1.1 never false-positives on 0.1.10; a 404
    project page is False (absent); a network error is None (unknown)."""
    import io
    import urllib.error

    class _Resp(io.BytesIO):
        def __enter__(self) -> _Resp:
            return self

        def __exit__(self, *a: object) -> None:
            self.close()

    served = {"url": ""}

    def _fake_urlopen(url: str, timeout: int = 0) -> _Resp:
        served["url"] = url
        return _Resp(b'<a href="x/mgf_x-0.1.10-py3-none-any.whl">mgf_x-0.1.10</a>')

    monkeypatch.setenv("DEVPI_URL", "https://example.test/magogi/prod/+simple/")
    monkeypatch.setattr(_release.urllib.request, "urlopen", _fake_urlopen)
    assert _release._devpi_has("mgf-x", "0.1.10") is True
    assert served["url"] == "https://example.test/magogi/prod/+simple/mgf-x/"
    assert _release._devpi_has("mgf-x", "0.1.1") is False  # no substring false-positive

    def _boom_404(url: str, timeout: int = 0) -> _Resp:
        raise urllib.error.HTTPError(url, 404, "not found", None, None)  # type: ignore[arg-type]

    monkeypatch.setattr(_release.urllib.request, "urlopen", _boom_404)
    assert _release._devpi_has("mgf-x", "0.1.10") is False

    def _boom_net(url: str, timeout: int = 0) -> _Resp:
        raise urllib.error.URLError("unreachable")

    monkeypatch.setattr(_release.urllib.request, "urlopen", _boom_net)
    assert _release._devpi_has("mgf-x", "0.1.10") is None


# --- SC-11: the publish token must never reach a log/argv ---------------------
def test_redact_cmd_strips_secret_flag_values() -> None:
    leaked = (
        "Command '['uv', 'publish', '--password', 'SECRET_TOKEN', 'x.whl']' "
        "timed out after 600 seconds"
    )
    out = _release._redact_cmd(leaked)
    assert "SECRET_TOKEN" not in out
    assert "<redacted>" in out


def test_run_does_not_leak_secret_on_subprocess_failure(monkeypatch: pytest.MonkeyPatch) -> None:
    import subprocess

    def _boom(*_a: object, **_k: object) -> None:
        raise subprocess.TimeoutExpired(["uv", "publish", "--password", "SECRET_TOKEN"], 600)

    monkeypatch.setattr(_release.subprocess, "run", _boom)
    ok, detail = _release._run(["uv", "publish", "--password", "SECRET_TOKEN"])
    assert ok is False
    assert "SECRET_TOKEN" not in detail
