"""
Tests for http_probe session cleanup in util.py.

Verifies that non-winner HttpClient sessions are always closed, even when
http_probe is cancelled.  The cancellation re-injection behaviour tested here
is Python 3.14-specific; on 3.12 a single task.cancel() is consumed at the
first await and does not re-inject into the finally block, so the old
'await asyncio.gather(...)' approach would also pass on 3.12.
"""
import asyncio
import unittest

from preseem.util import http_probe


class FakeClient:
    """Minimal HttpClient stub for testing http_probe cleanup."""

    def __init__(self, url, response=None, **kwargs):
        self.url = url
        self._response = response  # True = probe succeeds, else fails
        self.closed = False

    async def request(self, path, handler=None, **kwargs):
        if self._response is True and handler:
            return await handler(FakeResponse(200))
        return None

    async def close(self):
        self.closed = True


class SlowFakeClient(FakeClient):
    """Client whose close() suspends (simulates real HttpClient.close behaviour)."""

    async def close(self):
        await asyncio.sleep(0)  # suspension point — tests GC-safety of strong refs
        self.closed = True


class FakeResponse:

    def __init__(self, status):
        self.status = status
        self.request_info = FakeRequestInfo()
        self.headers = {}


class FakeRequestInfo:

    class url:
        scheme = 'https'


class TestHttpProbeCleanup(unittest.TestCase):

    def setUp(self):
        self.loop = asyncio.new_event_loop()

    def tearDown(self):
        # Run a few iterations so fire-and-forget close tasks complete.
        self.loop.run_until_complete(asyncio.sleep(0))
        self.loop.close()

    def _run(self, coro):
        return self.loop.run_until_complete(coro)

    def _make_factory(self, clients, response=None, slow_close=False):
        """Return a factory that records created clients."""
        cls = SlowFakeClient if slow_close else FakeClient

        def factory(url, **kwargs):
            c = cls(url, response=response)
            clients.append(c)
            return c

        return factory

    # ------------------------------------------------------------------
    # Normal (non-cancelled) path
    # ------------------------------------------------------------------

    def test_non_winners_closed_when_one_succeeds(self):
        """When one probe wins, all other clients must be closed."""
        clients = []

        def factory(url, **kwargs):
            # https succeeds, http fails
            response = True if 'https' in url else None
            c = FakeClient(url, response=response)
            clients.append(c)
            return c

        result = self._run(http_probe('host', factory, methods=('https', 'http')))

        self.assertIsNotNone(result)
        self._run(asyncio.sleep(0))  # let close tasks finish

        for c in clients:
            if c is not result:
                self.assertTrue(c.closed, f"Non-winner {c.url} was not closed")

    def test_all_closed_when_none_succeed(self):
        """When all probes fail, every client must be closed."""
        clients = []

        result = self._run(
            http_probe('host', self._make_factory(clients, response=None),
                       methods=('https', 'http')))

        self.assertIsNone(result)
        self._run(asyncio.sleep(0))

        for c in clients:
            self.assertTrue(c.closed, f"Client {c.url} was not closed after failed probe")

    def test_slow_close_clients_still_closed(self):
        """Clients whose close() suspends (real HttpClient) must still be closed."""
        clients = []

        result = self._run(
            http_probe('host', self._make_factory(clients, response=None, slow_close=True),
                       methods=('https', 'http')))

        self.assertIsNone(result)
        self._run(asyncio.sleep(0))  # let suspended close tasks resume

        for c in clients:
            self.assertTrue(c.closed, f"Slow-close client {c.url} was not closed")

    # ------------------------------------------------------------------
    # Cancelled path
    # ------------------------------------------------------------------

    def test_all_closed_when_cancelled(self):
        """When http_probe is cancelled, all clients must still be closed."""
        clients = []

        async def slow_request(path, handler=None, **kwargs):
            await asyncio.sleep(10)  # will be cancelled

        def factory(url, **kwargs):
            c = FakeClient(url)
            c.request = slow_request
            clients.append(c)
            return c

        async def run():
            task = self.loop.create_task(http_probe('host', factory,
                                                    methods=('https', 'http')))
            await asyncio.sleep(0)  # let task start
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            await asyncio.sleep(0)  # let close tasks run

        self._run(run())

        for c in clients:
            self.assertTrue(c.closed, f"Client {c.url} was not closed after cancellation")
