"""
Test for BrokenPipeError + SSL BAD_LENGTH cascading failure fix.

When a client disconnects mid-response:
1. First write raises BrokenPipeError (or ssl.SSLError on TLS)
2. Exception handler tries to send 500 JSON through same broken socket
3. Without the fix, this produces a second exception and noisy traceback

The fix:
- helpers._safe_write() catches _CLIENT_DISCONNECT_ERRORS (including ssl.SSLError)
- server.py exception handlers wrap their 500-response j() calls in try/except
"""
import io
import ssl
import unittest
from unittest.mock import MagicMock

import sys
import os

# Ensure project root is on path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from api.helpers import j, t, _safe_write, _CLIENT_DISCONNECT_ERRORS


class MockHandler:
    """Minimal mock of BaseHTTPRequestHandler for testing response helpers."""

    def __init__(self, write_raises=None, end_headers_raises=None):
        self._write_raises = write_raises
        self._end_headers_raises = end_headers_raises
        self.headers = {}
        self._wfile = MagicMock()
        self._sent_headers = []
        self._response_status = None

    @property
    def wfile(self):
        return self._wfile

    def send_response(self, status):
        self._response_status = status

    def send_header(self, key, value):
        self._sent_headers.append((key, value))

    def end_headers(self):
        if self._end_headers_raises:
            raise self._end_headers_raises

    def _wfile_write(self, data):
        if self._write_raises:
            raise self._write_raises
        return len(data)


class TestSafeWrite(unittest.TestCase):
    """Test _safe_write swallows client disconnect errors without raising."""

    def _make_handler(self, end_headers_raises=None, write_raises=None):
        handler = MockHandler(
            end_headers_raises=end_headers_raises,
            write_raises=write_raises,
        )
        handler.wfile.write = handler._wfile_write
        return handler

    def test_safe_write_success(self):
        handler = self._make_handler()
        _safe_write(handler, b"hello")
        self.assertEqual(handler._response_status, None)  # send_response not called by _safe_write

    def test_safe_write_broken_pipe(self):
        handler = self._make_handler(write_raises=BrokenPipeError())
        # Should NOT raise
        _safe_write(handler, b"hello")

    def test_safe_write_connection_reset(self):
        handler = self._make_handler(write_raises=ConnectionResetError())
        _safe_write(handler, b"hello")

    def test_safe_write_connection_aborted(self):
        handler = self._make_handler(write_raises=ConnectionAbortedError())
        _safe_write(handler, b"hello")

    def test_safe_write_timeout(self):
        handler = self._make_handler(write_raises=TimeoutError())
        _safe_write(handler, b"hello")

    def test_safe_write_ssl_bad_length(self):
        handler = self._make_handler(
            write_raises=ssl.SSLError("[BAD_LENGTH] write failed")
        )
        _safe_write(handler, b"hello")

    def test_safe_write_end_headers_broken_pipe(self):
        handler = self._make_handler(end_headers_raises=BrokenPipeError())
        _safe_write(handler, b"hello")

    def test_safe_write_end_headers_ssl_error(self):
        handler = self._make_handler(
            end_headers_raises=ssl.SSLError("[BAD_LENGTH]")
        )
        _safe_write(handler, b"hello")

    def test_safe_write_other_error_propagates(self):
        handler = self._make_handler(write_raises=ValueError("unexpected"))
        with self.assertRaises(ValueError):
            _safe_write(handler, b"hello")


