"""Tests for the synchronous (blocking) interface."""

# std imports
import time
import socket
import threading

# 3rd party
import pytest

# local
from telnetlib3.sync import ServerConnection, TelnetConnection, BlockingTelnetServer


@pytest.fixture
def started_server(bind_host, unused_tcp_port):
    """Yield a started BlockingTelnetServer and shut it down on teardown."""
    server = BlockingTelnetServer(bind_host, unused_tcp_port)
    server.start()
    yield server
    server.shutdown()


@pytest.fixture
def serve_with_handler(bind_host, unused_tcp_port):
    """Start a BlockingTelnetServer with a handler, yield (host, port), shutdown on teardown."""
    servers = []

    def _start(handler, **kwargs):
        server = BlockingTelnetServer(bind_host, unused_tcp_port, handler=handler, **kwargs)
        thread = threading.Thread(target=server.serve_forever, daemon=True)
        thread.start()
        server._started.wait(timeout=5)
        servers.append(server)
        return bind_host, unused_tcp_port

    yield _start

    for s in servers:
        s.shutdown()


def test_client_connect_and_close(bind_host, unused_tcp_port, started_server):
    """TelnetConnection connects and closes properly."""
    conn = TelnetConnection(bind_host, unused_tcp_port, timeout=5)
    conn.connect()
    assert conn._connected.is_set()
    conn.close()


def test_client_context_manager(bind_host, unused_tcp_port, started_server):
    """TelnetConnection works as context manager."""
    with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
        assert conn._connected.is_set()


