"""Test CHARSET, rfc-2066_."""

# std imports
import asyncio
import collections

# 3rd party
import pytest

# local
import telnetlib3
import telnetlib3.stream_writer
from telnetlib3.telopt import DO, SB, SE, IAC, WILL, WONT, TTYPE, CHARSET, REQUEST, ACCEPTED
from telnetlib3.stream_writer import TelnetWriter
from telnetlib3.tests.accessories import (
    MockTransport,
    create_server,
    asyncio_server,
    open_connection,
    asyncio_connection,
)


class MockProtocol:
    def __init__(self):
        self.force_binary = False

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

    async def _drain_helper(self):
        pass


def new_writer(server=True, client=False):
    t = MockTransport()
    p = MockProtocol()
    w = TelnetWriter(t, p, server=server, client=client)
    return w, t, p


class CustomTelnetClient(telnetlib3.TelnetClient):
    """Test client with controlled send_charset() behavior."""

    def __init__(self, *args, **kwargs):
        self.charset_behavior = kwargs.pop("charset_behavior", None)
        self.charset_response = kwargs.pop("charset_response", None)
        super().__init__(*args, **kwargs)

    def send_charset(self, offered):
        """Override to allow testing specific behavior branches."""
        if self.charset_behavior == "unknown_encoding":
            # Test LookupError handling with explicit encoding
            self.default_encoding = "unknown-encoding-xyz"
            return super().send_charset(offered)
        if self.charset_behavior == "no_viable_offers":
            # Return empty offers list to test no viable offers path
            return super().send_charset([])
        if self.charset_behavior == "explicit_non_latin1":
            # Test rejection when explicit encoding isn't offered
            self.default_encoding = "utf-16"
            return super().send_charset(["utf-8", "ascii"])
        if self.charset_response is not None:
            # Return a predetermined response
            return self.charset_response
        return super().send_charset(offered)


async def test_telnet_server_on_charset(bind_host, unused_tcp_port):
    """Test Server's callback method on_charset()."""
    _waiter = asyncio.Future()
    given_charset = "KOI8-U"

    class ServerTestCharset(telnetlib3.TelnetServer):
        def on_charset(self, charset):
            super().on_charset(charset)
            _waiter.set_result(self)

    async with create_server(
        protocol_factory=ServerTestCharset, host=bind_host, port=unused_tcp_port
    ):
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            await asyncio.wait_for(reader.readexactly(3), 0.5)
            writer.write(IAC + WILL + CHARSET)
            writer.write(IAC + WONT + TTYPE)
            writer.write(IAC + SB + CHARSET + ACCEPTED + given_charset.encode("ascii") + IAC + SE)

            srv_instance = await asyncio.wait_for(_waiter, 2.0)
            assert srv_instance.get_extra_info("charset") == given_charset

            srv_instance.writer.close()
            await srv_instance.writer.wait_closed()


async def test_telnet_client_send_charset(bind_host, unused_tcp_port):
    """Test Client's callback method send_charset() selection for illegals."""
    _waiter = asyncio.Future()
    server_instance = {"protocol": None}

    class ServerTestCharset(telnetlib3.TelnetServer):
        def begin_negotiation(self):
            server_instance["protocol"] = self
            return super().begin_negotiation()

        def on_request_charset(self):
            return ["illegal", "cp437"]

    class ClientTestCharset(telnetlib3.TelnetClient):
        def send_charset(self, offered):
            selected = super().send_charset(offered)
            _waiter.set_result(selected)
            return selected

    async with create_server(
        protocol_factory=ServerTestCharset, host=bind_host, port=unused_tcp_port
    ):
        async with open_connection(
            client_factory=ClientTestCharset,
            host=bind_host,
            port=unused_tcp_port,
            encoding="latin1",
        ) as (reader, writer):
            val = await asyncio.wait_for(_waiter, 1.5)
            assert val == "cp437"
            assert writer.get_extra_info("charset") == "cp437"

            if server_instance["protocol"]:
                server_instance["protocol"].writer.close()
                await server_instance["protocol"].writer.wait_closed()


