# jdq(2025): This file was modified from cpython 3.12 test_telnetlib.py, to make it compatible
# with more versions of python, and, to use pytest instead of unittest.
# std imports
import io
import re
import socket
import selectors
import threading
import contextlib

# 3rd party
import pytest

# Skip the whole module if a working socket is not available
try:
    _s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    _s.close()
except OSError:
    pytest.skip("Working socket required", allow_module_level=True)

# local
import telnetlib3.telnetlib as telnetlib  # noqa: E402

HOST = "127.0.0.1"


def server(evt, serv):
    serv.listen()
    evt.set()
    try:
        conn, addr = serv.accept()
        conn.close()
    except TimeoutError:
        pass
    finally:
        serv.close()


@pytest.fixture
def server_port():
    """
    Start a listening socket in a background thread that accepts one connection.

    Yields the bound port number.
    """
    evt = threading.Event()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(60)  # Safety net. Look issue 11812
    # Bind to an ephemeral port on localhost
    sock.bind((HOST, 0))
    port = sock.getsockname()[1]
    thread = threading.Thread(target=server, args=(evt, sock), daemon=True)
    thread.start()
    evt.wait()
    try:
        yield port
    finally:
        thread.join()


@contextlib.contextmanager
def captured_stdout():
    """Local replacement for test.support.captured_stdout()"""
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        yield buf


class SocketStub:
    """A socket proxy that re-defines sendall()"""

    def __init__(self, reads=()):
        self.reads = list(reads)  # Intentionally make a copy.
        self.writes = []
        self.block = False

    def sendall(self, data):
        self.writes.append(data)

    def recv(self, size):
        out = b""
        while self.reads and len(out) < size:
            out += self.reads.pop(0)
        if len(out) > size:
            self.reads.insert(0, out[size:])
            out = out[:size]
        return out


class TelnetAlike(telnetlib.Telnet):
    def fileno(self):
        """Provide a real OS-level file descriptor so selectors and any code that calls fileno() can
        work, even though the network I/O is mocked."""
        s = getattr(self, "_fileno_sock", None)
        if s is None:
            try:
                s1, s2 = socket.socketpair()
                s1.setblocking(False)
                s2.setblocking(False)
                self._fileno_sock, self._fileno_peer = s1, s2
            except AttributeError:
                # Fallback if socketpair is unavailable; a plain socket still yields a valid FD
                self._fileno_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self._fileno_sock.setblocking(False)
                self._fileno_peer = None
        return self._fileno_sock.fileno()

    def close(self):
        # Close the internal fileno() provider sockets, but leave the mocked self.sock alone
        try:
            if getattr(self, "_fileno_sock", None) is not None:
                try:
                    self._fileno_sock.close()
                finally:
                    self._fileno_sock = None
            if getattr(self, "_fileno_peer", None) is not None:
                try:
                    self._fileno_peer.close()
                finally:
                    self._fileno_peer = None
        finally:
            # Do not close self.sock here; tests manage the stubbed socket lifecycle
            pass

    def sock_avail(self):
        return not self.sock.block

    def msg(self, msg, *args):
        with captured_stdout() as out:
            telnetlib.Telnet.msg(self, msg, *args)
        self._messages += out.getvalue()


class MockSelector(selectors.BaseSelector):
    def __init__(self):
        self.keys = {}

    @property
    def resolution(self):
        return 1e-3

    def register(self, fileobj, events, data=None):
        key = selectors.SelectorKey(fileobj, 0, events, data)
        self.keys[fileobj] = key
        return key

    def unregister(self, fileobj):
        return self.keys.pop(fileobj)

    def select(self, timeout=None):
        block = False
        for fileobj in self.keys:
            if isinstance(fileobj, TelnetAlike):
                block = fileobj.sock.block
                break
        if block:
            return []
        return [(key, key.events) for key in self.keys.values()]

    def get_map(self):
        return self.keys


@contextlib.contextmanager
def mocktest_socket(reads):
    def new_conn(*ignored):
        return SocketStub(reads)

    old_conn = socket.create_connection
    try:
        socket.create_connection = new_conn
        yield None
    finally:
        socket.create_connection = old_conn


def make_telnet(reads=(), cls=TelnetAlike):
    """Return a telnetlib.Telnet object that uses a SocketStub with reads queued up to be read."""
    for x in reads:
        assert isinstance(x, bytes)
    with mocktest_socket(reads):
        telnet = cls("dummy", 0)
        telnet._messages = ""  # debuglevel output
    return telnet