def test_client_read_write(serve_with_handler):
    """TelnetConnection and ServerConnection read/write work correctly."""

    def handler(server_conn):
        data = server_conn.read(5, timeout=5)
        server_conn.write(data.upper())
        server_conn.flush(timeout=5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        conn.write("hello")
        conn.flush()
        assert conn.read(5, timeout=5) == "HELLO"


def test_client_readline(serve_with_handler):
    def handler(server_conn):
        server_conn.write("Hello, World!\r\n")
        server_conn.flush(timeout=5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        assert "Hello, World!" in conn.readline(timeout=5)


def test_client_read_until(serve_with_handler):
    def handler(server_conn):
        server_conn.write(">>> ")
        server_conn.flush(timeout=5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        assert conn.read_until(">>> ", timeout=5).endswith(b">>> ")


def test_client_read_some_alias(serve_with_handler):
    """TelnetConnection read_some is alias for read."""

    def handler(server_conn):
        server_conn.write("test")
        server_conn.flush(timeout=5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        assert "test" in conn.read_some(timeout=5)


def test_client_not_connected_error():
    """Operations fail when not connected."""
    conn = TelnetConnection("localhost", 12345)
    with pytest.raises(RuntimeError, match="Not connected"):
        conn.read()
    with pytest.raises(RuntimeError, match="Not connected"):
        conn.write("test")


def test_client_already_connected_error(bind_host, unused_tcp_port, started_server):
    """Connect fails if already connected."""
    with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
        with pytest.raises(RuntimeError, match="Already connected"):
            conn.connect()


def test_server_start_and_shutdown(bind_host, unused_tcp_port):
    """BlockingTelnetServer starts and shuts down properly."""
    server = BlockingTelnetServer(bind_host, unused_tcp_port)
    server.start()
    assert server._started.is_set()
    server.shutdown()


def test_server_accept(bind_host, unused_tcp_port, started_server):
    """BlockingTelnetServer accepts connections."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(0.5)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    assert isinstance(conn, ServerConnection)
    conn.close()


def test_server_serve_forever(serve_with_handler):
    """BlockingTelnetServer serve_forever with handler."""
    received = []

    def handler(conn):
        received.append(conn.read(4, timeout=5))
        conn.write(received[-1].upper())
        conn.flush(timeout=5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        conn.write("test")
        conn.flush()
        assert conn.read(4, timeout=5) == "TEST"

    assert received == ["test"]


def test_server_serve_forever_no_handler_error(bind_host, unused_tcp_port):
    """serve_forever raises without handler."""
    server = BlockingTelnetServer(bind_host, unused_tcp_port)
    with pytest.raises(RuntimeError, match="No handler provided"):
        server.serve_forever()


def test_server_accept_not_started_error(bind_host, unused_tcp_port):
    """Accept raises if server not started."""
    server = BlockingTelnetServer(bind_host, unused_tcp_port)
    with pytest.raises(RuntimeError, match="Server not started"):
        server.accept()


def test_server_accept_timeout(bind_host, unused_tcp_port, started_server):
    """Accept times out when no client connects."""
    with pytest.raises(TimeoutError, match="Accept timed out"):
        started_server.accept(timeout=0.1)


def test_server_connection_read_write(bind_host, unused_tcp_port, started_server):
    """ServerConnection read and write work correctly."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
            conn.write("hello")
            conn.flush()
            assert conn.read(5, timeout=5) == "HELLO"

    thread = threading.Thread(target=client_thread)
    thread.start()

    conn = started_server.accept(timeout=5)
    conn.write(conn.read(5, timeout=5).upper())
    conn.flush(timeout=5)
    conn.close()

    thread.join(timeout=5)


def test_server_connection_closed_error(bind_host, unused_tcp_port, started_server):
    """Operations fail on closed ServerConnection."""

    def client_thread():
        time.sleep(0.1)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(0.1)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    conn.close()

    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.read()
    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.write("test")


def test_server_connection_miniboa_properties(bind_host, unused_tcp_port, started_server):
    """ServerConnection has miniboa-compatible properties."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(0.5)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)

    assert conn.active is True
    assert conn.address == bind_host
    assert isinstance(conn.port, int) and conn.port > 0
    assert conn.terminal_type == "unknown"
    assert conn.columns == 80
    assert conn.rows == 25
    assert isinstance(conn.connect_time, float)
    assert isinstance(conn.last_input_time, float)

    conn.close()
    assert conn.active is False


def test_server_connection_miniboa_methods(bind_host, unused_tcp_port, started_server):
    """ServerConnection has miniboa-compatible methods."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
            conn.write("test\r\n")
            conn.flush()
            time.sleep(0.5)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)

    assert f"{bind_host}:" in conn.addrport()
    assert conn.idle() >= 0
    assert conn.duration() >= 0

    time.sleep(0.05)
    assert conn.idle() >= 0.05

    conn.readline(timeout=5)
    assert conn.idle() < 0.1

    conn.deactivate()
    assert conn.active is False


@pytest.mark.parametrize(
    "send_text,expected_suffix",
    [
        ("Hello\n", "Hello\r\n"),
        ("Hello\r\n", "Hello\r\n"),
        ("A\nB\r\nC\n", "A\r\nB\r\nC\r\n"),
        ("bare text", "bare text"),
    ],
)
def test_server_connection_send_newline_conversion(
    bind_host, unused_tcp_port, send_text, expected_suffix, started_server
):
    """Send() normalizes all newline styles to \\r\\n without doubling."""
    received = []

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
            received.append(conn.read(100, timeout=5))

    thread = threading.Thread(target=client_thread)
    thread.start()

    conn = started_server.accept(timeout=5)
    conn.send(send_text)
    conn.flush(timeout=5)
    conn.close()

    thread.join(timeout=5)

    assert len(received) == 1
    assert received[0].endswith(expected_suffix)
    assert "\r\r\n" not in received[0]


def _assert_writer_attrs(writer):
    assert writer is not None
    assert hasattr(writer, "mode")
    assert hasattr(writer, "remote_option")
    assert hasattr(writer, "local_option")


def test_client_writer_property(bind_host, unused_tcp_port, started_server):
    with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
        _assert_writer_attrs(conn.writer)


def test_server_connection_writer_property(bind_host, unused_tcp_port, started_server):
    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(0.5)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    _assert_writer_attrs(conn.writer)

    conn.close()


def test_client_get_extra_info(bind_host, unused_tcp_port, started_server):
    with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
        peername = conn.get_extra_info("peername")
        assert peername is not None
        assert len(peername) == 2
        assert isinstance(peername[1], int)
        assert conn.get_extra_info("nonexistent") is None
        assert conn.get_extra_info("nonexistent", "default") == "default"


def test_client_operations_after_close_raise(bind_host, unused_tcp_port, started_server):
    """Operations fail after connection is closed."""
    conn = TelnetConnection(bind_host, unused_tcp_port, timeout=5)
    conn.connect()
    conn.close()

    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.read()
    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.readline()
    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.write("test")
    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.flush()
    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.get_extra_info("peername")
    with pytest.raises(RuntimeError, match="Connection closed"):
        conn.wait_for(remote={"NAWS": True})


def test_client_read_timeout(serve_with_handler):
    """TelnetConnection.read times out when no data available."""

    def handler(server_conn):
        time.sleep(5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        with pytest.raises(TimeoutError, match="Read timed out"):
            conn.read(1, timeout=0.1)


def test_client_readline_timeout(serve_with_handler):
    """TelnetConnection.readline times out when no line available."""

    def handler(server_conn):
        server_conn.write("no newline here")
        server_conn.flush(timeout=5)
        time.sleep(5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        with pytest.raises(TimeoutError, match="Readline timed out"):
            conn.readline(timeout=0.1)


@pytest.mark.parametrize(
    "method,args,error_match",
    [
        pytest.param("read", (1,), "Read timed out", id="read"),
        pytest.param("readline", (), "Readline timed out", id="readline"),
    ],
)
def test_server_connection_timeout(
    bind_host, unused_tcp_port, method, args, error_match, started_server
):
    """ServerConnection methods time out when no data available."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(2)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    with pytest.raises(TimeoutError, match=error_match):
        getattr(conn, method)(*args, timeout=0.1)
    conn.close()


def test_server_connection_read_until_timeout(bind_host, unused_tcp_port, started_server):
    """ServerConnection.read_until times out when match not found."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
            conn.write("no match here")
            conn.flush()
            time.sleep(2)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    with pytest.raises(TimeoutError, match="Read until timed out"):
        conn.read_until(">>> ", timeout=0.1)
    conn.close()


def test_server_connection_wait_for_timeout(bind_host, unused_tcp_port, started_server):
    """ServerConnection.wait_for times out when conditions not met."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(1.0)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    with pytest.raises(TimeoutError, match="Wait for negotiation timed out"):
        conn.wait_for(remote={"LINEMODE": True}, timeout=0.1)
    conn.close()


@pytest.mark.parametrize(
    "method,args,kwargs",
    [
        pytest.param("wait_for", (), {"remote": {"NAWS": True}}, id="wait_for"),
        pytest.param("read_until", (">>> ",), {}, id="read_until"),
        pytest.param("flush", (), {}, id="flush"),
        pytest.param("readline", (), {}, id="readline"),
    ],
)
def test_server_connection_methods_closed_error(
    bind_host, unused_tcp_port, method, args, kwargs, started_server
):
    """ServerConnection methods raise RuntimeError when called after close."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(0.2)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    conn.close()
    with pytest.raises(RuntimeError, match="Connection closed"):
        getattr(conn, method)(*args, **kwargs)


def test_server_already_started_error(bind_host, unused_tcp_port, started_server):
    """Server start raises if already started."""
    with pytest.raises(RuntimeError, match="Server already started"):
        started_server.start()


def test_client_read_until_eof(serve_with_handler):
    """TelnetConnection.read_until raises EOFError on early close."""

    def handler(server_conn):
        server_conn.write("partial data")
        server_conn.flush(timeout=5)
        server_conn.close()

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        with pytest.raises(EOFError, match="Connection closed before match found"):
            conn.read_until(">>> ", timeout=2)


def test_client_connect_timeout_unreachable(bind_host, unused_tcp_port):
    """TelnetConnection connect_timeout raises ConnectionError."""
    conn = TelnetConnection(bind_host, unused_tcp_port, timeout=5, connect_timeout=0.1)
    with pytest.raises(ConnectionError):
        conn.connect()


def test_client_connect_timeout_success(bind_host, unused_tcp_port, started_server):
    """TelnetConnection connect_timeout does not interfere with success."""
    with TelnetConnection(bind_host, unused_tcp_port, timeout=5, connect_timeout=5.0) as conn:
        assert conn._connected.is_set()


def test_client_double_close(bind_host, unused_tcp_port, started_server):
    """Closing a TelnetConnection twice is safe (idempotent)."""
    conn = TelnetConnection(bind_host, unused_tcp_port, timeout=5)
    conn.connect()
    conn.close()
    conn.close()
    assert conn._closed is True


def test_client_connect_timeout_fires(bind_host, unused_tcp_port):
    """TelnetConnection.connect raises TimeoutError on very short timeout."""
    conn = TelnetConnection(bind_host, unused_tcp_port, timeout=0.001)
    with pytest.raises((TimeoutError, ConnectionError, OSError)):
        conn.connect()
    assert conn._closed is False or conn._thread is None


def test_client_wait_for_timeout(bind_host, unused_tcp_port, started_server):
    """TelnetConnection.wait_for raises TimeoutError."""
    with TelnetConnection(bind_host, unused_tcp_port, timeout=5) as conn:
        with pytest.raises(TimeoutError, match="Wait for negotiation timed out"):
            conn.wait_for(remote={"LINEMODE": True}, timeout=0.1)


def test_server_connection_double_close(bind_host, unused_tcp_port, started_server):
    """Closing a ServerConnection twice is safe (idempotent)."""

    def client_thread():
        time.sleep(0.05)
        with TelnetConnection(bind_host, unused_tcp_port, timeout=5):
            time.sleep(0.5)

    thread = threading.Thread(target=client_thread, daemon=True)
    thread.start()

    conn = started_server.accept(timeout=5)
    conn.close()
    conn.close()
    assert conn._closed is True


def test_client_read_until_timeout(serve_with_handler):
    """TelnetConnection.read_until times out when match not found."""

    def handler(server_conn):
        server_conn.write("no match here")
        server_conn.flush(timeout=5)
        time.sleep(5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        with pytest.raises(TimeoutError, match="Read until timed out"):
            conn.read_until(">>> ", timeout=0.1)


def test_client_flush_timeout(serve_with_handler):
    """TelnetConnection.flush works after writing data."""

    def handler(server_conn):
        server_conn.read(5, timeout=5)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        conn.write("hello")
        conn.flush(timeout=5)


def test_client_connect_timeout_ephemeral(bind_host):
    """TelnetConnection raises TimeoutError on connection timeout."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((bind_host, 0))
    port = sock.getsockname()[1]
    sock.close()

    conn = TelnetConnection(bind_host, port, timeout=0.1)
    with pytest.raises((TimeoutError, OSError)):
        conn.connect()


@pytest.mark.parametrize(
    "method,args",
    [pytest.param("read", (100,), id="read"), pytest.param("readline", (), id="readline")],
)
def test_client_read_eof(serve_with_handler, method, args):
    """TelnetConnection.read/readline raises EOFError when server closes."""
    received = threading.Event()

    def handler(server_conn):
        received.wait(timeout=5)
        server_conn.close()

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5) as conn:
        received.set()
        time.sleep(0.2)
        with pytest.raises(EOFError):
            getattr(conn, method)(*args, timeout=2)


def test_client_cleanup_exception_handling(bind_host, unused_tcp_port, started_server):
    """TelnetConnection._cleanup swallows exceptions."""
    conn = TelnetConnection(bind_host, unused_tcp_port, timeout=5)
    conn.connect()
    conn.close()
    conn._cleanup()


@pytest.mark.timeout(30)
def test_server_connection_read_some(serve_with_handler):
    """ServerConnection.read_some returns available data."""
    result = []
    done = threading.Event()

    def handler(server_conn):
        data = server_conn.read_some(timeout=10)
        result.append(data)
        done.set()

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=10, encoding=False) as conn:
        conn.write(b"hello")
        conn.flush(timeout=10)
        assert done.wait(timeout=10)

    assert len(result) == 1


def test_server_connection_read_until(serve_with_handler):
    """ServerConnection.read_until returns data up to match."""
    result = []

    def handler(server_conn):
        data = server_conn.read_until(b">>>", timeout=5)
        result.append(data)

    host, port = serve_with_handler(handler, encoding=False)
    with TelnetConnection(host, port, timeout=5, encoding=False) as conn:
        conn.write(b"hello>>>")
        conn.flush(timeout=5)
        time.sleep(0.3)

    assert len(result) == 1
    assert b">>>" in result[0]


def test_server_connection_send_newline(serve_with_handler):
    """ServerConnection.send normalizes newlines to CRLF."""
    result = []

    def handler(server_conn):
        server_conn.send("hello\nworld")
        time.sleep(0.2)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5, encoding=False) as conn:
        time.sleep(0.5)
        data = conn.read(-1, timeout=2)
        result.append(data)

    assert len(result) == 1
    assert b"\r\n" in result[0]


def test_server_shutdown_cancels_tasks(serve_with_handler):
    """BlockingTelnetServer.shutdown cancels pending tasks."""

    def handler(server_conn):
        time.sleep(10)

    host, port = serve_with_handler(handler)
    with TelnetConnection(host, port, timeout=5, encoding=False) as conn:
        time.sleep(0.1)