async def test_telnet_client_no_charset(bind_host, unused_tcp_port):
    """Test Client's callback method send_charset() does not select."""
    _waiter = asyncio.Future()
    server_instance = {"protocol": None}

    class ServerTestCharset(telnetlib3.TelnetServer):
        def begin_negotiation(self):
            server_instance["protocol"] = self
            return super().begin_negotiation()

        def on_request_charset(self):
            return ["illegal", "this-is-no-good-either"]

    class ClientTestCharset(telnetlib3.TelnetClient):
        def send_charset(self, offered):
            selected = super().send_charset(offered)
            _waiter.set_result(selected)
            return selected

    async with create_server(
        protocol_factory=ServerTestCharset, host=bind_host, port=unused_tcp_port
    ):
        async with open_connection(
            client_factory=ClientTestCharset,
            host=bind_host,
            port=unused_tcp_port,
            encoding="latin1",
        ) as (reader, writer):
            val = await asyncio.wait_for(_waiter, 0.5)
            assert not val
            assert writer.get_extra_info("charset") == "latin1"

            if server_instance["protocol"]:
                server_instance["protocol"].writer.close()
                await server_instance["protocol"].writer.wait_closed()


def test_server_sends_do_and_will_charset():
    """Test server can send both DO CHARSET and WILL CHARSET per RFC 2066."""
    ws, ts, _ = new_writer(server=True)

    # Server sends DO CHARSET (requesting client capability)
    assert ws.iac(DO, CHARSET) is True
    assert ts.writes[-1] == IAC + DO + CHARSET

    # Server also sends WILL CHARSET (advertising its own capability)
    assert ws.iac(WILL, CHARSET) is True
    assert ts.writes[-1] == IAC + WILL + CHARSET


def test_client_do_will_then_server_will_allows_client_request():
    """Test scenario from logfile: DO->WILL then server WILL allows client to send SB REQUEST."""
    wc, tc, _ = new_writer(server=False, client=True)
    wc.set_ext_offer_callback(CHARSET, lambda: ["UTF-8"])

    # Simulate server DO CHARSET
    # Note: handle_do() returns True but local_option[...] is set by the caller
    # path in feed_byte(); set it explicitly here for the unit test.
    wc.handle_do(CHARSET)
    wc.local_option[CHARSET] = True
    assert tc.writes[-1] == IAC + WILL + CHARSET

    # Simulate server WILL CHARSET (the "unsolicited" one from logfile)
    wc.handle_will(CHARSET)
    assert wc.remote_option[CHARSET] is True

    # Client should NOT automatically send SB CHARSET REQUEST,
    # but should be able to send one manually
    tc.writes.clear()

    # Verify client can send a request now that both WILL/DO are established
    assert wc.request_charset() is True
    assert tc.writes[-1].startswith(IAC + SB + CHARSET + REQUEST)


def test_bidirectional_charset_both_sides_can_request():
    """Test that both server and client can initiate CHARSET REQUEST when both have WILL/DO."""
    # Server side
    ws, ts, _ = new_writer(server=True)
    ws.set_ext_offer_callback(CHARSET, lambda: ["UTF-8", "ASCII"])

    # Client side
    wc, tc, _ = new_writer(server=False, client=True)
    wc.set_ext_offer_callback(CHARSET, lambda: ["UTF-8"])

    # Simulate full negotiation: server DO, client WILL, server WILL, client DO
    ws.remote_option[CHARSET] = True  # client sent WILL
    wc.local_option[CHARSET] = True  # client sent WILL/received DO
    wc.remote_option[CHARSET] = True  # server sent WILL
    ws.local_option[CHARSET] = True  # server sent WILL/received DO

    # Both sides should be able to initiate REQUEST
    assert ws.request_charset() is True
    assert wc.request_charset() is True

    # Verify both sent REQUEST frames
    assert ts.writes[-1].startswith(IAC + SB + CHARSET + REQUEST)
    assert tc.writes[-1].startswith(IAC + SB + CHARSET + REQUEST)


def test_charset_request_response_cycle():
    """Test complete CHARSET REQUEST/ACCEPTED cycle."""
    # Server initiates REQUEST
    ws, ts, _ = new_writer(server=True)
    ws.remote_option[CHARSET] = True
    ws.set_ext_offer_callback(CHARSET, lambda: ["UTF-8", "ASCII"])

    assert ws.request_charset() is True
    request_frame = ts.writes[-1]
    assert request_frame.startswith(IAC + SB + CHARSET + REQUEST)

    # Client responds with ACCEPTED (server should only invoke callback, not send)
    charset_selected = "UTF-8"
    response_buf = collections.deque([CHARSET, ACCEPTED, charset_selected.encode("ascii")])
    seen = {}
    ws.set_ext_callback(CHARSET, lambda cs: seen.setdefault("cs", cs))
    ws._handle_sb_charset(response_buf)
    assert seen.get("cs") == "UTF-8"


