"""Test LINEMODE, rfc-1184_."""

# std imports
import sys
import asyncio
import collections

# 3rd party
import pytest

# local
import telnetlib3
import telnetlib3.stream_writer
from telnetlib3 import slc
from telnetlib3.slc import LMODE_SLC, LMODE_MODE, LMODE_MODE_ACK, LMODE_MODE_LOCAL
from telnetlib3.telopt import DO, SB, SE, IAC, WILL, LINEMODE
from telnetlib3.stream_writer import TelnetWriter
from telnetlib3.tests.accessories import (
    MockProtocol,
    MockTransport,
    create_server,
    asyncio_connection,
)


def _make_server_writer():
    t = MockTransport()
    p = MockProtocol()
    w = TelnetWriter(t, p, server=True)
    return w, t


def _make_client_writer():
    t = MockTransport()
    p = MockProtocol()
    w = TelnetWriter(t, p, client=True)
    return w, t


async def test_server_demands_remote_linemode_client_agrees(bind_host, unused_tcp_port):
    class ServerTestLinemode(telnetlib3.BaseServer):
        def begin_negotiation(self):
            super().begin_negotiation()
            self.writer.iac(DO, LINEMODE)

    async with create_server(
        protocol_factory=ServerTestLinemode, host=bind_host, port=unused_tcp_port
    ) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (client_reader, client_writer):
            expect_mode = telnetlib3.stream_writer.TelnetWriter.default_linemode.mask
            expect_stage1 = IAC + DO + LINEMODE
            expect_stage2 = IAC + SB + LINEMODE + LMODE_MODE + expect_mode + IAC + SE

            reply_mode = bytes([ord(expect_mode) | ord(LMODE_MODE_ACK)])
            reply_stage1 = IAC + WILL + LINEMODE
            reply_stage2 = IAC + SB + LINEMODE + LMODE_MODE + reply_mode + IAC + SE

            result = await client_reader.readexactly(len(expect_stage1))
            assert result == expect_stage1
            client_writer.write(reply_stage1)

            result = await client_reader.readexactly(len(expect_stage2))
            assert result == expect_stage2
            client_writer.write(reply_stage2)

            srv_instance = await asyncio.wait_for(server.wait_for_client(), 1.0)
            await asyncio.wait_for(
                srv_instance.writer.wait_for(
                    remote={"LINEMODE": True}, pending={"LINEMODE": False}
                ),
                1.0,
            )

            # server sends SLC table after MODE ACK; close server side to get EOF
            asyncio.get_running_loop().call_soon(srv_instance.connection_lost, None)
            result = await client_reader.read()
            assert result.startswith(IAC + SB + LINEMODE + LMODE_SLC)

            assert srv_instance.writer.mode == "remote"
            assert srv_instance.writer.linemode.remote is True
            assert srv_instance.writer.linemode.local is False
            assert srv_instance.writer.linemode.trapsig is False
            assert srv_instance.writer.linemode.ack is True
            assert srv_instance.writer.linemode.soft_tab is False
            assert srv_instance.writer.linemode.lit_echo is True


async def test_server_demands_remote_linemode_client_demands_local(bind_host, unused_tcp_port):
    class ServerTestLinemode(telnetlib3.BaseServer):
        def begin_negotiation(self):
            super().begin_negotiation()
            self.writer.iac(DO, LINEMODE)

    async with create_server(
        protocol_factory=ServerTestLinemode, host=bind_host, port=unused_tcp_port
    ) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (client_reader, client_writer):
            expect_mode = telnetlib3.stream_writer.TelnetWriter.default_linemode.mask
            expect_stage1 = IAC + DO + LINEMODE
            expect_stage2 = IAC + SB + LINEMODE + LMODE_MODE + expect_mode + IAC + SE

            # No, we demand local mode -- using ACK will finalize such request
            reply_mode = bytes([ord(LMODE_MODE_LOCAL) | ord(LMODE_MODE_ACK)])
            reply_stage1 = IAC + WILL + LINEMODE
            reply_stage2 = IAC + SB + LINEMODE + LMODE_MODE + reply_mode + IAC + SE

            result = await client_reader.readexactly(len(expect_stage1))
            assert result == expect_stage1
            client_writer.write(reply_stage1)

            result = await client_reader.readexactly(len(expect_stage2))
            assert result == expect_stage2
            client_writer.write(reply_stage2)

            srv_instance = await asyncio.wait_for(server.wait_for_client(), 1.0)
            await asyncio.wait_for(
                srv_instance.writer.wait_for(
                    remote={"LINEMODE": True}, pending={"LINEMODE": False}
                ),
                1.0,
            )

            # server sends SLC table after MODE ACK; close server side to get EOF
            asyncio.get_running_loop().call_soon(srv_instance.connection_lost, None)
            result = await client_reader.read()
            assert result.startswith(IAC + SB + LINEMODE + LMODE_SLC)

            assert srv_instance.writer.mode == "local"
            assert srv_instance.writer.linemode.remote is False
            assert srv_instance.writer.linemode.local is True
            assert srv_instance.writer.linemode.trapsig is False
            assert srv_instance.writer.linemode.ack is True
            assert srv_instance.writer.linemode.soft_tab is False
            assert srv_instance.writer.linemode.lit_echo is False


