# std imports
import ssl as ssl_module
import socket
import asyncio
import logging
from unittest.mock import MagicMock, patch

# 3rd party
import pytest

# local
from telnetlib3.server import StatusLogger, TelnetServer, parse_server_args, _TLSAutoDetectProtocol
from telnetlib3.server_base import BaseServer


@pytest.mark.asyncio
async def test_connection_lost_closes_transport_despite_set_protocol_error():
    server = BaseServer.__new__(BaseServer)
    server.log = __import__("logging").getLogger("test_server")
    server._tasks = []
    server._waiter_connected = asyncio.get_running_loop().create_future()
    server._extra = {}
    server.shell = None

    closed = []

    class BadTransport:
        def set_protocol(self, proto):
            raise RuntimeError("set_protocol failed")

        def close(self):
            closed.append(True)

        def get_extra_info(self, name, default=None):
            return default

    class FakeReader:
        def feed_eof(self):
            pass

    server._transport = BadTransport()
    server.reader = FakeReader()
    server.connection_lost(None)
    assert len(closed) == 1
    assert server._transport is None


@pytest.mark.asyncio
async def test_connection_lost_remove_done_callback_raises():
    server = BaseServer.__new__(BaseServer)
    server.log = __import__("logging").getLogger("test_server")
    server._tasks = []
    server._extra = {}
    server.shell = None
    server._closing = False

    waiter = asyncio.get_running_loop().create_future()

    class _BadWaiter:
        done = waiter.done
        cancelled = waiter.cancelled
        cancel = waiter.cancel

        def remove_done_callback(self, cb):
            raise ValueError("already removed")

    server._waiter_connected = _BadWaiter()

    class FakeReader:
        def feed_eof(self):
            pass

    class FakeTransport:
        def close(self):
            pass

        def get_extra_info(self, name, default=None):
            return default

    server._transport = FakeTransport()
    server.reader = FakeReader()
    server.connection_lost(None)
    assert server._transport is None


@pytest.mark.asyncio
async def test_data_received_trace_log():
    from telnetlib3.accessories import TRACE

    server = BaseServer(encoding=False)

    class FakeTransport:
        def get_extra_info(self, name, default=None):
            return ("127.0.0.1", 9999) if name == "peername" else default

        def write(self, data):
            pass

        def is_closing(self):
            return False

        def close(self):
            pass

    server.connection_made(FakeTransport())
    from telnetlib3 import server_base

    old_level = server_base.logger.level
    server_base.logger.setLevel(TRACE)
    try:
        server.data_received(b"hello")
    finally:
        server_base.logger.setLevel(old_level)


@pytest.mark.asyncio
async def test_data_received_fast_path_no_iac():
    server = BaseServer(encoding=False)

    class FakeTransport:
        def get_extra_info(self, name, default=None):
            return ("127.0.0.1", 9999) if name == "peername" else default

        def write(self, data):
            pass

        def is_closing(self):
            return False

        def close(self):
            pass

    server.connection_made(FakeTransport())
    server.writer.slc_simulated = False
    server.data_received(b"hello world")
    assert len(server.reader._buffer) >= len(b"hello world")


def _make_telnet_server(**kwargs):
    """Create a TelnetServer with a FakeTransport for unit testing."""
    defaults = {"encoding": False, "connect_maxwait": 0.01}
    defaults.update(kwargs)
    server = TelnetServer(**defaults)

    class FakeTransport:
        def get_extra_info(self, name, default=None):
            return ("127.0.0.1", 9999) if name == "peername" else default

        def write(self, data):
            pass

        def is_closing(self):
            return False

        def close(self):
            pass

    server.connection_made(FakeTransport())
    return server


@pytest.mark.asyncio
async def test_check_negotiation_deferred_echo_environ():
    """check_negotiation triggers deferred ECHO/NEW_ENVIRON when TTYPE refused."""
    from telnetlib3.telopt import TTYPE

    server = _make_telnet_server()
    server._advanced = True
    server._echo_negotiated = False
    server._environ_requested = False
    server.writer.remote_option[TTYPE] = False

    echo_calls = []
    environ_calls = []
    server._negotiate_echo = lambda: echo_calls.append(True)
    server._negotiate_environ = lambda: environ_calls.append(True)

    server.check_negotiation()
    assert len(echo_calls) == 1
    assert len(environ_calls) == 1


@pytest.mark.asyncio
async def test_check_negotiation_final_subneg_timeout_warning(caplog):
    """check_negotiation warns when critical subneg times out."""
    from telnetlib3.telopt import SB, NEW_ENVIRON

    server = _make_telnet_server()
    server._advanced = True
    server._echo_negotiated = True
    server._environ_requested = True
    server.writer.pending_option[SB + NEW_ENVIRON] = True

    with caplog.at_level(logging.WARNING, logger="telnetlib3.server"):
        server.check_negotiation(final=True)

    assert "critical subnegotiation" in caplog.text.lower() or "environ" in caplog.text.lower()