@pytest.fixture
def _mock_selector(monkeypatch):
    monkeypatch.setattr(telnetlib, "_TelnetSelector", MockSelector)


def test_basic(server_port):
    telnet = telnetlib.Telnet(HOST, server_port)
    telnet.sock.close()


def test_context_manager(server_port):
    with telnetlib.Telnet(HOST, server_port) as tn:
        assert tn.get_socket() is not None
    assert tn.get_socket() is None


def test_timeout_default(server_port):
    assert socket.getdefaulttimeout() is None
    socket.setdefaulttimeout(30)
    try:
        telnet = telnetlib.Telnet(HOST, server_port)
    finally:
        socket.setdefaulttimeout(None)
    assert telnet.sock.gettimeout() == 30
    telnet.sock.close()


def test_timeout_none(server_port):
    assert socket.getdefaulttimeout() is None
    socket.setdefaulttimeout(30)
    try:
        telnet = telnetlib.Telnet(HOST, server_port, timeout=None)
    finally:
        socket.setdefaulttimeout(None)
    assert telnet.sock.gettimeout() is None
    telnet.sock.close()


def test_timeout_value(server_port):
    telnet = telnetlib.Telnet(HOST, server_port, timeout=30)
    assert telnet.sock.gettimeout() == 30
    telnet.sock.close()


def test_timeout_open(server_port):
    telnet = telnetlib.Telnet()
    telnet.open(HOST, server_port, timeout=30)
    assert telnet.sock.gettimeout() == 30
    telnet.sock.close()


def test_getters(server_port):
    telnet = telnetlib.Telnet(HOST, server_port, timeout=30)
    t_sock = telnet.sock
    assert telnet.get_socket() == t_sock
    assert telnet.fileno() == t_sock.fileno()
    telnet.sock.close()


def _read_eager(func_name):
    """Read all data available already queued or on the socket, without blocking."""
    want = b"x" * 100
    telnet = make_telnet([want])
    func = getattr(telnet, func_name)
    telnet.sock.block = True
    assert b"" == func()
    telnet.sock.block = False
    data = b""
    while True:
        try:
            data += func()
        except EOFError:
            break
    assert data == want


def test_read_until(_mock_selector):
    """read_until(expected, timeout=None) test the blocking version of read_util."""
    want = [b"xxxmatchyyy"]
    telnet = make_telnet(want)
    data = telnet.read_until(b"match")
    assert data == b"xxxmatch", (telnet.cookedq, telnet.rawq, telnet.sock.reads)

    reads = [b"x" * 50, b"match", b"y" * 50]
    expect = b"".join(reads[:-1])
    telnet = make_telnet(reads)
    data = telnet.read_until(b"match")
    assert data == expect


def test_read_all(_mock_selector):
    """read_all() Read all data until EOF; may block."""
    reads = [b"x" * 500, b"y" * 500, b"z" * 500]
    expect = b"".join(reads)
    telnet = make_telnet(reads)
    data = telnet.read_all()
    assert data == expect


def test_read_some(_mock_selector):
    """read_some() Read at least one byte or EOF; may block."""
    telnet = make_telnet([b"x" * 500])
    data = telnet.read_some()
    assert len(data) >= 1
    telnet = make_telnet()
    data = telnet.read_some()
    assert b"" == data


def test_read_eager(_mock_selector):
    _read_eager("read_eager")
    _read_eager("read_very_eager")


def read_very_lazy(_mock_selector):
    want = b"x" * 100
    telnet = make_telnet([want])
    assert b"" == telnet.read_very_lazy()
    while telnet.sock.reads:
        telnet.fill_rawq()
    data = telnet.read_very_lazy()
    assert want == data
    with pytest.raises(EOFError):
        telnet.read_very_lazy()


def test_read_lazy(_mock_selector):
    want = b"x" * 100
    telnet = make_telnet([want])
    assert b"" == telnet.read_lazy()
    data = b""
    while True:
        try:
            read_data = telnet.read_lazy()
            data += read_data
            if not read_data:
                telnet.fill_rawq()
        except EOFError:
            break
        assert want.startswith(data)
    assert data == want


class nego_collector:
    def __init__(self, sb_getter=None):
        self.seen = b""
        self.sb_getter = sb_getter
        self.sb_seen = b""

    def do_nego(self, sock, cmd, opt):
        self.seen += cmd + opt
        if cmd == tl.SE and self.sb_getter:
            sb_data = self.sb_getter()
            self.sb_seen += sb_data