def test_slc_validation_rejects_misaligned():
    """_handle_sb_linemode_slc raises ValueError for non-multiple-of-3 buffer."""
    w, _ = _make_server_writer()
    buf = collections.deque([b"\x09", b"\x02", b"\x03", b"\x04"])
    with pytest.raises(ValueError, match="multiple of 3"):
        w._handle_sb_linemode_slc(buf)


def test_slc_change_default_uses_default_table():
    """_slc_change with SLC_DEFAULT restores value from default table, not incoming."""
    w, _ = _make_server_writer()
    default_val = w.default_slc_tab[slc.SLC_EC].val
    slc_def = slc.SLC(slc.SLC_DEFAULT, b"\x03")
    w._slc_change(slc.SLC_EC, slc_def)
    assert w.slctab[slc.SLC_EC].val == default_val
    assert w.slctab[slc.SLC_EC].val != b"\x03"


def test_forwardmask_stored():
    """_handle_do_forwardmask stores a Forwardmask for valid lengths."""
    w, _ = _make_client_writer()
    assert w.forwardmask is None
    buf = collections.deque([bytes([b]) for b in b"\x00" * 16])
    w._handle_do_forwardmask(buf)
    assert w.forwardmask is not None
    assert isinstance(w.forwardmask, slc.Forwardmask)
    assert len(w.forwardmask.value) == 16


@pytest.mark.parametrize("length", [0, 33])
def test_forwardmask_invalid_length(length):
    """_handle_do_forwardmask logs warning and stores nothing for invalid lengths."""
    w, _ = _make_client_writer()
    buf = collections.deque([bytes([0]) for _ in range(length)])
    w._handle_do_forwardmask(buf)
    assert w.forwardmask is None


def test_client_sends_slc_request_on_will_linemode():
    """Client emits SLC (0, SLC_DEFAULT, 0) triplet immediately after WILL LINEMODE."""
    w, t = _make_client_writer()
    w.handle_do(LINEMODE)
    all_writes = b"".join(t.writes)
    assert IAC + WILL + LINEMODE in all_writes
    assert IAC + SB + LINEMODE + LMODE_SLC in all_writes
    from telnetlib3.telopt import theNULL

    assert theNULL + slc.SLC_DEFAULT + theNULL in all_writes


def test_server_sends_slc_after_mode_ack():
    """Server proactively sends SLC table after client acknowledges MODE."""
    w, t = _make_server_writer()
    w.remote_option[LINEMODE] = True
    buf = collections.deque([slc.LMODE_MODE_ACK])
    w._handle_sb_linemode_mode(buf)
    all_writes = b"".join(t.writes)
    assert IAC + SB + LINEMODE + LMODE_SLC in all_writes
    assert w._slc_sent is True
    t.writes.clear()
    w._handle_sb_linemode_mode(collections.deque([slc.LMODE_MODE_ACK]))
    assert IAC + SB + LINEMODE + LMODE_SLC not in b"".join(t.writes)


