from __future__ import annotations

import subprocess
from pathlib import Path

from recoil.pipeline.tools.autonomy import nightwatch_payload


class StubRunner:
    def __init__(self, outputs: dict[str, subprocess.CompletedProcess[str]] | None = None):
        self.calls: list[list[str]] = []
        self.outputs = outputs or {}

    def __call__(self, cmd, **kwargs):
        self.calls.append(list(cmd))
        step = cmd[3]
        return self.outputs.get(
            step,
            subprocess.CompletedProcess(cmd, 0, stdout=f"{step} ok\n", stderr=""),
        )


def _isolate(monkeypatch, tmp_path: Path):
    monkeypatch.setattr(nightwatch_payload.constants, "MAINT_DIR", tmp_path / "maintenance")
    monkeypatch.setattr(nightwatch_payload, "REPORT_DIR", tmp_path / "logs")
    monkeypatch.setattr(nightwatch_payload.resource_gate, "breaker_tripped", lambda night_id: False)
    emitted = []
    monkeypatch.setattr(
        nightwatch_payload.events,
        "emit",
        lambda event_type, **fields: emitted.append((event_type, fields)),
    )
    return emitted


def test_run_maintenance_calls_ingest_verify_report_emits_and_touches(monkeypatch, tmp_path):
    emitted = _isolate(monkeypatch, tmp_path)
    runner = StubRunner(
        {
            "report": subprocess.CompletedProcess(
                ["python3", "-m", "recoil.pipeline.tools.nightwatch", "report"],
                0,
                stdout="# Nightwatch\n\nNo confirmed findings.\n",
                stderr="",
            )
        }
    )

    result = nightwatch_payload.run_maintenance(
        "run-1",
        "2026-06-06-evening",
        runner=runner,
    )

    assert [call[3:] for call in runner.calls] == [
        ["ingest"],
        ["verify", "--limit", "5"],
        ["report"],
    ]
    assert result["ran"] == ["ingest", "verify", "report"]
    assert result["skipped_verify"] is False
    report_path = Path(result["report_path"])
    assert report_path.read_text(encoding="utf-8") == "# Nightwatch\n\nNo confirmed findings.\n"
    assert (tmp_path / "maintenance" / "2026-06-06-evening.done").exists()
    assert emitted == [
        (
            "maintenance_ran",
            {
                "run_id": "run-1",
                "night_id": "2026-06-06-evening",
                "report_path": str(report_path),
            },
        )
    ]


def test_second_call_same_night_skips_without_event_or_runner(monkeypatch, tmp_path):
    emitted = _isolate(monkeypatch, tmp_path)
    night_id = "2026-06-06-evening"
    sentinel = tmp_path / "maintenance" / f"{night_id}.done"
    sentinel.parent.mkdir(parents=True)
    sentinel.touch()
    runner = StubRunner()

    result = nightwatch_payload.run_maintenance("run-2", night_id, runner=runner)

    assert result == {"skipped": "already_ran"}
    assert runner.calls == []
    assert emitted == []


def test_shadow_dry_tier_skips_verify(monkeypatch, tmp_path):
    _isolate(monkeypatch, tmp_path)
    runner = StubRunner()

    result = nightwatch_payload.run_maintenance(
        "run-shadow",
        "2026-06-07-evening",
        shadow=True,
        runner=runner,
    )

    assert [call[3:] for call in runner.calls] == [["ingest"], ["report"]]
    assert result["ran"] == ["ingest", "report"]
    assert result["skipped_verify"] is True


def test_rate_limit_output_trips_breaker_and_stops(monkeypatch, tmp_path):
    _isolate(monkeypatch, tmp_path)
    tripped = []
    monkeypatch.setattr(
        nightwatch_payload.resource_gate,
        "trip_breaker",
        lambda night_id, reason: tripped.append((night_id, reason)),
    )
    runner = StubRunner(
        {
            "verify": subprocess.CompletedProcess(
                ["python3", "-m", "recoil.pipeline.tools.nightwatch", "verify", "--limit", "5"],
                1,
                stdout="",
                stderr="HTTP 429 Too Many Requests",
            )
        }
    )

    result = nightwatch_payload.run_maintenance(
        "run-rate",
        "2026-06-08-evening",
        runner=runner,
    )

    assert result == {"rate_limited": True}
    assert [call[3:] for call in runner.calls] == [["ingest"], ["verify", "--limit", "5"]]
    assert tripped and tripped[0][0] == "2026-06-08-evening"
    assert "429" in tripped[0][1]
    assert not (tmp_path / "maintenance" / "2026-06-08-evening.done").exists()
