"""Tests for deliver_batch.py — batched-verb delivery fold (plan §4, AC4).

push_remediated_file.push_remediated_file is stubbed everywhere (no network, no
Canvas). Covers: mixed-batch routing (deliver / skip-gate-fail / needs-manual /
appearance-drift), verb refusal, retire-smuggling guard, a failed delivery attempt
(distinct from a correctly-skipped item), office appearance routing both ways, and
provenance-string presence on delivered items.
"""

import os
import re
import sys

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import deliver_batch  # noqa: E402
import push_remediated_file  # noqa: E402

COURSE_ID = "99997"
PROVENANCE_RE = re.compile(r"^batched-verb:(push|approved|deploy)@\d{8}T\d{6}Z$")


def _pdf_item(file_id, compliant=True, fidelity=True, needs_manual=False):
    return {
        "local_path": f"/staging/{file_id}.pdf",
        "file_id": file_id,
        "display_name": f"file-{file_id}.pdf",
        "kind": "pdf",
        "gates": {"verapdf_compliant": compliant, "fidelity_identical": fidelity},
        "appearance": None,
        "needs_manual": needs_manual,
    }


def _office_item(file_id, appearance_ok, needs_manual=False):
    return {
        "local_path": f"/staging/{file_id}.docx",
        "file_id": file_id,
        "display_name": f"file-{file_id}.docx",
        "kind": "office",
        "gates": {},
        "appearance": {"ok": appearance_ok},
        "needs_manual": needs_manual,
    }


def _stub_push(monkeypatch, fn):
    monkeypatch.setattr(push_remediated_file, "push_remediated_file", fn)


def test_mixed_batch_routes_correctly_and_calls_push_exactly_twice(monkeypatch):
    calls = []

    def fake_push(course_id, file_id, local_path, confirm_verb):
        calls.append(file_id)
        return {"ok": True, "canvas_url": f"https://canvas.test/files/{file_id}",
                 "backup_path": f"/backups/{file_id}"}

    _stub_push(monkeypatch, fake_push)

    manifest = [
        _pdf_item("1", compliant=True, fidelity=True),
        _pdf_item("2", compliant=True, fidelity=True),
        _pdf_item("3", compliant=False, fidelity=True),  # gate-fail
        _office_item("4", appearance_ok=False),  # appearance-drift
        _pdf_item("5", compliant=True, fidelity=True, needs_manual=True),  # needs_manual
    ]

    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "push")

    assert result["ok"], result
    assert calls == ["1", "2"], "push stub must fire exactly for the 2 gate-passing files"
    assert {d["file_id"] for d in result["delivered"]} == {"1", "2"}
    assert {d["file_id"] for d in result["skipped_gate_fail"]} == {"3", "5"}
    assert {d["file_id"] for d in result["needs_individual_approval"]} == {"4"}
    assert result["failed"] == []
    assert result["totals"] == {
        "manifest_count": 5, "delivered": 2, "skipped_gate_fail": 2,
        "needs_individual_approval": 1, "skipped_malformed": 0, "failed": 0,
    }


def test_bad_verb_refuses_before_any_push(monkeypatch):
    calls = []
    _stub_push(monkeypatch, lambda *a, **k: calls.append(1))

    manifest = [_pdf_item("1")]
    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "ok")

    assert not result["ok"]
    assert "confirm-verb" in result["error"]
    assert calls == []


def test_retire_smuggled_into_manifest_errors_and_delivers_nothing(monkeypatch):
    calls = []
    _stub_push(monkeypatch, lambda *a, **k: calls.append(1))

    manifest = [
        _pdf_item("1"),  # would otherwise deliver
        {"file_id": "2", "display_name": "old.pdf", "action": "retire"},
    ]
    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "push")

    assert not result["ok"]
    assert "retire" in result["error"]
    assert calls == [], "no delivery may happen once any row smuggles action=retire"


def test_failed_delivery_lands_in_failed_bucket_others_still_processed(monkeypatch):
    def fake_push(course_id, file_id, local_path, confirm_verb):
        if file_id == "1":
            return {"ok": False, "error": "verify mismatch", "backup_path": "/backups/1"}
        return {"ok": True, "canvas_url": f"https://canvas.test/files/{file_id}",
                 "backup_path": f"/backups/{file_id}"}

    _stub_push(monkeypatch, fake_push)

    manifest = [_pdf_item("1"), _pdf_item("2")]
    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "push")

    assert not result["ok"], "a failed delivery attempt must not report batch ok"
    assert len(result["failed"]) == 1
    assert result["failed"][0]["file_id"] == "1"
    assert result["failed"][0]["error"] == "verify mismatch"
    assert {d["file_id"] for d in result["delivered"]} == {"2"}, \
        "the other item must still be processed and delivered"
    assert result["totals"]["failed"] == 1


def test_cli_exits_nonzero_on_failed_delivery(monkeypatch, tmp_path, capsys):
    import json as _json

    def fake_push(course_id, file_id, local_path, confirm_verb):
        return {"ok": False, "error": "verify mismatch", "backup_path": "/backups/1"}

    _stub_push(monkeypatch, fake_push)

    manifest_path = tmp_path / "batch.json"
    manifest_path.write_text(_json.dumps([_pdf_item("1")]))

    monkeypatch.setattr(sys, "argv", [
        "deliver_batch.py", "--course-id", COURSE_ID,
        "--manifest", str(manifest_path), "--confirm-verb", "push", "--json",
    ])
    with pytest.raises(SystemExit) as exc_info:
        deliver_batch.main()
    assert exc_info.value.code != 0


