from __future__ import annotations

import subprocess
from datetime import datetime
from pathlib import Path

from recoil.pipeline.tools.autonomy import tick


READY_BODY = """\
# Goal
Do the thing.

# Acceptance Criteria
- Implement the change and verify it.

# Verification
Run tests.

# Scope
One focused change.

# Out of Scope
No control-plane changes.
"""


class StubRunner:
    def __init__(self, worktree: Path, *, marker_on_launch: bool = True):
        self.worktree = worktree
        self.marker_on_launch = marker_on_launch
        self.calls: list[list[str]] = []

    def __call__(self, cmd, **kwargs):
        del kwargs
        command = list(cmd)
        self.calls.append(command)
        if "session_workspace.sh" in command[0] and command[1] == "create":
            self.worktree.mkdir(parents=True, exist_ok=True)
            return subprocess.CompletedProcess(
                command,
                0,
                stdout=f"created codex/REC-1-auton  worktree: {self.worktree}\n",
                stderr="",
            )
        if command[:3] == ["tmux", "new-session", "-d"]:
            if self.marker_on_launch:
                (self.worktree / "build-log.md").write_text(
                    "## Codex Spec Review\nREADY\n",
                    encoding="utf-8",
                )
            return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
        if command[:3] == ["tmux", "kill-session", "-t"]:
            return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
        return subprocess.CompletedProcess(command, 0, stdout="", stderr="")


def _ready_issue(issue_id: str = "issue-1", identifier: str = "REC-1", priority: int = 2):
    return {
        "issue_id": issue_id,
        "identifier": identifier,
        "title": "Ready work",
        "body": READY_BODY,
        "labels": ["autonomy-ok"],
        "priority": priority,
    }


def _stub_common(monkeypatch, tmp_path: Path, *, candidates=None, cap=(True, "")):
    calls: dict[str, list] = {
        "events": [],
        "claims": [],
        "claim_releases": [],
        "project_claims": [],
        "maintenance": [],
        "lease_releases": [],
        "lease_converts": [],
        "build_started": [],
        "reaps": [],
    }
    worktrees_root = tmp_path / "sessions"
    monkeypatch.setattr(tick.constants, "WORKTREES_ROOT", worktrees_root)
    monkeypatch.setattr(tick, "SESSION_WORKSPACE", Path("/tmp/session_workspace.sh"))
    monkeypatch.setattr(tick, "SUPERVISOR", Path("/tmp/supervisor.sh"))
    monkeypatch.setattr(tick, "_dispatch_status_active", lambda: False)
    monkeypatch.setattr(tick, "_live_tmux_with_build_marker", lambda runner: False)
    monkeypatch.setattr(tick, "preflight", lambda: (True, "ok"))
    monkeypatch.setattr(tick.reaper, "reap", lambda: calls["reaps"].append(True))
    monkeypatch.setattr(tick, "human_active", lambda repo_root: (False, "idle"))
    monkeypatch.setattr(tick.resource_gate, "may_start_build", lambda night: cap)
    monkeypatch.setattr(
        tick.resource_gate,
        "record_build_started",
        lambda night: calls["build_started"].append(night) or len(calls["build_started"]),
    )
    monkeypatch.setattr(
        tick.events,
        "emit",
        lambda event_type, **fields: calls["events"].append((event_type, fields)),
    )
    monkeypatch.setattr(
        tick.linear_client,
        "list_candidates",
        lambda team: list(candidates if candidates is not None else [_ready_issue()]),
    )
    monkeypatch.setattr(
        tick.linear_client,
        "project_claim",
        lambda issue_id, identifier, run_id: calls["project_claims"].append(
            (issue_id, identifier, run_id)
        )
        or True,
    )
    monkeypatch.setattr(
        tick.claim_ledger,
        "claim",
        lambda issue_id, identifier, run_id, night: calls["claims"].append(
            (issue_id, identifier, run_id, night)
        )
        or {"issue_id": issue_id, "run_id": run_id},
    )
    monkeypatch.setattr(
        tick.claim_ledger,
        "release",
        lambda issue_id, run_id, state, failure_signature=None: calls[
            "claim_releases"
        ].append((issue_id, run_id, state, failure_signature))
        or {"issue_id": issue_id, "run_id": run_id, "state": state},
    )
    monkeypatch.setattr(
        tick.lease,
        "acquire",
        lambda mode, **kwargs: {"mode": mode, "run_id": kwargs["run_id"]},
    )
    monkeypatch.setattr(tick.lease, "read", lambda: {"mode": "tick"})
    monkeypatch.setattr(
        tick.lease,
        "release",
        lambda run_id: calls["lease_releases"].append(run_id) or True,
    )
    monkeypatch.setattr(
        tick.lease,
        "convert",
        lambda run_id, **kwargs: calls["lease_converts"].append((run_id, kwargs))
        or True,
    )
    monkeypatch.setattr(
        tick.nightwatch_payload,
        "run_maintenance",
        lambda run_id, night, shadow=None: calls["maintenance"].append(
            (run_id, night, shadow)
        )
        or {"ran": []},
    )
    return calls