def test_linemode_buffer_ec_el_ew():
    """LinemodeBuffer.feed() handles EC, EL, EW SLC functions correctly."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab)
    for c in "hel":
        buf.feed(c)
    # EC (^? = 0x7F in BSD_SLC_TAB)
    echo, data = buf.feed("\x7f")
    assert echo == "\b \b"
    assert data is None
    assert buf._buf == ["h", "e"]
    # EL (^U = 0x15)
    echo, data = buf.feed("\x15")
    assert echo == "\b \b" * 2
    assert data is None
    assert not buf._buf
    # EW (^W = 0x17) -- add a word first
    for c in "hello world":
        buf.feed(c)
    echo, data = buf.feed("\x17")
    assert echo == "\b \b" * 5
    assert data is None
    assert buf._buf == list("hello ")


def test_linemode_buffer_forwardmask_flush():
    """LinemodeBuffer flushes buffer immediately when a forwardmask character arrives."""
    from telnetlib3.client_shell import LinemodeBuffer

    fm_bytes = bytearray(16)
    # byte 0x01: mask=0, flag=2**(7-1)=64=0x40
    fm_bytes[0] = 0x40
    fm = slc.Forwardmask(bytes(fm_bytes))
    slctab = slc.generate_slctab()
    buf = LinemodeBuffer(slctab=slctab, forwardmask=fm)
    buf.feed("a")
    buf.feed("b")
    echo, data = buf.feed("\x01")
    assert data == b"ab\x01"
    assert not buf._buf


def test_linemode_buffer_trapsig():
    """LinemodeBuffer returns IAC command bytes for signal chars when TRAPSIG is on."""
    from telnetlib3.telopt import IP, IAC
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab, trapsig=True)
    # SLC_IP is ^C = 0x03 in BSD_SLC_TAB
    echo, data = buf.feed("\x03")
    assert echo == ""
    assert data == IAC + IP


def test_linemode_buffer_ec_empty_buf():
    """EC on an empty LinemodeBuffer returns empty echo and no data."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab)
    # EC = 0x7F in BSD_SLC_TAB; buffer is empty
    echo, data = buf.feed("\x7f")
    assert echo == ""
    assert data is None
    assert not buf._buf


def test_linemode_buffer_cr_sends_line():
    """CR flushes the buffer and returns the line as bytes for the server."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab)
    for c in "hello":
        buf.feed(c)
    echo, data = buf.feed("\r")
    assert echo == "\r"
    assert data == b"hello\r"
    assert not buf._buf


def test_linemode_buffer_lf_sends_line():
    """LF flushes the buffer and returns the line as bytes for the server."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab)
    for c in "hi":
        buf.feed(c)
    echo, data = buf.feed("\n")
    assert echo == "\n"
    assert data == b"hi\n"
    assert not buf._buf


def test_linemode_buffer_trapsig_regular_char_buffered():
    """Regular char with trapsig=True is buffered, not sent as IAC."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab, trapsig=True)
    echo, data = buf.feed("a")
    assert echo == "a"
    assert data is None
    assert buf._buf == ["a"]


def test_linemode_buffer_slc_val_nosupport():
    """_slc_val returns None when the SLC entry is nosupport."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab()
    # Force SLC_EC to nosupport
    slctab[slc.SLC_EC] = slc.SLC(slc.SLC_NOSUPPORT, slc.theNULL)
    buf = LinemodeBuffer(slctab=slctab)
    assert buf._slc_val(slc.SLC_EC) is None


def test_linemode_buffer_slc_val_missing():
    """_slc_val returns None when the SLC function is absent from the table."""
    from telnetlib3.client_shell import LinemodeBuffer

    buf = LinemodeBuffer(slctab={})
    assert buf._slc_val(slc.SLC_EC) is None


def test_slc_send_uses_parameter_not_self_slctab():
    """_slc_send(slctab) sends values from the given table, not self.slctab."""
    w, t = _make_server_writer()
    # Modify slctab so SLC_EC has a different value from the default
    modified_val = b"\x42"
    default_val = w.default_slc_tab[slc.SLC_EC].val
    assert default_val != modified_val, "precondition: values must differ"
    w.slctab[slc.SLC_EC] = slc.SLC(slc.SLC_VARIABLE, modified_val)

    # Call _slc_send with the default table; result should contain default_val
    w._slc_start()
    w._slc_send(w.default_slc_tab)
    w._slc_end()

    sent = b"".join(t.writes)
    # Locate the SLC_EC triplet (func, flag, val) in the sent bytes
    ec_byte = slc.SLC_EC
    idx = 0
    found_default = False
    found_modified = False
    while idx < len(sent) - 2:
        if sent[idx : idx + 1] == ec_byte:
            val = sent[idx + 2 : idx + 3]
            if val == default_val:
                found_default = True
            if val == modified_val:
                found_modified = True
        idx += 1
    assert found_default, "expected default value in SLC table"
    assert not found_modified, "must not send modified self.slctab value"


