"""Tests for PR-B: image staging push path (B1 token resolution in push_to_canvas).

Covers:
- B1: tokens in page body are replaced with Canvas URLs before push
- B1: pushed body contains NO remaining asucb-staged-file:// tokens
- B1: staged files are cleared after overall success
- B1: upload failure aborts push, preserves staging, surfaces failed key
- B1: multiple tokens + repeated token resolve with exactly one upload per unique key
"""

import json
import pathlib
import sys
import unittest.mock as mock

import pytest

SCRIPTS_DIR = pathlib.Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))

import push_to_canvas
import staging_manager


# ============================================================
# Fixtures
# ============================================================

COURSE_ID = "99999"
SLUG = "m1-test-page"


@pytest.fixture()
def plugin_root(tmp_path, monkeypatch):
    """Redirect plugin root so all staging I/O goes to tmp_path."""
    import canvas_api
    monkeypatch.setattr(canvas_api, "get_plugin_root", lambda: str(tmp_path))
    return tmp_path


@pytest.fixture()
def sample_png_bytes():
    """Minimal PNG bytes (just the signature)."""
    return b"\x89PNG\r\n\x1a\n" + b"\x00" * 8


@pytest.fixture()
def staged_image(plugin_root, sample_png_bytes, tmp_path):
    """Stage a PNG file with key 'hero-img' and return the key."""
    src = tmp_path / "hero.png"
    src.write_bytes(sample_png_bytes)
    result = staging_manager.stage_file(
        COURSE_ID, "hero-img", "png", src,
        meta={"mime": "image/png", "alt": "Hero image", "module": "1"},
    )
    assert result["ok"] is True, f"stage_file failed: {result}"
    return "hero-img"


@pytest.fixture()
def staged_page_with_token(plugin_root, staged_image):
    """Stage a page HTML containing one asucb-staged-file://hero-img token."""
    html = (
        "<h1>Test Page</h1>"
        '<img src="asucb-staged-file://hero-img" alt="Hero image">'
    )
    r = staging_manager.stage(COURSE_ID, "pages", SLUG, html, validate=False)
    assert r["ok"] is True
    return html


@pytest.fixture()
def mock_config():
    return {
        "token": "fake-token",
        "domain": "canvas.asu.edu",
        "course_id": COURSE_ID,
        "base_url": "https://canvas.asu.edu/api/v1",
        "course_url": f"https://canvas.asu.edu/api/v1/courses/{COURSE_ID}",
        "headers": {"Authorization": "Bearer fake-token"},
    }


# ============================================================
# Helper: build a canvas_api.upload_image mock that returns a result
# ============================================================

def _upload_mock(file_id=42001):
    """Return a mock for canvas_api.upload_image that yields embed_url."""
    def _upload(config, filepath, module_number=None, content_type=None):
        cid = config["course_id"]
        return {"file_id": file_id, "embed_url": f"/courses/{cid}/files/{file_id}/preview"}
    return _upload


# ============================================================
# B1: basic token replacement
# ============================================================

class TestTokenReplacement:

    def test_token_replaced_with_canvas_url(
        self, plugin_root, staged_page_with_token, mock_config
    ):
        """Tokens in page body should be replaced with Canvas embed URLs."""
        with mock.patch("canvas_api.upload_image", side_effect=_upload_mock(42001)):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        result = push_to_canvas.push_page(
                            mock_config, SLUG, COURSE_ID
                        )

        assert result["ok"] is True
        # Verify no tokens remain by checking what update_page was called with.
        # We need to capture the body passed to update_page.

    def test_pushed_body_has_no_remaining_tokens(
        self, plugin_root, staged_page_with_token, mock_config
    ):
        """The body passed to canvas_api.update_page must have zero remaining tokens."""
        captured = {}

        def _capture_update_page(config, slug, body, from_staging=False):
            captured["body"] = body
            return True

        with mock.patch("canvas_api.upload_image", side_effect=_upload_mock(42001)):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", side_effect=_capture_update_page):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        push_to_canvas.push_page(mock_config, SLUG, COURSE_ID)

        assert "asucb-staged-file://" not in captured.get("body", ""), (
            "Body still contains unresolved asucb-staged-file:// tokens after push"
        )
        assert "/files/42001/preview" in captured["body"]

    def test_staged_files_cleared_on_success(
        self, plugin_root, staged_image, staged_page_with_token, mock_config
    ):
        """Staged files for uploaded keys must be cleared after successful push."""
        # Confirm staged file exists before push
        assert staging_manager.get_staged_file(COURSE_ID, staged_image) is not None

        with mock.patch("canvas_api.upload_image", side_effect=_upload_mock(42001)):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        result = push_to_canvas.push_page(
                            mock_config, SLUG, COURSE_ID
                        )

        assert result["ok"] is True
        # Staged file should be gone
        assert staging_manager.get_staged_file(COURSE_ID, staged_image) is None