def test_server_sends_will_charset_after_client_will():
    """Test server sends WILL CHARSET after receiving WILL CHARSET from client."""
    ws, ts, _ = new_writer(server=True)

    # Server has not yet sent WILL CHARSET
    assert not ws.local_option.enabled(CHARSET)

    # Simulate client sending WILL CHARSET
    ws.handle_will(CHARSET)

    # Verify server sent WILL CHARSET in response
    assert IAC + WILL + CHARSET in ts.writes

    # Verify server also called request_charset as usual
    # (this is tested by checking if it would send a request,
    # but we need to set up the callback first)
    ws.set_ext_offer_callback(CHARSET, lambda: ["UTF-8"])
    # Clear previous writes to test just the request
    ts.writes.clear()

    # The handle_will should have also called request_charset
    # Since remote_option[CHARSET] is now True from handle_will
    assert ws.remote_option.enabled(CHARSET)


def test_server_does_not_send_duplicate_will_charset():
    """Test server doesn't send WILL CHARSET if already sent."""
    ws, ts, _ = new_writer(server=True)

    # Server has already sent WILL CHARSET
    ws.local_option[CHARSET] = True

    # Clear any previous writes
    ts.writes.clear()

    # Simulate client sending WILL CHARSET
    ws.handle_will(CHARSET)

    # Verify server did NOT send WILL CHARSET again
    assert IAC + WILL + CHARSET not in ts.writes

    # But remote option should still be set
    assert ws.remote_option.enabled(CHARSET)


def test_client_responds_with_do_to_will_charset():
    """Test client responds with DO CHARSET when receiving WILL CHARSET from server."""
    # Create client writer instance
    transport = MockTransport()
    protocol = MockProtocol()
    client_writer = TelnetWriter(transport, protocol, client=True, server=False)

    # Simulate server sending WILL CHARSET
    client_writer.handle_will(CHARSET)

    assert IAC + DO + CHARSET in transport.writes
    assert client_writer.remote_option.enabled(CHARSET)


def test_unit_charset_negotiation_sequence():
    """Unit test for the CHARSET negotiation sequence with the fixed code."""
    # Create mock server and client
    server_transport = MockTransport()
    server_protocol = MockProtocol()
    server_writer = TelnetWriter(server_transport, server_protocol, server=True)

    client_transport = MockTransport()
    client_protocol = MockProtocol()
    client_writer = TelnetWriter(client_transport, client_protocol, client=True)

    # Simulate the exact bug scenario:
    # 1. Server sends IAC DO CHARSET
    server_writer.iac(DO, CHARSET)
    assert server_transport.writes[-1] == IAC + DO + CHARSET

    # 2. Client receives IAC DO CHARSET and responds with IAC WILL CHARSET
    client_writer.handle_do(CHARSET)
    client_writer.local_option[CHARSET] = True  # Simulating the full code path
    assert client_transport.writes[-1] == IAC + WILL + CHARSET

    # 3. Server receives IAC WILL CHARSET
    server_writer.handle_will(CHARSET)
    assert server_writer.remote_option.enabled(CHARSET)

    # Server should respond with its own IAC WILL CHARSET (bi-directional exchange)
    # Note: The server also immediately sends CHARSET REQUEST after WILL CHARSET
    # when both sides have CHARSET capability, so we need to check if WILL CHARSET is in the writes
    assert IAC + WILL + CHARSET in server_transport.writes

    # 4. Client receives IAC WILL CHARSET and should respond with IAC DO CHARSET
    client_transport.writes.clear()  # Clear previous writes
    client_writer.handle_will(CHARSET)

    assert IAC + DO + CHARSET in client_transport.writes

    # After this exchange, both sides should have CHARSET capability enabled
    assert server_writer.remote_option.enabled(CHARSET)
    assert server_writer.local_option.enabled(CHARSET)
    assert client_writer.remote_option.enabled(CHARSET)
    assert client_writer.local_option.enabled(CHARSET)


