"""Tests for push_remediated_file.py — backup -> upload -> verify -> clear staging.

All Canvas API calls are stubbed (no network). Covers: happy path, confirm-verb
refusal, non-staged-path refusal, verify-mismatch hard-fail (staging + backup
preserved), and backlog auto-close firing only on verified success. A stub for
every canvas_api write method other than upload_file raises if called, so an
unexpected write (e.g. a stray delete) fails the test immediately.
"""

import json
import os
import sys

import pytest

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

import backlog_manager  # noqa: E402
import canvas_api  # noqa: E402
import push_remediated_file  # noqa: E402

COURSE_ID = "99997"
FILE_ID = "555"


def _config():
    return {
        "token": "fake", "domain": "canvas.test.edu", "course_id": COURSE_ID,
        "base_url": "https://canvas.test.edu/api/v1",
        "course_url": f"https://canvas.test.edu/api/v1/courses/{COURSE_ID}",
        "headers": {"Authorization": "Bearer fake"},
    }


def _live_meta(size=100, updated_at="2026-01-01T00:00:00Z"):
    return {
        "id": int(FILE_ID), "display_name": "syllabus.pdf", "filename": "syllabus.pdf",
        "folder_id": 42, "content-type": "application/pdf",
        "url": "https://canvas.test.edu/files/555/download",
        "size": size, "updated_at": updated_at,
    }


def _stage_local(tmp_path, name="syllabus.pdf", content=b"remediated bytes"):
    staging_dir = tmp_path / "courses" / COURSE_ID / "staging" / "files"
    staging_dir.mkdir(parents=True, exist_ok=True)
    local = staging_dir / name
    local.write_bytes(content)
    return local


def _forbid_writes(monkeypatch):
    """Raise if any destructive canvas_api write other than upload_file fires."""
    def _boom(*a, **k):
        raise AssertionError("unexpected Canvas write method called")
    for name in ("delete_file", "delete_page", "delete_quiz",
                 "delete_assignment", "delete_discussion", "update_page"):
        monkeypatch.setattr(canvas_api, name, _boom, raising=False)


@pytest.fixture(autouse=True)
def _tmp_plugin_root(monkeypatch, tmp_path):
    monkeypatch.setattr(push_remediated_file, "get_plugin_root", lambda: str(tmp_path))
    monkeypatch.setattr(backlog_manager, "get_plugin_root", lambda: str(tmp_path))
    (tmp_path / "courses" / COURSE_ID).mkdir(parents=True, exist_ok=True)
    return tmp_path


class _FakeResponse:
    def __init__(self, content=b"old live bytes"):
        self.content = content

    def raise_for_status(self):
        pass


def test_happy_path_verifies_and_clears_staging(monkeypatch, tmp_path):
    local = _stage_local(tmp_path)
    local_size = local.stat().st_size

    _forbid_writes(monkeypatch)
    monkeypatch.setattr(canvas_api, "get_config", lambda course_id=None: _config())
    monkeypatch.setattr(canvas_api, "require_course_id", lambda config: None)
    # First call = pre-upload live-metadata fetch; second = post-write verify.
    # An iterator keeps call order explicit and unambiguous.
    responses = iter([
        _live_meta(size=999, updated_at="2026-01-01T00:00:00Z"),
        _live_meta(size=local_size, updated_at="2026-02-01T00:00:00Z"),
    ])
    monkeypatch.setattr(canvas_api, "get_file", lambda config, file_id: next(responses))
    monkeypatch.setattr(canvas_api, "upload_file",
                        lambda config, path, folder_id, content_type=None, filename=None: 555)
    monkeypatch.setattr(push_remediated_file.requests, "get",
                        lambda url, headers=None, timeout=None: _FakeResponse())

    resolve_calls = []
    monkeypatch.setattr(
        backlog_manager, "resolve_file_a11y",
        lambda course_id, file_id=None, file_name=None: (
            resolve_calls.append((course_id, file_id, file_name)) or ["backlog-abc123"]
        ),
    )

    result = push_remediated_file.push_remediated_file(COURSE_ID, FILE_ID, str(local), "push")

    assert result["ok"], result
    assert result["verified_file_id"] == "555"
    assert result["file_id_changed"] is False
    assert result["backlog_closed"] == ["backlog-abc123"]
    assert resolve_calls == [(COURSE_ID, FILE_ID, "syllabus.pdf")]
    assert not local.exists(), "staged file should be cleared on verified success"

    backup_dir = tmp_path / "courses" / COURSE_ID / "backups" / "files"
    backups = list(backup_dir.glob("*.pdf"))
    assert len(backups) == 1
    assert backups[0].read_bytes() == b"old live bytes"
    meta = json.loads((backup_dir / f"{backups[0].name}.meta.json").read_text())
    assert meta["file_id"] == FILE_ID


