"""Tests that staging is cleared after successful mockup creates in push_to_canvas.py.

Guardrail 3: staging auto-clear on mockup create.
Verifies that push_new_mockup_quiz and push_new_mockup_assignment call
staging_manager.clear_staged on success, so staging artifacts do not linger.
"""

import os
import sys
import pathlib
from unittest import mock

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

import push_to_canvas  # noqa: E402
import staging_manager  # noqa: E402


_MOCK_QUIZ_PAYLOAD = {
    "title": "Mock Quiz Title",
    "quiz_type": "assignment",
    "time_limit": 10,
    "questions": [
        {
            "question_name": "Q1",
            "question_text": "What is law?",
            "question_type": "multiple_choice_question",
            "points_possible": 1,
            "answers": [
                {"text": "A", "weight": 100},
                {"text": "B", "weight": 0},
            ],
        }
    ],
}

_MOCK_ASSIGNMENT_PAYLOAD = {
    "name": "Mock Assignment",
    "description": "<p>Do the thing.</p>",
    "points_possible": 100,
    "submission_types": ["online_text_entry"],
}


class TestMockupQuizStagingCleared:
    """push_new_mockup_quiz must clear staging on verified success."""

    def _run(self, verify_ok=True):
        clear_calls = []
        quiz_id = 555
        mock_config = {"course_id": "123"}

        with mock.patch("push_to_canvas.canvas_api.get_config",
                        return_value=mock_config), \
             mock.patch("push_to_canvas.canvas_api.require_course_id"), \
             mock.patch("push_to_canvas.staging_manager.get_staged",
                        return_value=_MOCK_QUIZ_PAYLOAD), \
             mock.patch("push_to_canvas.canvas_api.create_quiz",
                        return_value={"id": quiz_id, "title": "Mock Quiz Title"}), \
             mock.patch("push_to_canvas.canvas_api.add_quiz_question",
                        return_value={"id": 1}), \
             mock.patch("push_to_canvas.canvas_api.add_module_item",
                        return_value={"id": 1}), \
             mock.patch("push_to_canvas.canvas_api.get_quiz",
                        return_value=({"id": quiz_id} if verify_ok else None)), \
             mock.patch("push_to_canvas.staging_manager.clear_staged",
                        side_effect=lambda *a, **kw: (
                            clear_calls.append(a) or {"ok": True}
                        )):
            result = push_to_canvas.push_new_mockup_quiz(
                "123", "quizzes/mock-q1", "mod-module-1",
            )

        return result, clear_calls

    def test_staging_cleared_on_success(self):
        result, clear_calls = self._run(verify_ok=True)
        assert result.get("ok"), f"Expected ok result, got: {result}"
        assert clear_calls, "staging_manager.clear_staged must be called on success"
        # The staged_key passed to clear_staged should contain the key we provided.
        object_ids = [c[2] for c in clear_calls if len(c) >= 3]
        assert any("mock-q1" in str(oid) for oid in object_ids), (
            f"Expected staged key 'mock-q1' in clear_staged calls; got: {object_ids}"
        )

    def test_staging_not_cleared_when_verify_fails(self):
        result, clear_calls = self._run(verify_ok=False)
        # When get_quiz returns None, verify_ok is False so ok=False.
        assert not result.get("ok"), "Expected failed result when verify fails"
        # clear_staged should NOT be called when verification fails.
        assert not clear_calls, (
            "staging_manager.clear_staged must NOT be called when verify fails"
        )