@pytest.mark.asyncio
async def test_check_encoding_binary_incoming_request():
    """_check_encoding sends DO BINARY when outbinary set but not inbinary."""
    from telnetlib3.telopt import DO, BINARY

    server = _make_telnet_server()
    server.writer.local_option[BINARY] = True
    server.writer.remote_option[BINARY] = False

    iac_calls = []
    orig_iac = server.writer.iac

    def track_iac(*args):
        iac_calls.append(args)
        return orig_iac(*args)

    server.writer.iac = track_iac
    result = server._check_encoding()
    assert result is False
    assert any(args == (DO, BINARY) for args in iac_calls)


@pytest.mark.asyncio
async def test_tls_autodetect_empty_peek():
    """TLS auto-detect closes transport on empty peek."""
    proto = _TLSAutoDetectProtocol(ssl_module.SSLContext(ssl_module.PROTOCOL_TLS_SERVER), MagicMock)
    transport = MagicMock()
    mock_sock = MagicMock()
    transport.get_extra_info = MagicMock(
        side_effect=lambda name, **kw: mock_sock if name == "socket" else None
    )
    proto._transport = transport
    proto._detect_timer = MagicMock()

    dup_sock = MagicMock()
    dup_sock.recv.return_value = b""
    with patch("socket.fromfd", return_value=dup_sock):
        proto._try_peek()

    transport.close.assert_called_once()


@pytest.mark.asyncio
async def test_tls_upgrade_handshake_failure():
    """_upgrade_to_tls handles SSLError gracefully."""
    proto = _TLSAutoDetectProtocol(ssl_module.SSLContext(ssl_module.PROTOCOL_TLS_SERVER), MagicMock)
    transport = MagicMock()
    transport.is_closing.return_value = False
    proto._transport = transport

    loop = asyncio.get_running_loop()
    with patch.object(loop, "start_tls", side_effect=ssl_module.SSLError("handshake failed")):
        await proto._upgrade_to_tls()

    transport.close.assert_called_once()


@pytest.mark.asyncio
async def test_tls_clienthello_rejected_without_ssl(caplog):
    """TLS ClientHello on a plain server logs a warning and closes."""
    server = TelnetServer(encoding=False, connect_maxwait=0.01)
    closed = []

    class FakeTransport:
        def get_extra_info(self, name, default=None):
            if name == "peername":
                return ("10.0.0.1", 4444)
            return default

        def write(self, data):
            pass

        def is_closing(self):
            return False

        def close(self):
            closed.append(True)

    server.connection_made(FakeTransport())

    # TLS ClientHello record: content_type=0x16, version=TLS 1.0
    clienthello = b"\x16\x03\x01\x00\x05hello"
    with caplog.at_level(logging.WARNING, logger="telnetlib3.server"):
        server.data_received(clienthello)

    assert len(closed) == 1
    assert "TLS ClientHello" in caplog.text
    assert "10.0.0.1" in caplog.text


@pytest.mark.asyncio
async def test_tls_clienthello_check_only_first_data():
    """The TLS check only fires on the first data_received call."""
    server = TelnetServer(encoding=False, connect_maxwait=0.01)

    class FakeTransport:
        def get_extra_info(self, name, default=None):
            if name == "peername":
                return ("127.0.0.1", 9999)
            return default

        def write(self, data):
            pass

        def is_closing(self):
            return False

        def close(self):
            pass

    server.connection_made(FakeTransport())

    # First call with normal telnet data
    server.data_received(b"\xff\xfb\x18")
    # Second call starting with 0x16 should NOT trigger TLS rejection
    server.data_received(b"\x16some data")
    assert server._tls_checked is True


@pytest.mark.asyncio
async def test_status_logger_run_loop():
    """StatusLogger._run() logs when status changes."""
    mock_server = MagicMock()
    mock_server.sockets = []
    logger_obj = StatusLogger(mock_server, interval=0.01)

    call_count = [0]

    def fake_status():
        call_count[0] += 1
        return {"count": call_count[0], "clients": []}

    logger_obj._get_status = fake_status
    logger_obj.start()
    await asyncio.sleep(0.1)
    logger_obj.stop()
    assert call_count[0] >= 2


def test_parse_server_args_force_binary_auto():
    """parse_server_args auto-enables force_binary for non-ASCII encoding."""
    with patch("sys.argv", ["test", "--encoding", "cp437"]):
        result = parse_server_args()
    assert result["force_binary"] is True


def test_parse_server_args_ascii_no_force_binary():
    """parse_server_args does not auto-enable force_binary for ASCII."""
    with patch("sys.argv", ["test", "--encoding", "us-ascii"]):
        result = parse_server_args()
    assert result["force_binary"] is False