@pytest.mark.parametrize(
    "charset_behavior",
    [
        pytest.param("unknown_encoding", id="unknown-encoding"),
        pytest.param("no_viable_offers", id="no-viable-offers"),
        pytest.param("explicit_non_latin1", id="explicit-non-latin1"),
    ],
)
async def test_charset_send_edge_cases(bind_host, unused_tcp_port, charset_behavior):
    """Test client charset edge cases all fall back to US-ASCII."""
    async with asyncio_server(asyncio.Protocol, bind_host, unused_tcp_port):
        async with open_connection(
            client_factory=lambda **kwargs: CustomTelnetClient(
                charset_behavior=charset_behavior, **kwargs
            ),
            host=bind_host,
            port=unused_tcp_port,
            connect_maxwait=0.25,
        ) as (reader, writer):
            assert writer.protocol.encoding(incoming=True) == "US-ASCII"


def test_charset_accepted_sets_force_binary_on_request_side():
    """CHARSET ACCEPTED by peer sets force_binary on the requesting side."""
    w, t, p = new_writer(server=True)
    w.handle_will(CHARSET)
    w.remote_option[CHARSET] = True
    w.local_option[CHARSET] = True

    buf = collections.deque([CHARSET, ACCEPTED, b"UTF-8"])
    w._handle_sb_charset(buf)

    assert w.environ_encoding == "UTF-8"
    assert p.force_binary is True


def test_charset_accepted_sets_force_binary_on_accepting_side():
    """Selecting a charset from a REQUEST sets force_binary on the accepting side."""
    w, t, p = new_writer(server=False, client=True)
    w.handle_do(CHARSET)
    w.local_option[CHARSET] = True
    w.remote_option[CHARSET] = True

    sep = b";"
    w._ext_send_callback[CHARSET] = lambda offers: "UTF-8"
    buf = collections.deque([CHARSET, REQUEST, sep, b"UTF-8"])
    w._handle_sb_charset(buf)

    assert w.environ_encoding == "UTF-8"
    assert p.force_binary is True


def test_client_request_charset_uses_offer_callback():
    """Client request_charset() must use offer callback, not send callback."""
    wc, tc, _ = new_writer(server=False, client=True)
    wc.local_option[CHARSET] = True
    wc.remote_option[CHARSET] = True

    wc.set_ext_offer_callback(CHARSET, lambda: ["UTF-8", "CP437"])
    wc.set_ext_send_callback(CHARSET, lambda offered: offered[0])

    assert wc.request_charset() is True
    frame = tc.writes[-1]
    assert frame.startswith(IAC + SB + CHARSET + REQUEST)
    assert b"UTF-8" in frame
    assert b"CP437" in frame


def test_server_request_charset_uses_offer_callback():
    """Server request_charset() must use offer callback, not send callback."""
    ws, ts, _ = new_writer(server=True)
    ws.remote_option[CHARSET] = True

    ws.set_ext_offer_callback(CHARSET, lambda: ["UTF-8", "ASCII"])
    ws.set_ext_send_callback(CHARSET, lambda offered: offered[0])

    assert ws.request_charset() is True
    frame = ts.writes[-1]
    assert frame.startswith(IAC + SB + CHARSET + REQUEST)
    assert b"UTF-8" in frame


def test_handle_sb_charset_request_uses_send_callback():
    """Receiving SB CHARSET REQUEST must use send callback (not offer)."""
    wc, tc, _ = new_writer(server=False, client=True)
    wc.local_option[CHARSET] = True
    wc.remote_option[CHARSET] = True

    offer_called = []
    wc.set_ext_offer_callback(CHARSET, lambda: offer_called.append(True) or ["NOPE"])
    wc.set_ext_send_callback(CHARSET, lambda offered: "UTF-8")

    sep = b" "
    buf = collections.deque([CHARSET, REQUEST, sep, b"UTF-8"])
    wc._handle_sb_charset(buf)

    assert not offer_called
    assert wc.environ_encoding == "UTF-8"


def test_server_handle_sb_charset_request_uses_send_callback():
    """Server receiving SB CHARSET REQUEST from client uses send callback."""
    ws, ts, _ = new_writer(server=True)
    ws.remote_option[CHARSET] = True
    ws.local_option[CHARSET] = True

    ws.set_ext_offer_callback(CHARSET, lambda: ["SHOULD-NOT-USE"])
    ws.set_ext_send_callback(CHARSET, lambda offered: "CP437" if "CP437" in offered else "")

    sep = b" "
    buf = collections.deque([CHARSET, REQUEST, sep, b"CP437"])
    ws._handle_sb_charset(buf)

    assert ws.environ_encoding == "CP437"