class TestJsonHelper(unittest.TestCase):
    """Test j() helper with disconnect errors."""

    def _make_handler(self, end_headers_raises=None, write_raises=None):
        handler = MockHandler(
            end_headers_raises=end_headers_raises,
            write_raises=write_raises,
        )
        handler.wfile.write = handler._wfile_write
        return handler

    def test_j_success(self):
        handler = self._make_handler()
        j(handler, {"ok": True}, status=200)
        self.assertEqual(handler._response_status, 200)
        self.assertTrue(any(h[0] == "Content-Type" for h in handler._sent_headers))

    def test_j_broken_pipe_on_write(self):
        handler = self._make_handler(write_raises=BrokenPipeError())
        # Should NOT raise — headers sent, write fails silently
        j(handler, {"ok": True}, status=200)
        self.assertEqual(handler._response_status, 200)

    def test_j_ssl_error_on_write(self):
        handler = self._make_handler(
            write_raises=ssl.SSLError("[BAD_LENGTH] write failed")
        )
        j(handler, {"ok": True}, status=200)
        self.assertEqual(handler._response_status, 200)

    def test_j_broken_pipe_on_end_headers(self):
        handler = self._make_handler(end_headers_raises=BrokenPipeError())
        j(handler, {"ok": True}, status=200)
        self.assertEqual(handler._response_status, 200)


class TestTextHelper(unittest.TestCase):
    """Test t() helper with disconnect errors."""

    def _make_handler(self, end_headers_raises=None, write_raises=None):
        handler = MockHandler(
            end_headers_raises=end_headers_raises,
            write_raises=write_raises,
        )
        handler.wfile.write = handler._wfile_write
        return handler

    def test_t_success(self):
        handler = self._make_handler()
        t(handler, "hello", status=200)
        self.assertEqual(handler._response_status, 200)

    def test_t_broken_pipe(self):
        handler = self._make_handler(write_raises=BrokenPipeError())
        t(handler, "hello", status=200)
        self.assertEqual(handler._response_status, 200)

    def test_t_ssl_error(self):
        handler = self._make_handler(
            write_raises=ssl.SSLError("[BAD_LENGTH]")
        )
        t(handler, "hello", status=200)
        self.assertEqual(handler._response_status, 200)


class TestClientDisconnectErrorsTuple(unittest.TestCase):
    """Verify _CLIENT_DISCONNECT_ERRORS includes all expected types."""

    def test_includes_broken_pipe(self):
        self.assertIn(BrokenPipeError, _CLIENT_DISCONNECT_ERRORS)

    def test_includes_connection_reset(self):
        self.assertIn(ConnectionResetError, _CLIENT_DISCONNECT_ERRORS)

    def test_includes_connection_aborted(self):
        self.assertIn(ConnectionAbortedError, _CLIENT_DISCONNECT_ERRORS)

    def test_includes_timeout(self):
        self.assertIn(TimeoutError, _CLIENT_DISCONNECT_ERRORS)

    def test_includes_ssl_error(self):
        self.assertIn(ssl.SSLError, _CLIENT_DISCONNECT_ERRORS)

    def test_excludes_broad_oserror(self):
        """OSError is too broad — it masks real errors like file-not-found."""
        self.assertNotIn(OSError, _CLIENT_DISCONNECT_ERRORS)