# ============================================================
# B1: upload failure aborts push, preserves staging
# ============================================================

class TestUploadFailureAborts:

    def test_upload_failure_returns_ok_false(
        self, plugin_root, staged_page_with_token, mock_config
    ):
        """An upload failure must return ok=False without pushing to Canvas."""
        def _failing_upload(config, filepath, module_number=None, content_type=None):
            return None  # simulate upload failure

        update_called = {"count": 0}

        def _track_update(config, slug, body, from_staging=False):
            update_called["count"] += 1
            return True

        with mock.patch("canvas_api.upload_image", side_effect=_failing_upload):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", side_effect=_track_update):
                    result = push_to_canvas.push_page(
                        mock_config, SLUG, COURSE_ID
                    )

        assert result["ok"] is False
        assert update_called["count"] == 0, "update_page must not be called on upload failure"
        assert "staging_preserved" in result and result["staging_preserved"] is True

    def test_upload_failure_surfaces_failed_key(
        self, plugin_root, staged_page_with_token, mock_config
    ):
        """The failed staged-file key must appear in the error result."""
        with mock.patch("canvas_api.upload_image", return_value=None):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    result = push_to_canvas.push_page(
                        mock_config, SLUG, COURSE_ID
                    )

        assert "failed_staged_file_key" in result
        assert result["failed_staged_file_key"] == "hero-img"

    def test_staging_preserved_on_failure(
        self, plugin_root, staged_image, staged_page_with_token, mock_config
    ):
        """Staged files and the staged page must be intact after an upload failure."""
        with mock.patch("canvas_api.upload_image", return_value=None):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    push_to_canvas.push_page(mock_config, SLUG, COURSE_ID)

        # Staged page still present
        assert staging_manager.get_staged(COURSE_ID, "pages", SLUG) is not None
        # Staged file still present
        assert staging_manager.get_staged_file(COURSE_ID, staged_image) is not None


# ============================================================
# B1: multiple tokens + repeated token (one upload per unique key)
# ============================================================