def test_cli_exits_zero_when_only_correctly_skipped(monkeypatch, tmp_path):
    import json as _json

    _stub_push(monkeypatch, lambda *a, **k: (_ for _ in ()).throw(
        AssertionError("push must not be called for a gate-fail-only batch")))

    manifest_path = tmp_path / "batch.json"
    manifest_path.write_text(_json.dumps([_pdf_item("1", compliant=False)]))

    monkeypatch.setattr(sys, "argv", [
        "deliver_batch.py", "--course-id", COURSE_ID,
        "--manifest", str(manifest_path), "--confirm-verb", "push", "--json",
    ])
    deliver_batch.main()  # must NOT raise SystemExit -- a clean run has no sys.exit() call


def test_office_appearance_ok_true_delivers(monkeypatch):
    calls = []

    def fake_push(course_id, file_id, local_path, confirm_verb):
        calls.append(file_id)
        return {"ok": True, "canvas_url": "https://canvas.test/files/9",
                 "backup_path": "/backups/9"}

    _stub_push(monkeypatch, fake_push)

    manifest = [_office_item("9", appearance_ok=True)]
    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "push")

    assert result["ok"], result
    assert calls == ["9"]
    assert {d["file_id"] for d in result["delivered"]} == {"9"}
    assert result["needs_individual_approval"] == []


def test_office_appearance_ok_false_needs_individual_approval(monkeypatch):
    _stub_push(monkeypatch, lambda *a, **k: (_ for _ in ()).throw(
        AssertionError("push must not fire for an appearance-drift file")))

    manifest = [_office_item("9", appearance_ok=False)]
    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "push")

    assert result["ok"], result
    assert result["delivered"] == []
    assert {d["file_id"] for d in result["needs_individual_approval"]} == {"9"}
    assert "appearance-drift" in result["needs_individual_approval"][0]["reason"]


def test_provenance_string_present_on_delivered_items(monkeypatch):
    def fake_push(course_id, file_id, local_path, confirm_verb):
        return {"ok": True, "canvas_url": "https://canvas.test/files/1",
                 "backup_path": "/backups/1"}

    _stub_push(monkeypatch, fake_push)

    manifest = [_pdf_item("1")]
    result = deliver_batch.deliver_batch(COURSE_ID, manifest, "approved")

    assert result["delivered"][0]["approved_via"] == result["approved_via"]
    assert PROVENANCE_RE.match(result["approved_via"]), result["approved_via"]


def test_office_row_missing_appearance_never_pdf_routed(monkeypatch):
    """Codex finding 1: an office row missing appearance must NOT ride the PDF
    gates and deliver — it is malformed, never delivered."""
    monkeypatch.setattr(deliver_batch.push_remediated_file, "push_remediated_file",
                        lambda *a, **k: (_ for _ in ()).throw(
                            AssertionError("push must not fire for a malformed office row")))
    bad = {"file_id": "9", "display_name": "f.docx", "kind": "office",
           "local_path": "/x", "gates": {"verapdf_compliant": True, "fidelity_identical": True}}
    result = deliver_batch.deliver_batch("256162", [bad], "push")
    assert result["totals"]["delivered"] == 0
    assert result["totals"]["skipped_malformed"] == 1
    assert result["ok"] is False


def test_missing_kind_is_malformed_not_delivered(monkeypatch):
    monkeypatch.setattr(deliver_batch.push_remediated_file, "push_remediated_file",
                        lambda *a, **k: (_ for _ in ()).throw(AssertionError("no push")))
    row = {"file_id": "1", "gates": {"verapdf_compliant": True, "fidelity_identical": True}}
    result = deliver_batch.deliver_batch("256162", [row], "push")
    assert result["totals"]["skipped_malformed"] == 1 and result["ok"] is False


def test_non_dict_manifest_row_controlled_skip(monkeypatch):
    monkeypatch.setattr(deliver_batch.push_remediated_file, "push_remediated_file",
                        lambda *a, **k: {"ok": True})
    result = deliver_batch.deliver_batch("256162", [_pdf_item("1"), "garbage", None], "push")
    assert result["totals"]["delivered"] == 1
    assert result["totals"]["skipped_malformed"] == 2  # controlled, no traceback


def test_non_list_manifest_errors():
    result = deliver_batch.deliver_batch("256162", {"not": "a list"}, "push")
    assert result["ok"] is False and "list" in result["error"]


def test_retire_row_last_still_blocks(monkeypatch):
    monkeypatch.setattr(deliver_batch.push_remediated_file, "push_remediated_file",
                        lambda *a, **k: (_ for _ in ()).throw(AssertionError("no push on retire batch")))
    manifest = [_pdf_item("1"), _pdf_item("2"), {"file_id": "3", "action": "retire"}]
    result = deliver_batch.deliver_batch("256162", manifest, "push")
    assert result["ok"] is False and "retire" in result["error"]


def test_partial_failure_first_two_succeed_third_fails(monkeypatch):
    calls = {"n": 0}
    def stub(course_id, file_id, local_path, verb):
        calls["n"] += 1
        return {"ok": file_id != "3", "error": None if file_id != "3" else "verify mismatch"}
    monkeypatch.setattr(deliver_batch.push_remediated_file, "push_remediated_file", stub)
    manifest = [_pdf_item("1"), _pdf_item("2"), _pdf_item("3")]
    result = deliver_batch.deliver_batch("256162", manifest, "push")
    assert result["totals"]["delivered"] == 2  # 1 and 2 stay delivered
    assert result["totals"]["failed"] == 1 and result["failed"][0]["file_id"] == "3"
    assert result["ok"] is False
