"""Tests for canvas_api safety guards:
  1. create_rubric bookmark=True posts a Course bookmark association.
  2. update_quiz_question warns when question_text is given without answers.
"""

import logging
import os
import sys
import unittest
from unittest.mock import MagicMock, call, patch

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

import canvas_api


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


def _make_ok_response(status=201, body=None):
    resp = MagicMock()
    resp.status_code = status
    resp.json.return_value = body or {}
    return resp


# ---------------------------------------------------------------------------
# create_rubric
# ---------------------------------------------------------------------------

class TestCreateRubricBookmark(unittest.TestCase):

    CRITERIA = [
        {"description": "Analysis", "points": 10,
         "ratings": [{"description": "Full", "points": 10},
                     {"description": "Partial", "points": 5}]}
    ]

    @patch("canvas_api._request_with_retry")
    def test_bookmark_true_posts_course_association(self, mock_req):
        """bookmark=True should trigger a rubric_associations POST with Course type."""
        rubric_create_resp = _make_ok_response(
            201, {"rubric": {"id": 42, "title": "Test Rubric"}}
        )
        assoc_resp = _make_ok_response(201, {"id": 7})
        mock_req.side_effect = [rubric_create_resp, assoc_resp]

        result = canvas_api.create_rubric(
            _mock_config(), "Test Rubric", self.CRITERIA, bookmark=True
        )

        self.assertIsNotNone(result)
        self.assertEqual(mock_req.call_count, 2, "Expected rubric POST + association POST")

        # Inspect the second call — the bookmark association
        assoc_call = mock_req.call_args_list[1]
        payload = assoc_call.kwargs["json"]["rubric_association"]
        self.assertEqual(payload["association_type"], "Course")
        self.assertEqual(payload["association_id"], "99999")
        self.assertEqual(payload["purpose"], "bookmark")
        self.assertFalse(payload["use_for_grading"])
        self.assertEqual(payload["rubric_id"], 42)

    @patch("canvas_api._request_with_retry")
    def test_bookmark_false_no_association(self, mock_req):
        """bookmark=False (default) must not post a rubric_associations call."""
        rubric_create_resp = _make_ok_response(
            201, {"rubric": {"id": 55, "title": "No Bookmark"}}
        )
        mock_req.return_value = rubric_create_resp

        canvas_api.create_rubric(
            _mock_config(), "No Bookmark", self.CRITERIA, bookmark=False
        )

        self.assertEqual(mock_req.call_count, 1, "Should only POST the rubric itself")

    @patch("canvas_api._request_with_retry")
    def test_bookmark_true_with_assignment_id_skips_course_association(self, mock_req):
        """When assignment_id is given, grading association is in the creation payload;
        the bookmark branch must be skipped even when bookmark=True."""
        rubric_create_resp = _make_ok_response(
            201, {"rubric": {"id": 77, "title": "Graded Rubric"}}
        )
        mock_req.return_value = rubric_create_resp

        canvas_api.create_rubric(
            _mock_config(), "Graded Rubric", self.CRITERIA,
            assignment_id=123, bookmark=True
        )

        self.assertEqual(mock_req.call_count, 1,
                         "assignment_id present: only one request expected")

    @patch("canvas_api._request_with_retry")
    def test_default_signature_unchanged(self, mock_req):
        """Calling create_rubric without bookmark= must behave identically to before."""
        rubric_create_resp = _make_ok_response(
            201, {"rubric": {"id": 99, "title": "Default"}}
        )
        mock_req.return_value = rubric_create_resp

        result = canvas_api.create_rubric(
            _mock_config(), "Default", self.CRITERIA
        )

        self.assertIsNotNone(result)
        self.assertEqual(mock_req.call_count, 1)


# ---------------------------------------------------------------------------
# update_quiz_question
# ---------------------------------------------------------------------------

class TestUpdateQuizQuestionWipeGuard(unittest.TestCase):

    @patch("canvas_api._request_with_retry")
    def test_warns_when_question_text_without_answers(self, mock_req):
        """Partial update (question_text only) must emit a logging.WARNING."""
        resp = _make_ok_response(200, {"id": 1, "question_text": "Updated?"})
        mock_req.return_value = resp

        with self.assertLogs("canvas_api", level="WARNING") as log_ctx:
            canvas_api.update_quiz_question(
                _mock_config(), quiz_id=10, question_id=1,
                question_text="Updated?"
            )

        self.assertTrue(
            any("answers" in msg and "wiped" in msg.lower()
                for msg in log_ctx.output),
            f"Expected a wipe-risk warning in logs; got: {log_ctx.output}"
        )

    @patch("canvas_api._request_with_retry")
    def test_no_warn_when_both_question_text_and_answers_provided(self, mock_req):
        """Full update (question_text + answers) must NOT emit the warning."""
        resp = _make_ok_response(200, {"id": 1})
        mock_req.return_value = resp

        # assertLogs would fail if no WARNING is emitted — use assertNoLogs
        # (Python 3.10+) with a fallback for older versions.
        try:
            with self.assertNoLogs("canvas_api", level="WARNING"):
                canvas_api.update_quiz_question(
                    _mock_config(), quiz_id=10, question_id=1,
                    question_text="Full update",
                    answers=[{"text": "42", "weight": 100}]
                )
        except AttributeError:
            # Python < 3.10: assertNoLogs not available — use caplog via manual check
            import io
            handler = logging.StreamHandler(io.StringIO())
            handler.setLevel(logging.WARNING)
            logging.getLogger("canvas_api").addHandler(handler)
            try:
                canvas_api.update_quiz_question(
                    _mock_config(), quiz_id=10, question_id=1,
                    question_text="Full update",
                    answers=[{"text": "42", "weight": 100}]
                )
                output = handler.stream.getvalue()
                self.assertNotIn("wiped", output.lower())
            finally:
                logging.getLogger("canvas_api").removeHandler(handler)

    @patch("canvas_api._request_with_retry")
    def test_no_warn_when_neither_field_provided(self, mock_req):
        """Update with unrelated fields (e.g. only points_possible) must not warn."""
        resp = _make_ok_response(200, {"id": 1})
        mock_req.return_value = resp

        try:
            with self.assertNoLogs("canvas_api", level="WARNING"):
                canvas_api.update_quiz_question(
                    _mock_config(), quiz_id=10, question_id=1,
                    points_possible=5
                )
        except AttributeError:
            pass  # Older Python — absence of warning is fine, skip assertion


if __name__ == "__main__":
    unittest.main()