class TestMultipleAndRepeatedTokens:

    @pytest.fixture()
    def two_staged_images(self, plugin_root, tmp_path, sample_png_bytes):
        """Stage two images with different keys."""
        for key in ("img-a", "img-b"):
            src = tmp_path / f"{key}.png"
            src.write_bytes(sample_png_bytes)
            r = staging_manager.stage_file(
                COURSE_ID, key, "png", src,
                meta={"mime": "image/png", "alt": f"Alt for {key}"},
            )
            assert r["ok"] is True
        return ["img-a", "img-b"]

    @pytest.fixture()
    def staged_page_multi(self, plugin_root, two_staged_images):
        """Stage a page with img-a repeated + img-b once."""
        html = (
            "<h1>Multi</h1>"
            '<img src="asucb-staged-file://img-a" alt="A">'
            '<p>See also <img src="asucb-staged-file://img-b" alt="B"></p>'
            '<img src="asucb-staged-file://img-a" alt="A again">'
        )
        r = staging_manager.stage(COURSE_ID, "pages", SLUG, html, validate=False)
        assert r["ok"] is True
        return html

    def test_one_upload_per_unique_key(
        self, plugin_root, staged_page_multi, two_staged_images, mock_config
    ):
        """Each unique key must be uploaded exactly once, even if a token repeats."""
        upload_calls = []

        def _tracking_upload(config, filepath, module_number=None, content_type=None):
            upload_calls.append(str(filepath))
            cid = config["course_id"]
            fid = len(upload_calls) * 1000
            return {"file_id": fid, "embed_url": f"/courses/{cid}/files/{fid}/preview"}

        with mock.patch("canvas_api.upload_image", side_effect=_tracking_upload):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        result = push_to_canvas.push_page(
                            mock_config, SLUG, COURSE_ID
                        )

        assert result["ok"] is True
        # Two unique keys: img-a and img-b. img-a appears twice in HTML but
        # should be uploaded only once.
        assert len(upload_calls) == 2, (
            f"Expected 2 uploads (one per unique key), got {len(upload_calls)}"
        )

    def test_all_tokens_replaced_no_remainder(
        self, plugin_root, staged_page_multi, two_staged_images, mock_config
    ):
        """All token occurrences (including repeated ones) must be replaced."""
        captured = {}

        def _capture_update(config, slug, body, from_staging=False):
            captured["body"] = body
            return True

        id_counter = [0]

        def _counting_upload(config, filepath, module_number=None, content_type=None):
            id_counter[0] += 1
            cid = config["course_id"]
            fid = id_counter[0] * 1000
            return {"file_id": fid, "embed_url": f"/courses/{cid}/files/{fid}/preview"}

        with mock.patch("canvas_api.upload_image", side_effect=_counting_upload):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", side_effect=_capture_update):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        push_to_canvas.push_page(mock_config, SLUG, COURSE_ID)

        body = captured.get("body", "")
        assert "asucb-staged-file://" not in body, (
            "Remaining unresolved tokens in pushed body"
        )
        # Both img-a (x2) and img-b (x1) occurrences replaced
        assert body.count("/files/") == 3, (
            f"Expected 3 file URL occurrences (img-a twice, img-b once), "
            f"got {body.count('/files/')}"
        )

    def test_all_staged_files_cleared_on_success(
        self, plugin_root, staged_page_multi, two_staged_images, mock_config
    ):
        """All uploaded staged files must be cleared after success."""
        id_counter = [0]

        def _counting_upload(config, filepath, module_number=None, content_type=None):
            id_counter[0] += 1
            cid = config["course_id"]
            fid = id_counter[0] * 1000
            return {"file_id": fid, "embed_url": f"/courses/{cid}/files/{fid}/preview"}

        with mock.patch("canvas_api.upload_image", side_effect=_counting_upload):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        result = push_to_canvas.push_page(
                            mock_config, SLUG, COURSE_ID
                        )

        assert result["ok"] is True
        for key in two_staged_images:
            assert staging_manager.get_staged_file(COURSE_ID, key) is None, (
                f"Staged file '{key}' should be cleared after successful push"
            )


# ============================================================
# B1: page with no tokens (no regression)
# ============================================================

class TestNoTokens:

    def test_page_without_tokens_pushes_normally(
        self, plugin_root, mock_config
    ):
        """Pages with no tokens must push normally without calling upload_image."""
        html = "<h1>Plain Page</h1><p>No images here.</p>"
        r = staging_manager.stage(COURSE_ID, "pages", SLUG, html, validate=False)
        assert r["ok"] is True

        upload_called = {"count": 0}

        def _track(config, filepath, module_number=None, content_type=None):
            upload_called["count"] += 1
            return {"file_id": 99, "embed_url": "/courses/99999/files/99/preview"}

        with mock.patch("canvas_api.upload_image", side_effect=_track):
            with mock.patch("canvas_api.get_page", return_value=None):
                with mock.patch("canvas_api.update_page", return_value=True):
                    with mock.patch(
                        "post_write_verify.verify_page",
                        return_value={"ok": True, "canvas_url": "https://...", "warnings": []},
                    ):
                        result = push_to_canvas.push_page(
                            mock_config, SLUG, COURSE_ID
                        )

        assert result["ok"] is True
        assert upload_called["count"] == 0, "upload_image should not be called for pages without tokens"
