#!/usr/bin/env python3
"""
Unit tests for http_utils.fetch's retry/backoff behavior. All network calls
are mocked -- these never touch the real network.

Run: python -m unittest discover -s .claude/skills/literature-search-verify/scripts
"""
import os
import sys
import unittest
import urllib.error
import urllib.request
from email.message import Message
from unittest import mock

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import http_utils


def _http_error(code, retry_after=None):
    hdrs = Message()
    if retry_after is not None:
        hdrs["Retry-After"] = str(retry_after)
    return urllib.error.HTTPError("http://example.com", code, "err", hdrs, None)


class FakeResponse:
    def __init__(self, data):
        self._data = data

    def read(self):
        return self._data

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        return False


class TestFetch(unittest.TestCase):
    @mock.patch("urllib.request.urlopen")
    @mock.patch("http_utils.time.sleep", return_value=None)
    def test_succeeds_first_try_without_sleeping(self, mock_sleep, mock_urlopen):
        mock_urlopen.return_value = FakeResponse(b"ok")
        result = http_utils.fetch("http://example.com")
        self.assertEqual(result, b"ok")
        mock_sleep.assert_not_called()

    @mock.patch("urllib.request.urlopen")
    @mock.patch("http_utils.time.sleep", return_value=None)
    def test_retries_on_429_then_succeeds(self, mock_sleep, mock_urlopen):
        mock_urlopen.side_effect = [_http_error(429), FakeResponse(b"ok")]
        result = http_utils.fetch("http://example.com", max_retries=3)
        self.assertEqual(result, b"ok")
        self.assertEqual(mock_urlopen.call_count, 2)
        mock_sleep.assert_called_once()

    @mock.patch("urllib.request.urlopen")
    @mock.patch("http_utils.time.sleep", return_value=None)
    def test_retries_on_5xx(self, mock_sleep, mock_urlopen):
        mock_urlopen.side_effect = [_http_error(503), FakeResponse(b"ok")]
        result = http_utils.fetch("http://example.com", max_retries=3)
        self.assertEqual(result, b"ok")

    @mock.patch("urllib.request.urlopen")
    @mock.patch("http_utils.time.sleep", return_value=None)
    def test_raises_after_exhausting_retries(self, mock_sleep, mock_urlopen):
        mock_urlopen.side_effect = [_http_error(429)] * 10
        with self.assertRaises(urllib.error.HTTPError):
            http_utils.fetch("http://example.com", max_retries=2)
        self.assertEqual(mock_urlopen.call_count, 3)  # initial attempt + 2 retries

    @mock.patch("urllib.request.urlopen")
    @mock.patch("http_utils.time.sleep", return_value=None)
    def test_non_retryable_status_raises_immediately(self, mock_sleep, mock_urlopen):
        mock_urlopen.side_effect = _http_error(404)
        with self.assertRaises(urllib.error.HTTPError):
            http_utils.fetch("http://example.com")
        self.assertEqual(mock_urlopen.call_count, 1)
        mock_sleep.assert_not_called()


class TestWaitSeconds(unittest.TestCase):
    def test_honors_retry_after_header(self):
        err = _http_error(429, retry_after=5)
        self.assertEqual(http_utils._wait_seconds(err, attempt=0), 5.0)

    def test_falls_back_to_exponential_backoff_without_header(self):
        err = _http_error(429)
        self.assertAlmostEqual(http_utils._wait_seconds(err, attempt=2, base=1.0), 4.0)

    def test_caps_wait_time(self):
        err = _http_error(429, retry_after=999)
        self.assertEqual(http_utils._wait_seconds(err, attempt=0, cap=20.0), 20.0)

    def test_ignores_unparseable_retry_after(self):
        err = _http_error(429, retry_after="not-a-number")
        self.assertAlmostEqual(http_utils._wait_seconds(err, attempt=0, base=1.0), 1.0)


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