def test_refuses_ambiguous_confirm_verb(monkeypatch, tmp_path):
    local = _stage_local(tmp_path)
    called = {"n": 0}
    monkeypatch.setattr(canvas_api, "get_config",
                        lambda course_id=None: called.update(n=called["n"] + 1))
    for verb in ("yes", "ok", "confirm", ""):
        result = push_remediated_file.push_remediated_file(COURSE_ID, FILE_ID, str(local), verb)
        assert not result["ok"]
        assert "confirm-verb" in result["error"]
    assert called["n"] == 0, "should refuse before ever touching Canvas config"


def test_refuses_non_staged_local_path(monkeypatch, tmp_path):
    outside = tmp_path / "not-staged.pdf"
    outside.write_bytes(b"x")
    called = {"n": 0}
    monkeypatch.setattr(canvas_api, "get_config",
                        lambda course_id=None: called.update(n=called["n"] + 1))

    result = push_remediated_file.push_remediated_file(COURSE_ID, FILE_ID, str(outside), "push")

    assert not result["ok"]
    assert "staging/files" in result["error"]
    assert called["n"] == 0, "should refuse before ever touching Canvas config"


def test_verify_mismatch_hard_fails_preserves_staging_and_backup(monkeypatch, tmp_path):
    local = _stage_local(tmp_path)

    _forbid_writes(monkeypatch)
    monkeypatch.setattr(canvas_api, "get_config", lambda course_id=None: _config())
    monkeypatch.setattr(canvas_api, "require_course_id", lambda config: None)
    # Every re-fetch (pre-upload AND both verify attempts) returns stale metadata
    # that never reflects the new size/updated_at -- simulates a write that
    # silently didn't take.
    monkeypatch.setattr(canvas_api, "get_file",
                        lambda config, file_id: _live_meta(size=999,
                                                           updated_at="2026-01-01T00:00:00Z"))
    monkeypatch.setattr(canvas_api, "upload_file",
                        lambda config, path, folder_id, content_type=None, filename=None: 555)
    monkeypatch.setattr(push_remediated_file.requests, "get",
                        lambda url, headers=None, timeout=None: _FakeResponse())

    resolve_calls = []
    monkeypatch.setattr(
        backlog_manager, "resolve_file_a11y",
        lambda course_id, file_id=None, file_name=None: resolve_calls.append(1),
    )

    result = push_remediated_file.push_remediated_file(COURSE_ID, FILE_ID, str(local), "push")

    assert not result["ok"]
    assert result["staging_preserved"] is True
    assert result["backup_path"]
    assert os.path.exists(result["backup_path"]), "backup must survive a verify failure"
    assert local.exists(), "staged file must NOT be cleared on verify failure"
    assert not resolve_calls, "backlog must not be auto-closed on a failed push"


def test_upload_failure_preserves_staging_and_backup(monkeypatch, tmp_path):
    local = _stage_local(tmp_path)

    _forbid_writes(monkeypatch)
    monkeypatch.setattr(canvas_api, "get_config", lambda course_id=None: _config())
    monkeypatch.setattr(canvas_api, "require_course_id", lambda config: None)
    monkeypatch.setattr(canvas_api, "get_file", lambda config, file_id: _live_meta())
    monkeypatch.setattr(canvas_api, "upload_file",
                        lambda config, path, folder_id, content_type=None, filename=None: None)
    monkeypatch.setattr(push_remediated_file.requests, "get",
                        lambda url, headers=None, timeout=None: _FakeResponse())

    result = push_remediated_file.push_remediated_file(COURSE_ID, FILE_ID, str(local), "push")

    assert not result["ok"]
    assert result["staging_preserved"] is True
    assert os.path.exists(result["backup_path"])
    assert local.exists()