class TestMockupQuizPartialQuestionFailure:
    """push_new_mockup_quiz must not clear staging when some questions fail.

    create_quiz succeeding (the quiz shell exists) is not sufficient to
    verify a mockup quiz push. If any staged question fails to create,
    Canvas ends up holding an incomplete quiz — verification must fail
    and staging must be preserved so the push can be retried. Regression
    test for issue #214.
    """

    _TWO_QUESTION_PAYLOAD = {
        "title": "Mock Quiz Title",
        "quiz_type": "assignment",
        "time_limit": 10,
        "questions": [
            {
                "question_name": "Q1",
                "question_text": "What is law?",
                "question_type": "multiple_choice_question",
                "points_possible": 1,
                "answers": [
                    {"text": "A", "weight": 100},
                    {"text": "B", "weight": 0},
                ],
            },
            {
                "question_name": "Q2",
                "question_text": "What is equity?",
                "question_type": "multiple_choice_question",
                "points_possible": 1,
                "answers": [
                    {"text": "C", "weight": 100},
                    {"text": "D", "weight": 0},
                ],
            },
        ],
    }

    def _run(self, question_side_effects):
        clear_calls = []
        quiz_id = 555
        mock_config = {"course_id": "123"}

        with mock.patch("push_to_canvas.canvas_api.get_config",
                        return_value=mock_config), \
             mock.patch("push_to_canvas.canvas_api.require_course_id"), \
             mock.patch("push_to_canvas.staging_manager.get_staged",
                        return_value=self._TWO_QUESTION_PAYLOAD), \
             mock.patch("push_to_canvas.canvas_api.create_quiz",
                        return_value={"id": quiz_id, "title": "Mock Quiz Title"}), \
             mock.patch("push_to_canvas.canvas_api.add_quiz_question",
                        side_effect=question_side_effects), \
             mock.patch("push_to_canvas.canvas_api.add_module_item",
                        return_value={"id": 1}), \
             mock.patch("push_to_canvas.canvas_api.get_quiz",
                        return_value={"id": quiz_id}), \
             mock.patch("push_to_canvas.staging_manager.clear_staged",
                        side_effect=lambda *a, **kw: (
                            clear_calls.append(a) or {"ok": True}
                        )):
            result = push_to_canvas.push_new_mockup_quiz(
                "123", "quizzes/mock-q1", "mod-module-1",
            )

        return result, clear_calls

    def test_partial_question_failure_blocks_verify_and_preserves_staging(self):
        # add_quiz_question returns None on failure (canvas_api contract):
        # one question succeeds, one fails.
        result, clear_calls = self._run([{"id": 1}, None])

        assert not result.get("ok"), (
            f"Expected failed result when a question fails to create, got: {result}"
        )
        assert result.get("questions_failed") == 1, (
            f"Expected questions_failed=1 to surface the partial failure, got: {result}"
        )
        assert not clear_calls, (
            "staging_manager.clear_staged must NOT be called when a "
            "question fails to create"
        )

    def test_all_questions_succeed_clears_staging(self):
        result, clear_calls = self._run([{"id": 1}, {"id": 2}])

        assert result.get("ok"), f"Expected ok result, got: {result}"
        assert result.get("questions_failed") == 0, (
            f"Expected questions_failed=0 when all questions succeed, got: {result}"
        )
        assert clear_calls, (
            "staging_manager.clear_staged must be called when all "
            "questions succeed"
        )


class TestMockupAssignmentStagingCleared:
    """push_new_mockup_assignment must clear staging on verified success.

    Note: push_new_mockup_assignment verifies by checking the create_assignment
    response (not a separate get_assignment call). To test the failure path,
    we mock create_assignment to return a response without a matching 'id'.
    """

    def _run(self, verify_ok=True):
        clear_calls = []
        assignment_id = 777
        mock_config = {"course_id": "123"}

        # For the success path, create_assignment returns a proper response.
        # For the failure path, create_assignment returns an empty/mismatched dict.
        if verify_ok:
            create_return = {"id": assignment_id, "name": "Mock Assignment"}
        else:
            # Simulate create_assignment returning None (API failure).
            create_return = None

        with mock.patch("push_to_canvas.canvas_api.get_config",
                        return_value=mock_config), \
             mock.patch("push_to_canvas.canvas_api.require_course_id"), \
             mock.patch("push_to_canvas.staging_manager.get_staged",
                        return_value=_MOCK_ASSIGNMENT_PAYLOAD), \
             mock.patch("push_to_canvas.canvas_api.create_assignment",
                        return_value=create_return), \
             mock.patch("push_to_canvas.canvas_api.add_module_item",
                        return_value={"id": 1}), \
             mock.patch("push_to_canvas.staging_manager.clear_staged",
                        side_effect=lambda *a, **kw: (
                            clear_calls.append(a) or {"ok": True}
                        )):
            result = push_to_canvas.push_new_mockup_assignment(
                "123", "assignments/mock-hw1", "mod-module-1",
            )

        return result, clear_calls

    def test_staging_cleared_on_success(self):
        result, clear_calls = self._run(verify_ok=True)
        assert result.get("ok"), f"Expected ok result, got: {result}"
        assert clear_calls, "staging_manager.clear_staged must be called on success"
        object_ids = [c[2] for c in clear_calls if len(c) >= 3]
        assert any("mock-hw1" in str(oid) for oid in object_ids), (
            f"Expected staged key 'mock-hw1' in clear_staged calls; got: {object_ids}"
        )

    def test_staging_not_cleared_when_create_fails(self):
        result, clear_calls = self._run(verify_ok=False)
        # create_assignment returned None, so the function returns early with error.
        assert not result.get("ok"), "Expected failed result when create_assignment fails"
        assert not clear_calls, (
            "staging_manager.clear_staged must NOT be called when create fails"
        )