@pytest.mark.parametrize(
    "arg,expected_encoding,expected_force_binary",
    [
        pytest.param("false", False, True, id="false_lowercase"),
        pytest.param("False", False, True, id="False_capitalized"),
        pytest.param("FALSE", False, True, id="FALSE_uppercase"),
    ],
)
def test_parse_server_args_encoding_false(arg, expected_encoding, expected_force_binary):
    """parse_server_args converts --encoding=false/False to boolean False."""
    with patch("sys.argv", ["test", "--encoding", arg]):
        result = parse_server_args()
    assert result["encoding"] is expected_encoding
    assert result["force_binary"] is expected_force_binary


@pytest.mark.asyncio
async def test_run_server_guarded_shell_wrapping():
    """run_server wraps shell with robot_check and pty_fork_limit guards."""
    from telnetlib3.server import run_server, create_server

    created_server = MagicMock()
    created_server.wait_closed = MagicMock(side_effect=asyncio.CancelledError)

    async def mock_create_server(*args, **kwargs):
        created_server.shell = kwargs.get("shell")
        return created_server

    loop = asyncio.get_running_loop()
    with patch("telnetlib3.server.create_server", side_effect=mock_create_server):
        with patch.object(loop, "add_signal_handler"):
            with patch.object(loop, "remove_signal_handler"):
                try:
                    await run_server(
                        host="127.0.0.1",
                        port=0,
                        shell=lambda r, w: None,
                        robot_check=True,
                        pty_fork_limit=2,
                    )
                except (asyncio.CancelledError, OSError):
                    pass

    assert created_server.shell is not None


@pytest.mark.asyncio
async def test_run_server_status_logger_lifecycle():
    """run_server starts and stops StatusLogger when status_interval > 0."""
    from telnetlib3.server import run_server

    created_server = MagicMock()
    loop = asyncio.get_running_loop()
    wait_future = loop.create_future()
    wait_future.set_result(None)
    created_server.wait_closed = MagicMock(return_value=wait_future)
    created_server.sockets = []

    async def mock_create_server(*args, **kwargs):
        return created_server

    with patch("telnetlib3.server.create_server", side_effect=mock_create_server):
        with patch.object(loop, "add_signal_handler"):
            with patch.object(loop, "remove_signal_handler"):
                await run_server(
                    host="127.0.0.1", port=0, shell=lambda r, w: None, status_interval=1
                )


@pytest.mark.asyncio
async def test_check_negotiation_ttype_resolved_no_pending():
    """check_negotiation triggers environ when TTYPE resolved with no pending."""
    from telnetlib3.telopt import TTYPE

    server = _make_telnet_server()
    server._advanced = True
    server._echo_negotiated = True
    server._environ_requested = False
    server.writer.remote_option[TTYPE] = True

    environ_calls = []
    server._negotiate_environ = lambda: environ_calls.append(True)

    server.check_negotiation()
    assert len(environ_calls) == 1


@pytest.mark.asyncio
async def test_check_encoding_charset_request():
    """_check_encoding sends CHARSET REQUEST when both sides support it."""
    from telnetlib3.telopt import CHARSET

    server = _make_telnet_server()
    server.writer.remote_option[CHARSET] = True
    server.writer.local_option[CHARSET] = True

    charset_calls = []
    server.writer.request_charset = lambda: charset_calls.append(True)
    server._check_encoding()
    assert len(charset_calls) == 1


@pytest.mark.asyncio
async def test_data_received_no_iac_batch():
    """data_received fast path batches data without IAC bytes."""
    server = BaseServer(encoding=False)

    class FakeTransport:
        def get_extra_info(self, name, default=None):
            return ("127.0.0.1", 9999) if name == "peername" else default

        def write(self, data):
            pass

        def is_closing(self):
            return False

        def close(self):
            pass

    server.connection_made(FakeTransport())
    server.writer.slc_simulated = False
    data = b"plain text no iac"
    server.data_received(data)
    assert bytes(server.reader._buffer).endswith(data)


@pytest.mark.parametrize(
    "ssl_obj,expect_log",
    [
        pytest.param(None, False, id="no_tls"),
        pytest.param(
            type(
                "SSL",
                (),
                {"version": lambda self: "TLSv1.3", "cipher": lambda self: ("AES", "TLSv1.3", 256)},
            )(),
            True,
            id="tls_with_cipher",
        ),
        pytest.param(
            type("SSL", (), {"version": lambda self: None, "cipher": lambda self: None})(),
            True,
            id="tls_no_cipher",
        ),
    ],
)
def test_log_tls_info(ssl_obj, expect_log, caplog):
    from telnetlib3._base import TelnetProtocolBase

    proto = TelnetProtocolBase.__new__(TelnetProtocolBase)
    proto._extra = {}
    proto._transport = MagicMock()
    proto._transport.get_extra_info = lambda name, default=None: (
        ssl_obj if name == "ssl_object" else default
    )
    log = logging.getLogger("test_tls_info")
    with caplog.at_level(logging.DEBUG, logger="test_tls_info"):
        proto._log_tls_info(log)
    tls_msgs = [r for r in caplog.records if "TLS handshake" in r.message]
    assert bool(tls_msgs) == expect_log
    if ssl_obj and ssl_obj.cipher():
        assert "AES" in tls_msgs[0].message
