"""Pushed-branch tier of the worktree pruners (#A, Aug 2026).

Behavior contract: a clean worktree whose branch head EXACTLY matches what
origin holds (open-PR lane on a single-branch-refspec install) gets its TREE
reaped while its BRANCH ref survives — including surviving the
orphaned-branch pass. Anything not provably on origin stays preserved.

Uses a real local bare repo as `origin` so `git ls-remote`/push are genuine,
no network and no mocks.
"""

import os
import subprocess
import time
from pathlib import Path

import pytest

from hermes_cli import worktree_ops


def _run(args, cwd):
    return subprocess.run(
        args, cwd=cwd, capture_output=True, text=True, encoding="utf-8"
    )


@pytest.fixture
def repo_with_bare_origin(tmp_path):
    """A working clone whose origin is a real local bare repo.

    Mirrors the managed-install fetch config: single-branch refspec for main
    only, so pushed side branches never gain refs/remotes/origin/<branch>
    entries — the exact condition that made the old pruner misread pushed PR
    lanes as 'unpushed'.
    """
    bare = tmp_path / "origin.git"
    bare.mkdir()
    _run(["git", "init", "--bare", "--initial-branch=main"], bare)

    repo = tmp_path / "clone"
    repo.mkdir()
    _run(["git", "init", "--initial-branch=main"], repo)
    _run(["git", "config", "user.email", "test@test.com"], repo)
    _run(["git", "config", "user.name", "Test"], repo)
    (repo / "README.md").write_text("# repo\n")
    _run(["git", "add", "."], repo)
    _run(["git", "commit", "-m", "init"], repo)
    _run(["git", "remote", "add", "origin", str(bare)], repo)
    # Managed-install shape: fetch refspec covers main ONLY.
    _run(
        ["git", "config", "remote.origin.fetch",
         "+refs/heads/main:refs/remotes/origin/main"],
        repo,
    )
    _run(["git", "push", "-u", "origin", "main"], repo)
    _run(["git", "fetch", "origin"], repo)
    return repo


def _age(path, hours=100):
    t = time.time() - hours * 3600
    os.utime(path, (t, t))


def _mk_worktree(repo, name, branch, commit=True, push=False, extra_commit_after_push=False):
    """Create .worktrees/<name> on <branch> with an optional pushed commit."""
    (repo / ".worktrees").mkdir(exist_ok=True)
    p = repo / ".worktrees" / name
    _run(["git", "worktree", "add", str(p), "-b", branch, "HEAD"], repo)
    if commit:
        (p / f"{name}.txt").write_text("payload\n")
        _run(["git", "add", f"{name}.txt"], p)
        _run(["git", "commit", "-m", f"work on {name}"], p)
    if push:
        # Plain push — deliberately NO refs/remotes/* tracking ref appears,
        # because the fetch refspec only covers main.
        result = _run(["git", "push", "origin", branch], p)
        assert result.returncode == 0, result.stderr
    if extra_commit_after_push:
        (p / "later.txt").write_text("post-push work\n")
        _run(["git", "add", "later.txt"], p)
        _run(["git", "commit", "-m", "post-push commit"], p)
    _age(p)
    return p


def _branch_exists(repo, branch):
    return _run(
        ["git", "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"], repo
    ).returncode == 0


