"""Tests for QB1: narrowed except clause in audit_image_alt._get closure.

Verifies that:
  1. requests.RequestException is still caught (returns [] — skip-on-error preserved).
  2. ValueError is still caught (off-domain next-link guard preserved).
  3. Programming errors (NameError, AttributeError) now propagate instead of
     being swallowed — the old `except (ValueError, Exception)` masked these.
"""

import os
import sys
import types
import unittest
from unittest.mock import MagicMock, patch

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

import requests as _real_requests
import audit_image_alt as ait


def _make_fake_canvas_api(paginated_get_side_effect):
    """Return a minimal fake canvas_api module stub."""
    fake = types.SimpleNamespace()
    fake.get_config = MagicMock(return_value={
        "domain": "canvas.test.edu",
        "headers": {"Authorization": "Bearer fake"},
        "course_id": "99999",
        "base_url": "https://canvas.test.edu/api/v1",
        "course_url": "https://canvas.test.edu/api/v1/courses/99999",
    })
    fake.paginated_get = MagicMock(side_effect=paginated_get_side_effect)
    return fake


def _run_build_manifest_pages_call(paginated_get_side_effect):
    """Patch _canvas_api in the module, call build_manifest, and return pages_meta.

    Stops execution right after the first _get("pages") by having the second
    _get("quizzes") side_effect raise StopIteration, which we catch. Returns
    the result of the pages call (either [] or raises).
    """
    captured = {}

    # paginated_get is called once per _get() invocation; first call = pages
    original_side_effect = paginated_get_side_effect

    call_count = [0]

    def side_effect_wrapper(*args, **kwargs):
        call_count[0] += 1
        if call_count[0] == 1:
            return original_side_effect(*args, **kwargs)
        # Subsequent calls (quizzes, assignments, etc.) raise StopIteration
        # to abort the manifest build early
        raise StopIteration("stop after first call")

    fake_api = _make_fake_canvas_api(side_effect_wrapper)

    with patch.object(ait, "_canvas_api", fake_api):
        with patch.object(ait, "_requests") as mock_req:
            # Make any _requests.get calls in the loop return empty-ish responses
            mock_req.get.return_value = MagicMock(status_code=404)
            mock_req.RequestException = _real_requests.RequestException

            try:
                ait.build_manifest("99999")
            except StopIteration:
                pass  # expected — we aborted after pages
            except Exception as exc:
                # Re-raise programming errors so tests can assert on them
                captured["raised"] = exc

    return captured


class TestGetClosureExceptNarrowed(unittest.TestCase):

    def test_request_exception_returns_empty_list(self):
        """requests.RequestException in paginated_get should be caught → _get returns []."""
        import requests as _real_requests

        fake_api = _make_fake_canvas_api(
            lambda *a, **kw: (_ for _ in ()).throw(
                _real_requests.ConnectionError("timeout")
            )
        )

        with patch.object(ait, "_canvas_api", fake_api):
            with patch.object(ait, "_requests") as mock_req:
                mock_req.RequestException = _real_requests.RequestException
                mock_req.get.return_value = MagicMock(status_code=404)

                call_results = []

                original_paginated_get = fake_api.paginated_get

                call_count = [0]

                def counting_side_effect(*args, **kwargs):
                    call_count[0] += 1
                    if call_count[0] == 1:
                        raise _real_requests.ConnectionError("timeout")
                    raise StopIteration("stop")

                fake_api.paginated_get = MagicMock(side_effect=counting_side_effect)

                try:
                    ait.build_manifest("99999")
                except StopIteration:
                    pass  # stopped after pages

                # pages call raised ConnectionError → _get should return []
                # build_manifest will try to iterate pages_meta → no error expected
                # If we reach StopIteration on call 2, pages returned [] successfully
                self.assertEqual(call_count[0], 2,
                    "paginated_get should have been called twice (pages + quizzes "
                    "before StopIteration) — confirming pages _get returned []")

    def test_value_error_returns_empty_list(self):
        """ValueError (off-domain link) in paginated_get should be caught → _get returns []."""
        import requests as _real_requests

        fake_api = _make_fake_canvas_api(None)
        call_count = [0]

        def side_effect(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                raise ValueError("off-domain next link")
            raise StopIteration("stop")

        fake_api.paginated_get = MagicMock(side_effect=side_effect)

        with patch.object(ait, "_canvas_api", fake_api):
            with patch.object(ait, "_requests") as mock_req:
                mock_req.RequestException = _real_requests.RequestException
                mock_req.get.return_value = MagicMock(status_code=404)

                try:
                    ait.build_manifest("99999")
                except StopIteration:
                    pass

                self.assertEqual(call_count[0], 2,
                    "ValueError should be caught → pages _get returned [], "
                    "then quizzes _get was called (confirming pages did not raise)")

    def test_name_error_propagates(self):
        """NameError (programming error) must propagate — old bare except would swallow it."""
        import requests as _real_requests

        fake_api = _make_fake_canvas_api(None)

        def side_effect(*args, **kwargs):
            raise NameError("undefined_variable")

        fake_api.paginated_get = MagicMock(side_effect=side_effect)

        with patch.object(ait, "_canvas_api", fake_api):
            with patch.object(ait, "_requests") as mock_req:
                mock_req.RequestException = _real_requests.RequestException
                mock_req.get.return_value = MagicMock(status_code=404)

                with self.assertRaises(NameError):
                    ait.build_manifest("99999")

    def test_attribute_error_propagates(self):
        """AttributeError (programming error) must propagate."""
        import requests as _real_requests

        fake_api = _make_fake_canvas_api(None)

        def side_effect(*args, **kwargs):
            raise AttributeError("'NoneType' object has no attribute 'json'")

        fake_api.paginated_get = MagicMock(side_effect=side_effect)

        with patch.object(ait, "_canvas_api", fake_api):
            with patch.object(ait, "_requests") as mock_req:
                mock_req.RequestException = _real_requests.RequestException
                mock_req.get.return_value = MagicMock(status_code=404)

                with self.assertRaises(AttributeError):
                    ait.build_manifest("99999")


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