def test_happy_path_claims_dispatches_and_converts_lease(monkeypatch, tmp_path):
    calls = _stub_common(
        monkeypatch,
        tmp_path,
        candidates=[_ready_issue("low", "REC-2", 4), _ready_issue("high", "REC-1", 1)],
    )
    runner = StubRunner(tmp_path / "sessions" / "host" / "REC-1--codex--sid")

    rc = tick.tick_once(
        runner=runner,
        now=datetime(2026, 6, 7, 1, 2, 3),
        startup_poll_interval=0,
        environ={"AUTONOMY_LINEAR_TEAM": "Recoil"},
    )

    assert rc == 0
    assert calls["reaps"] == [True]
    assert calls["claims"][0][0:2] == ("high", "REC-1")
    assert any(event == "issue_claimed" for event, _fields in calls["events"])
    assert any(event == "dispatch_started" for event, _fields in calls["events"])
    assert calls["lease_converts"][0][1]["new_mode"] == "build"
    assert calls["lease_converts"][0][1]["tmux_session"] == "autonomy-auton-20260607-010203"
    assert calls["build_started"] == ["2026-06-06-evening"]
    assert any(call[:3] == ["tmux", "new-session", "-d"] for call in runner.calls)


def test_human_active_yields_without_claim(monkeypatch, tmp_path):
    calls = _stub_common(monkeypatch, tmp_path)
    monkeypatch.setattr(tick, "human_active", lambda repo_root: (True, "recent_login"))
    runner = StubRunner(tmp_path / "sessions" / "host" / "REC-1--codex--sid")

    rc = tick.tick_once(
        runner=runner,
        now=datetime(2026, 6, 7, 1, 2, 3),
        environ={"AUTONOMY_LINEAR_TEAM": "Recoil"},
    )

    assert rc == 0
    assert calls["claims"] == []
    assert ("yield_human_active", {
        "run_id": "auton-20260607-010203",
        "night_id": "2026-06-06-evening",
        "reason": "recent_login",
    }) in calls["events"]


def test_no_ready_candidates_runs_maintenance_and_emits_no_work(monkeypatch, tmp_path):
    calls = _stub_common(monkeypatch, tmp_path, candidates=[dict(_ready_issue(), ready=False)])
    monkeypatch.setattr(tick.readiness, "is_ready", lambda issue: (False, ["not ready"]))
    runner = StubRunner(tmp_path / "sessions" / "host" / "REC-1--codex--sid")

    rc = tick.tick_once(
        runner=runner,
        now=datetime(2026, 6, 7, 1, 2, 3),
        shadow="tier1",
        environ={"AUTONOMY_LINEAR_TEAM": "Recoil"},
    )

    assert rc == 0
    assert calls["claims"] == []
    assert calls["maintenance"] == [
        ("auton-20260607-010203", "2026-06-06-evening", "tier1")
    ]
    assert any(event == "no_work" for event, _fields in calls["events"])
    assert not any(call and call[0] == "tmux" for call in runner.calls)


def test_cap_reached_does_not_dispatch(monkeypatch, tmp_path):
    calls = _stub_common(monkeypatch, tmp_path, cap=(False, "builds_cap"))
    runner = StubRunner(tmp_path / "sessions" / "host" / "REC-1--codex--sid")

    rc = tick.tick_once(
        runner=runner,
        now=datetime(2026, 6, 7, 1, 2, 3),
        environ={"AUTONOMY_LINEAR_TEAM": "Recoil"},
    )

    assert rc == 0
    assert calls["claims"] == []
    assert calls["build_started"] == []
    assert any(
        event == "cap_tripped" and fields["reason"] == "builds_cap"
        for event, fields in calls["events"]
    )
    assert not any(call and call[0] == "tmux" for call in runner.calls)


def test_startup_marker_absent_releases_claim_and_lease(monkeypatch, tmp_path):
    calls = _stub_common(monkeypatch, tmp_path)
    runner = StubRunner(
        tmp_path / "sessions" / "host" / "REC-1--codex--sid",
        marker_on_launch=False,
    )

    rc = tick.tick_once(
        runner=runner,
        now=datetime(2026, 6, 7, 1, 2, 3),
        startup_timeout=0,
        startup_poll_interval=0,
        environ={"AUTONOMY_LINEAR_TEAM": "Recoil"},
    )

    assert rc == 1
    assert calls["claim_releases"] == [
        ("issue-1", "auton-20260607-010203", "failed", "startup_marker_absent")
    ]
    assert calls["lease_releases"] == ["auton-20260607-010203"]
    assert calls["lease_converts"] == []
    assert any(call[:3] == ["tmux", "kill-session", "-t"] for call in runner.calls)


def test_tier0_shadow_claims_but_does_not_create_worktree_or_supervisor(monkeypatch, tmp_path):
    calls = _stub_common(monkeypatch, tmp_path)
    runner = StubRunner(tmp_path / "sessions" / "host" / "REC-1--codex--sid")

    rc = tick.tick_once(
        runner=runner,
        now=datetime(2026, 6, 7, 1, 2, 3),
        shadow="tier0",
        environ={"AUTONOMY_LINEAR_TEAM": "Recoil"},
    )

    assert rc == 0
    assert calls["claims"][0][0:2] == ("issue-1", "REC-1")
    assert calls["lease_converts"] == []
    assert any(event == "shadow_would_dispatch" for event, _fields in calls["events"])
    assert runner.calls == []