class TestFetchRemoteBranchHeads:
    def test_lists_pushed_branches(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        _mk_worktree(repo, "hermes-a", "hermes/hermes-a", push=True)
        heads = worktree_ops._fetch_remote_branch_heads(str(repo))
        assert heads is not None
        assert "main" in heads
        assert "hermes/hermes-a" in heads

    def test_unreachable_remote_returns_none(self, tmp_path):
        import cli
        repo = tmp_path / "r"
        repo.mkdir()
        _run(["git", "init"], repo)
        _run(["git", "remote", "add", "origin", str(tmp_path / "missing.git")], repo)
        assert worktree_ops._fetch_remote_branch_heads(str(repo)) is None


class TestBranchPushedExact:
    def test_exact_match_true(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-x", "hermes/hermes-x", push=True)
        heads = worktree_ops._fetch_remote_branch_heads(str(repo))
        assert worktree_ops._worktree_branch_pushed_exact(str(wt), heads) is True

    def test_local_ahead_of_push_false(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(
            repo, "hermes-y", "hermes/hermes-y",
            push=True, extra_commit_after_push=True,
        )
        heads = worktree_ops._fetch_remote_branch_heads(str(repo))
        assert worktree_ops._worktree_branch_pushed_exact(str(wt), heads) is False

    def test_never_pushed_false(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-z", "hermes/hermes-z", push=False)
        heads = worktree_ops._fetch_remote_branch_heads(str(repo))
        assert worktree_ops._worktree_branch_pushed_exact(str(wt), heads) is False

    def test_none_heads_false(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-n", "hermes/hermes-n", push=True)
        assert worktree_ops._worktree_branch_pushed_exact(str(wt), None) is False
        assert worktree_ops._worktree_branch_pushed_exact(str(wt), {}) is False


class TestStartupPrunerPushedTier:
    """cli._prune_stale_worktrees against the real repo fixture."""

    def test_pushed_lane_tree_reaped_branch_kept(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-pushed", "hermes/hermes-pushed", push=True)

        cli._prune_stale_worktrees(str(repo))

        assert not wt.exists(), (
            "pushed-as-is open-PR lane must be reaped (checkout is redundant)"
        )
        assert _branch_exists(repo, "hermes/hermes-pushed"), (
            "branch ref must survive the reap AND the orphaned-branch pass"
        )

    def test_unpushed_lane_still_preserved(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-unpushed", "hermes/hermes-unp", push=False)

        cli._prune_stale_worktrees(str(repo))

        assert wt.exists(), "never-pushed unique work must survive, any age"

    def test_ahead_of_pushed_lane_preserved(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(
            repo, "hermes-ahead", "hermes/hermes-ahead",
            push=True, extra_commit_after_push=True,
        )

        cli._prune_stale_worktrees(str(repo))

        assert wt.exists(), (
            "local commits beyond the pushed head are unpushed work — preserve"
        )

    def test_pushed_but_dirty_preserved(self, repo_with_bare_origin):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-pdirty", "hermes/hermes-pdirty", push=True)
        (wt / "uncommitted.txt").write_text("in-flight\n")
        _age(wt)

        cli._prune_stale_worktrees(str(repo))

        assert wt.exists(), "dirty guard outranks the pushed tier"

    def test_offline_remote_preserves(self, repo_with_bare_origin, tmp_path):
        import cli
        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-off", "hermes/hermes-off", push=True)
        # Sever the remote AFTER pushing: ls-remote now fails -> None ->
        # pushed tier must degrade to preserve.
        _run(["git", "remote", "set-url", "origin", str(tmp_path / "gone.git")], repo)

        cli._prune_stale_worktrees(str(repo))

        assert wt.exists(), "unverifiable push state must fail toward preserve"


class TestAttendedGcPushedTier:
    """worktree_gc audit/reclaim behavior for the pushed tier."""

    def test_audit_verdict_and_reclaim_keeps_branch(self, repo_with_bare_origin):
        import cli  # noqa: F401  (worktree_gc lazily imports cli)
        from hermes_cli import worktree_gc

        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "salv-lane", "salv/pushed-lane", push=True)

        records = worktree_gc.audit_worktrees(str(repo), with_sizes=False)
        by_name = {r.name: r for r in records}
        assert by_name["salv-lane"].verdict == "reap-keep-branch"
        assert "pushed to origin" in by_name["salv-lane"].reason

        actions = worktree_gc.reclaim_worktrees(str(repo), records=records)
        assert any("salv-lane" in a and "kept" in a for a in actions)
        assert not wt.exists()
        assert _branch_exists(repo, "salv/pushed-lane")

    def test_audit_never_pushed_keeps(self, repo_with_bare_origin):
        import cli  # noqa: F401
        from hermes_cli import worktree_gc

        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "salv-keep", "salv/never-pushed", push=False)

        records = worktree_gc.audit_worktrees(str(repo), with_sizes=False)
        by_name = {r.name: r for r in records}
        assert by_name["salv-keep"].verdict == "keep"
        worktree_gc.reclaim_worktrees(str(repo), records=records)
        assert wt.exists()


class TestCronWorktreeMaintenance:
    def test_throttle_dispatches_once_per_interval(self, monkeypatch):
        import cron.scheduler as sched

        calls = []
        monkeypatch.setattr(sched, "_last_worktree_maintenance_at", None)
        monkeypatch.setattr(
            sched.threading, "Thread",
            lambda *a, **k: type("T", (), {"start": lambda self: calls.append(k.get("name"))})(),
        )

        sched._maybe_run_worktree_maintenance()
        sched._maybe_run_worktree_maintenance()  # inside interval — no-op

        assert len(calls) == 1

    def test_repo_discovery_requires_worktrees_dir(self, monkeypatch, tmp_path):
        import cron.scheduler as sched

        # A job workdir inside a git repo WITHOUT .worktrees/ is filtered out.
        repo = tmp_path / "jobrepo"
        repo.mkdir()
        _run(["git", "init"], repo)
        monkeypatch.setattr(
            "cron.jobs.load_jobs",
            lambda: [{"workdir": str(repo)}],
        )
        repos = sched._worktree_maintenance_repos()
        assert repo not in [Path(p) for p in repos]

        # Adding .worktrees/ makes it eligible.
        (repo / ".worktrees").mkdir()
        repos = sched._worktree_maintenance_repos()
        assert repo in [Path(p) for p in repos]

    def test_maintenance_prunes_via_real_pruner(self, repo_with_bare_origin, monkeypatch):
        import cron.scheduler as sched

        repo = repo_with_bare_origin
        wt = _mk_worktree(repo, "hermes-cronreap", "hermes/hermes-cronreap", push=True)

        monkeypatch.setattr(sched, "_last_worktree_maintenance_at", None)
        monkeypatch.setattr(
            sched, "_worktree_maintenance_repos", lambda: [str(repo)]
        )

        sched._maybe_run_worktree_maintenance()
        # The sweep runs on a daemon thread — wait for it (bounded).
        deadline = time.time() + 30
        while wt.exists() and time.time() < deadline:
            time.sleep(0.2)

        assert not wt.exists(), "cron-tick maintenance must run the real pruner"
        assert _branch_exists(repo, "hermes/hermes-cronreap")