def test_slc_process_default_resets_slctab():
    """(0, SLC_DEFAULT, 0) resets self.slctab to the default table."""
    from telnetlib3.telopt import theNULL

    w, _ = _make_server_writer()
    # Modify an entry so it differs from the default
    w.slctab[slc.SLC_EC] = slc.SLC(slc.SLC_VARIABLE, b"\x42")
    assert w.slctab[slc.SLC_EC].val != w.default_slc_tab[slc.SLC_EC].val

    # Process the special (0, SLC_DEFAULT, 0) request
    w._slc_process(theNULL, slc.SLC(slc.SLC_DEFAULT, theNULL))

    assert w.slctab[slc.SLC_EC].val == w.default_slc_tab[slc.SLC_EC].val


def test_server_sends_slc_table_exactly_once():
    """Server sends SLC table on (0, SLC_DEFAULT, 0) and not again on MODE ACK."""
    from telnetlib3.telopt import theNULL

    w, t = _make_server_writer()
    w.remote_option[LINEMODE] = True

    # Simulate client sending (0, SLC_DEFAULT, 0)
    buf = collections.deque([theNULL, slc.SLC_DEFAULT, theNULL])
    w._handle_sb_linemode_slc(buf)
    slc_table_header = IAC + SB + LINEMODE + LMODE_SLC
    first_send = b"".join(t.writes)
    assert slc_table_header in first_send
    assert w._slc_sent is True

    # Now simulate client sending MODE ACK: server must NOT send SLC table again
    t.writes.clear()
    mode_ack_buf = collections.deque([slc.LMODE_MODE_ACK])
    w._handle_sb_linemode_mode(mode_ack_buf)
    second_send = b"".join(t.writes)
    assert slc_table_header not in second_send


def test_linemode_buffer_ew_skips_trailing_spaces():
    """EW erases trailing whitespace then the preceding word (POSIX VWERASE)."""
    from telnetlib3.client_shell import LinemodeBuffer

    slctab = slc.generate_slctab(slc.BSD_SLC_TAB)
    buf = LinemodeBuffer(slctab=slctab)
    for c in "hello ":
        buf.feed(c)
    # EW (^W = 0x17) on "hello " should erase the trailing space AND "hello"
    echo, data = buf.feed("\x17")
    assert echo == "\b \b" * 6
    assert data is None
    assert not buf._buf


if sys.platform != "win32":
    import termios

    def test_determine_mode_linemode_edit():
        """determine_mode() keeps cooked mode with kernel echo when LINEMODE EDIT is set."""
        import types

        from telnetlib3.telopt import LINEMODE
        from telnetlib3.client_shell import Terminal
        from telnetlib3._session_context import TelnetSessionContext

        class _Opt:
            def __init__(self, active):
                self._active = active

            def enabled(self, key):
                return key in self._active

        linemode = slc.Linemode(slc.LMODE_MODE_LOCAL)
        ctx = TelnetSessionContext()
        writer = types.SimpleNamespace(
            will_echo=False,
            client=True,
            remote_option=_Opt(set()),
            local_option=_Opt({LINEMODE}),
            linemode=linemode,
            log=types.SimpleNamespace(debug=lambda *a, **kw: None),
            ctx=ctx,
        )
        term = Terminal.__new__(Terminal)
        term.telnet_writer = writer
        term.software_echo = False
        mode = Terminal.ModeDef(
            iflag=termios.BRKINT | termios.ICRNL | termios.IXON,
            oflag=termios.OPOST | termios.ONLCR,
            cflag=termios.CS8 | termios.CREAD,
            lflag=termios.ICANON | termios.ECHO | termios.ISIG | termios.IEXTEN,
            ispeed=termios.B38400,
            ospeed=termios.B38400,
            cc=[b"\x00"] * termios.NCCS,
        )
        result = term.determine_mode(mode)
        assert result.lflag & termios.ICANON  # cooked mode: kernel handles line editing
        assert result.lflag & termios.ECHO  # kernel handles echo
        assert term.software_echo is False