class TestServerDisconnectHandling(unittest.TestCase):
    """Test server.py skips 500 response when client disconnects."""

    def _make_handler(self, route_raises=None):
        """Build a Handler with mocked socket and route."""
        from server import Handler
        handler = Handler.__new__(Handler)
        handler.command = "GET"
        handler.path = "/api/test"
        handler._req_t0 = 0.0
        handler.headers = {}
        handler.wfile = MagicMock()
        handler.wfile.write = MagicMock()
        handler.send_response = MagicMock()
        handler.send_header = MagicMock()
        handler.end_headers = MagicMock()
        handler._route_raises = route_raises
        return handler

    def test_do_get_skips_500_on_broken_pipe(self):
        from server import Handler
        handler = self._make_handler(route_raises=BrokenPipeError())

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        # Patch handle_get to raise BrokenPipeError
        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler.do_GET(handler)
        finally:
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        # send_response should NEVER be called for the 500 — client is gone
        handler.send_response.assert_not_called()

    def test_handle_write_skips_500_on_connection_reset(self):
        from server import Handler
        handler = self._make_handler(route_raises=ConnectionResetError())
        handler.command = "POST"

        def _fake_route(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler._handle_write(handler, _fake_route)
        finally:
            _server_mod.check_auth_or_close = orig_check_auth

        handler.send_response.assert_not_called()

    def test_log_request_ignores_closed_stdout(self):
        """A poisoned stdout stream must not break send_response()."""
        from server import Handler
        handler = Handler.__new__(Handler)
        handler.command = "GET"
        handler.path = "/health"
        handler._req_t0 = 0.0
        handler.headers = {}
        handler.client_address = ("127.0.0.1", 12345)

        closed_stdout = io.StringIO()
        closed_stdout.close()
        original_stdout = sys.stdout
        sys.stdout = closed_stdout
        try:
            Handler.log_request(handler, 200)
        finally:
            sys.stdout = original_stdout

    def test_do_get_sends_500_when_stdout_closed(self):
        """Route errors should still produce a 500 if stdout was closed."""
        from server import Handler
        handler = self._make_handler(route_raises=ValueError("real bug"))

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True

        closed_stdout = io.StringIO()
        closed_stdout.close()
        original_stdout = sys.stdout
        sys.stdout = closed_stdout
        try:
            Handler.do_GET(handler)
        finally:
            sys.stdout = original_stdout
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        handler.send_response.assert_called_once_with(500)

    def test_do_get_sends_500_on_real_error(self):
        from server import Handler
        handler = self._make_handler(route_raises=ValueError("real bug"))

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler.do_GET(handler)
        finally:
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        # Should send 500 for real errors
        handler.send_response.assert_called_once_with(500)

    def test_do_get_skips_500_on_ssl_disconnect(self):
        """SSL disconnect during route handling should not trigger 500."""
        from server import Handler
        handler = self._make_handler(route_raises=ssl.SSLError("[BAD_LENGTH]"))

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler.do_GET(handler)
        finally:
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        handler.send_response.assert_not_called()

    def test_do_get_skips_500_on_timeout_disconnect(self):
        """Timeout disconnect during route handling should not trigger 500."""
        from server import Handler
        handler = self._make_handler(route_raises=TimeoutError())

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler.do_GET(handler)
        finally:
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        handler.send_response.assert_not_called()


class TestServer500ResponseSafety(unittest.TestCase):
    """Test that 500-response failures are handled gracefully."""

    def _make_handler(self, route_raises=None, json_write_raises=None):
        """Build a Handler where the 500-response j() call may also fail."""
        from server import Handler
        handler = Handler.__new__(Handler)
        handler.command = "GET"
        handler.path = "/api/test"
        handler._req_t0 = 0.0
        handler.headers = {}
        handler.wfile = MagicMock()
        handler._json_write_raises = json_write_raises

        def _raising_write(data):
            if handler._json_write_raises:
                raise handler._json_write_raises
            return len(data)

        handler.wfile.write = _raising_write
        handler.send_response = MagicMock()
        handler.send_header = MagicMock()
        handler.end_headers = MagicMock()
        handler._route_raises = route_raises
        return handler

    def test_500_response_survives_client_disconnect(self):
        """If the 500-response itself hits a disconnect, don't crash."""
        from server import Handler
        handler = self._make_handler(
            route_raises=ValueError("real bug"),
            json_write_raises=BrokenPipeError(),
        )

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler.do_GET(handler)
        finally:
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        # send_response WAS called (we tried to send 500), but write failed
        handler.send_response.assert_called_once_with(500)

    def test_500_response_logs_unexpected_failure(self):
        """If the 500-response fails for a NON-disconnect reason, log it."""
        from server import Handler
        handler = self._make_handler(
            route_raises=ValueError("real bug"),
            json_write_raises=RuntimeError("json serializer exploded"),
        )

        def _fake_handle_get(self, parsed):
            raise self._route_raises

        import server as _server_mod
        orig_handle_get = _server_mod.handle_get
        orig_check_auth = _server_mod.check_auth_or_close
        _server_mod.handle_get = _fake_handle_get
        _server_mod.check_auth_or_close = lambda h, p: True
        try:
            Handler.do_GET(handler)
        finally:
            _server_mod.handle_get = orig_handle_get
            _server_mod.check_auth_or_close = orig_check_auth

        # send_response WAS called (we tried to send 500), but write failed
        handler.send_response.assert_called_once_with(500)


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