tl = telnetlib


@pytest.mark.parametrize(
    "data",
    [
        b"data sample without IAC",
        b"data sample with" + tl.IAC + b" one IAC",
        b"a few" + tl.IAC + tl.IAC + b" iacs" + tl.IAC,
        tl.IAC,
        b"",
    ],
)
def test_write_doubles_iac(data):
    telnet = make_telnet()
    telnet.write(data)
    written = b"".join(telnet.sock.writes)
    assert data.replace(tl.IAC, tl.IAC + tl.IAC) == written


_OPTION_CMDS = [tl.AO, tl.AYT, tl.BRK, tl.EC, tl.EL, tl.GA, tl.IP, tl.NOP]


def _test_option_command(data):
    """Helper for testing IAC + cmd."""
    telnet = make_telnet(data)
    data_len = len(b"".join(data))
    nego = nego_collector()
    telnet.set_option_negotiation_callback(nego.do_nego)
    txt = telnet.read_all()
    cmd = nego.seen
    assert len(cmd) > 0  # we expect at least one command
    assert cmd[:1] in _OPTION_CMDS
    assert cmd[1:2] == tl.NOOPT
    assert data_len == len(txt + cmd)
    nego.sb_getter = None  # break the nego => telnet cycle


def test_IAC_commands():
    for cmd in _OPTION_CMDS:
        _test_option_command([tl.IAC, cmd])
        _test_option_command([b"x" * 100, tl.IAC, cmd, b"y" * 100])
        _test_option_command([b"x" * 10, tl.IAC, cmd, b"y" * 10])
    _test_option_command([tl.IAC + cmd for cmd in _OPTION_CMDS])


def test_SB_commands():
    send = [
        tl.IAC + tl.SB + tl.IAC + tl.SE,
        tl.IAC + tl.SB + tl.IAC + tl.IAC + tl.IAC + tl.SE,
        tl.IAC + tl.SB + tl.IAC + tl.IAC + b"aa" + tl.IAC + tl.SE,
        tl.IAC + tl.SB + b"bb" + tl.IAC + tl.IAC + tl.IAC + tl.SE,
        tl.IAC + tl.SB + b"cc" + tl.IAC + tl.IAC + b"dd" + tl.IAC + tl.SE,
    ]
    telnet = make_telnet(send)
    nego = nego_collector(telnet.read_sb_data)
    telnet.set_option_negotiation_callback(nego.do_nego)
    txt = telnet.read_all()
    assert txt == b""
    want_sb_data = tl.IAC + tl.IAC + b"aabb" + tl.IAC + b"cc" + tl.IAC + b"dd"
    assert nego.sb_seen == want_sb_data
    assert b"" == telnet.read_sb_data()
    nego.sb_getter = None  # break the nego => telnet cycle


def test_debuglevel_reads():
    given_a_expect_b = [
        (b"a", ": recv b''\n"),
        (tl.IAC + bytes([88]), ": IAC 88 not recognized\n"),
        (tl.IAC + tl.DO + bytes([1]), ": IAC DO 1\n"),
        (tl.IAC + tl.DONT + bytes([1]), ": IAC DONT 1\n"),
        (tl.IAC + tl.WILL + bytes([1]), ": IAC WILL 1\n"),
        (tl.IAC + tl.WONT + bytes([1]), ": IAC WONT 1\n"),
    ]
    for a, b in given_a_expect_b:
        telnet = make_telnet([a])
        telnet.set_debuglevel(1)
        _ = telnet.read_all()
        assert b in telnet._messages


def test_debuglevel_write():
    telnet = make_telnet()
    telnet.set_debuglevel(1)
    telnet.write(b"xxx")
    expected = "send b'xxx'\n"
    assert expected in telnet._messages


def test_debug_accepts_str_port():
    with mocktest_socket([]):
        telnet = TelnetAlike("dummy", "0")
        telnet._messages = ""
    telnet.set_debuglevel(1)
    telnet.msg("test")
    assert re.search(r"0.*test", telnet._messages)


def test_expect(_mock_selector):
    """Expect(expected, [timeout]) Read until the expected string has been seen, or a timeout is hit
    (default is no timeout); may block."""
    want = [b"x" * 10, b"match", b"y" * 10]
    telnet = make_telnet(want)
    _, _, data = telnet.expect([b"match"])
    assert data == b"".join(want[:-1])
