"""Unit tests for cis490_doctor's recent-shipping-errors parser.

The parser scans `journalctl -u cis490-shipper` output for the same
strings the receiver/shipper actually emit. We monkeypatch `_run` so
the test doesn't need a real systemd journal.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location(
    "cis490_doctor", REPO_ROOT / "tools" / "cis490_doctor.py"
)
doctor = importlib.util.module_from_spec(spec)
sys.modules["cis490_doctor"] = doctor
spec.loader.exec_module(doctor)


def _run_journal(monkeypatch, output: str, rc: int = 0) -> None:
    """Stub `cis490_doctor._run` to return ``output`` for journalctl
    calls, real subprocess for everything else."""
    real = doctor._run

    def fake(cmd):
        if cmd and cmd[0] == "journalctl":
            return rc, output, ""
        return real(cmd)
    monkeypatch.setattr(doctor, "_run", fake)


def _check_for(report: doctor.Report, name_substr: str) -> doctor.Check | None:
    for c in report.checks:
        if name_substr in c.name:
            return c
    return None


def test_412_pattern_surfaces_as_fail(monkeypatch: pytest.MonkeyPatch) -> None:
    """5 minutes of 412 commit-rejected logs is the exact symptom we
    saw on k-gamingcom for 5+ hours pre-fix. Doctor must point the
    operator at the canonical pull+reinstall path."""
    log = "\n".join(
        f"ship 01EP{i:02d} -> 412 commit-rejected. your_commit=abc..."
        for i in range(20)
    )
    _run_journal(monkeypatch, log)
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent ship results")
    assert c is not None and c.status == "fail"
    assert "out-of-window" in c.detail
    assert "git pull origin main" in c.fix
    assert "install-lab-host.sh" in c.fix


def test_400_missing_commit_pattern(monkeypatch: pytest.MonkeyPatch) -> None:
    log = "\n".join(
        f'episode 01EP{i:02d} -> fatal (400) "missing X-Cis490-Code-Commit header"'
        for i in range(5)
    )
    _run_journal(monkeypatch, log)
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent ship results")
    assert c is not None and c.status == "fail"
    assert "missing-commit-header" in c.detail
    assert "install-lab-host.sh" in c.fix


def test_clean_journal_is_ok(monkeypatch: pytest.MonkeyPatch) -> None:
    log = "\n".join(
        f"ship 01EP{i:02d} -> stored (201) " for i in range(10)
    )
    _run_journal(monkeypatch, log)
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent ship results")
    assert c is not None and c.status == "ok"


def test_empty_journal_is_ok_idle(monkeypatch: pytest.MonkeyPatch) -> None:
    """No log output ≠ broken — could be a daemon that finished
    everything and is idle."""
    _run_journal(monkeypatch, "")
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent ship results")
    assert c is not None and c.status == "ok"
    assert "idle" in c.detail


def test_journalctl_unavailable_skips(monkeypatch: pytest.MonkeyPatch) -> None:
    """Doctor often runs as the unprivileged user and reading the
    system journal can need systemd-journal group membership.
    journalctl returning non-zero (perm denied / not installed)
    should produce a `skip` row, not noise."""
    _run_journal(monkeypatch, "", rc=1)
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent log scan")
    assert c is not None and c.status == "skip"


def test_transient_failures_warn_at_threshold(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """A pile of transient (5xx/network) failures isn't fatal but
    it's worth flagging — the receiver might be down or WG might be
    flaking. The threshold (>5) is tuned to ignore an occasional
    blip."""
    log = "\n".join(
        f"ship 01EP{i:02d} -> transient: timeout" for i in range(10)
    )
    _run_journal(monkeypatch, log)
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent ship results")
    assert c is not None and c.status == "warn"
    assert "transient" in c.detail


def test_412_takes_priority_over_400(monkeypatch: pytest.MonkeyPatch) -> None:
    """If both error classes are present, 412 (out-of-date code)
    is the more actionable diagnosis — fixing it fixes both, since
    a re-install also writes VERSION."""
    log = (
        "ship 01OLD -> 412 commit-rejected\n"
        'ship 01OLD2 -> fatal (400) "missing X-Cis490-Code-Commit header"\n'
    )
    _run_journal(monkeypatch, log)
    report = doctor.Report(role="lab-host")
    doctor.check_recent_shipping_errors(report)
    c = _check_for(report, "recent ship results")
    assert c is not None and c.status == "fail"
    assert "out-of-window" in c.detail
