import json
import signal
import unittest
from urllib.error import HTTPError
from unittest.mock import Mock, patch

from fgai.graylog_mcp import GraylogMcpClient, _hard_timeout


class _Response:
    def __init__(self, body: dict[str, object], session: str = "session-1") -> None:
        self.body = body
        self.headers = {"Mcp-Session-Id": session}

    def read(self) -> bytes:
        return json.dumps(self.body).encode("utf-8")

    def __enter__(self):
        return self

    def __exit__(self, *_args):
        return False


class GraylogMcpTests(unittest.TestCase):
    def test_accepts_raw_or_copied_basic_credentials(self):
        raw = GraylogMcpClient("http://graylog/api/mcp", "raw-token")._authorization_header()
        encoded = raw.removeprefix("Basic ")
        self.assertEqual(GraylogMcpClient("http://graylog/api/mcp", encoded)._authorization_header(), raw)
        self.assertEqual(GraylogMcpClient("http://graylog/api/mcp", raw)._authorization_header(), raw)

    def test_probe_initializes_and_lists_tools(self):
        responses = iter([
            _Response({"result": {"serverInfo": {"version": "7.1.6"}}}),
            _Response({}),
            _Response({"result": {"tools": [{"name": "search_messages"}, {"name": "aggregate_messages"}]}}),
        ])
        with patch("fgai.graylog_mcp.GraylogMcpClient._open", side_effect=responses):
            status = GraylogMcpClient("http://graylog/api/mcp", "raw-token").probe()
        self.assertEqual(status["status"], "connected")
        self.assertIn("search_messages", status["tools"])

    def test_probe_keeps_existing_session_unless_forced(self):
        responses = iter([
            _Response({"result": {"serverInfo": {"version": "7.1.6"}}}, session="session-1"),
            _Response({}, session="session-1"),
            _Response({"result": {"tools": []}}, session="session-1"),
        ])
        client = GraylogMcpClient("http://graylog/api/mcp", "raw-token")
        client.session_id = "existing-session"

        with patch("fgai.graylog_mcp.GraylogMcpClient._open", side_effect=responses):
            client.probe()

        self.assertEqual(client.session_id, "session-1")

    def test_call_tool_reinitializes_after_stale_session_error(self):
        responses = iter([
            HTTPError("http://graylog/api/mcp", 404, "stale session", {}, None),
            _Response({"result": {"serverInfo": {"version": "7.1.6"}}}, session="session-2"),
            _Response({}, session="session-2"),
            _Response({"result": {"tools": [{"name": "search_messages"}]}}, session="session-2"),
            _Response({"result": {"content": []}}, session="session-2"),
        ])
        client = GraylogMcpClient("http://graylog/api/mcp", "raw-token")
        client.session_id = "old-session"

        with patch("fgai.graylog_mcp.GraylogMcpClient._open", side_effect=responses):
            result = client.call_tool("search_messages", {"query": "*"})

        self.assertEqual(result["result"]["content"], [])
        self.assertEqual(client.session_id, "session-2")

    def test_default_open_bypasses_environment_proxies(self):
        client = GraylogMcpClient("http://graylog/api/mcp", "raw-token")
        req = object()
        opener = Mock()
        opener.open.return_value = "response"

        with patch("fgai.graylog_mcp.request.build_opener", return_value=opener) as build_opener:
            result = client._open(req, None)

        self.assertEqual(result, "response")
        self.assertEqual(opener.open.call_args.kwargs["timeout"], 15)
        self.assertEqual(build_opener.call_args.args[0].proxies, {})

    def test_hard_timeout_preserves_outer_timer_remaining_time(self):
        previous_handler = signal.getsignal(signal.SIGALRM)
        previous_timer = signal.getitimer(signal.ITIMER_REAL)
        try:
            signal.setitimer(signal.ITIMER_REAL, 20)
            with patch("fgai.graylog_mcp.time.monotonic", side_effect=[100.0, 103.0]):
                with _hard_timeout(5):
                    pass
            remaining = signal.getitimer(signal.ITIMER_REAL)[0]
            self.assertGreater(remaining, 16)
            self.assertLessEqual(remaining, 17.5)
        finally:
            signal.setitimer(signal.ITIMER_REAL, previous_timer[0], previous_timer[1])
            signal.signal(signal.SIGALRM, previous_handler)


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