import asyncio
import os
import tempfile
import unittest
from unittest.mock import MagicMock

from sglang.srt.entrypoints.ssl_utils import SSLCertRefresher
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase

register_cpu_ci(est_time=9, suite="stage-a-test-cpu")


def _make_temp_pem(content: bytes) -> str:
    """Create a temporary PEM file and return its path."""
    f = tempfile.NamedTemporaryFile(suffix=".pem", delete=False)
    f.write(content)
    f.flush()
    f.close()
    return f.name


class TestSSLCertRefresher(CustomTestCase):
    """Tests for the SSLCertRefresher class."""

    def setUp(self):
        super().setUp()
        self._temp_files: list[str] = []

    def tearDown(self):
        for path in self._temp_files:
            try:
                os.unlink(path)
            except OSError:
                pass
        super().tearDown()

    def _track(self, path: str) -> str:
        """Register a temp file for cleanup."""
        self._temp_files.append(path)
        return path

    def _run_async(self, coro):
        """Helper to run an async coroutine in tests."""
        loop = asyncio.new_event_loop()
        try:
            return loop.run_until_complete(coro)
        finally:
            loop.close()

    def test_reload_cert_key_on_file_change(self):
        """SSLCertRefresher calls load_cert_chain when cert/key files change."""
        mock_ctx = MagicMock()
        cert_path = self._track(_make_temp_pem(b"CERT_V1"))
        key_path = self._track(_make_temp_pem(b"KEY_V1"))

        async def _test():
            refresher = SSLCertRefresher(mock_ctx, key_path, cert_path)
            await asyncio.sleep(0.3)

            with open(cert_path, "w") as f:
                f.write("CERT_V2")

            await asyncio.sleep(1.5)
            refresher.stop()
            return mock_ctx

        result_ctx = self._run_async(_test())
        result_ctx.load_cert_chain.assert_called_with(cert_path, key_path)

    def test_reload_ca_on_file_change(self):
        """SSLCertRefresher calls load_verify_locations when CA file changes."""
        mock_ctx = MagicMock()
        cert_path = self._track(_make_temp_pem(b"CERT"))
        key_path = self._track(_make_temp_pem(b"KEY"))
        ca_path = self._track(_make_temp_pem(b"CA_V1"))

        async def _test():
            refresher = SSLCertRefresher(mock_ctx, key_path, cert_path, ca_path)
            await asyncio.sleep(0.3)

            with open(ca_path, "w") as f:
                f.write("CA_V2")

            await asyncio.sleep(1.5)
            refresher.stop()
            return mock_ctx

        result_ctx = self._run_async(_test())
        result_ctx.load_verify_locations.assert_called_with(ca_path)

    def test_stop_cancels_tasks(self):
        """Calling stop() prevents further reloads."""
        mock_ctx = MagicMock()
        cert_path = self._track(_make_temp_pem(b"CERT"))
        key_path = self._track(_make_temp_pem(b"KEY"))

        async def _test():
            refresher = SSLCertRefresher(mock_ctx, key_path, cert_path)
            await asyncio.sleep(0.2)

            refresher.stop()

            with open(cert_path, "w") as f:
                f.write("CERT_AFTER_STOP")

            await asyncio.sleep(1.0)
            return mock_ctx

        result_ctx = self._run_async(_test())
        result_ctx.load_cert_chain.assert_not_called()

    def test_no_ca_watcher_when_ca_not_provided(self):
        """No CA watcher task is created when ca_path is None."""
        mock_ctx = MagicMock()
        cert_path = self._track(_make_temp_pem(b"CERT"))
        key_path = self._track(_make_temp_pem(b"KEY"))

        async def _test():
            refresher = SSLCertRefresher(mock_ctx, key_path, cert_path)
            self.assertEqual(len(refresher._tasks), 1)
            refresher.stop()

        self._run_async(_test())

    def test_ca_watcher_created_when_ca_provided(self):
        """A CA watcher task is created when ca_path is provided."""
        mock_ctx = MagicMock()
        cert_path = self._track(_make_temp_pem(b"CERT"))
        key_path = self._track(_make_temp_pem(b"KEY"))
        ca_path = self._track(_make_temp_pem(b"CA"))

        async def _test():
            refresher = SSLCertRefresher(mock_ctx, key_path, cert_path, ca_path)
            self.assertEqual(len(refresher._tasks), 2)
            refresher.stop()

        self._run_async(_test())

    def test_reload_error_does_not_crash(self):
        """A reload error is logged but doesn't crash the watcher."""
        mock_ctx = MagicMock()
        mock_ctx.load_cert_chain.side_effect = Exception("bad cert")
        cert_path = self._track(_make_temp_pem(b"CERT"))
        key_path = self._track(_make_temp_pem(b"KEY"))

        async def _test():
            refresher = SSLCertRefresher(mock_ctx, key_path, cert_path)
            await asyncio.sleep(0.3)

            with open(cert_path, "w") as f:
                f.write("BAD_CERT")

            await asyncio.sleep(1.5)

            for task in refresher._tasks:
                self.assertFalse(task.done())

            refresher.stop()

        self._run_async(_test())


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