"""Test instantiation of basic server and client forms."""

# std imports
import os
import sys
import time
import asyncio
import platform
import tempfile

# 3rd party
import pytest
import pexpect

# local
import telnetlib3
from telnetlib3.telopt import DO, SB, IAC, SGA, ECHO, NAWS, WILL, WONT, TTYPE, BINARY, CHARSET
from telnetlib3.tests.accessories import (
    create_server,
    asyncio_server,
    open_connection,
    asyncio_connection,
)


async def test_create_server(bind_host, unused_tcp_port):
    """Test telnetlib3.create_server basic instantiation."""
    async with create_server(host=bind_host, port=unused_tcp_port):
        pass


async def test_create_server_conditionals(bind_host, unused_tcp_port):
    """Test telnetlib3.create_server conditionals."""
    async with create_server(
        protocol_factory=lambda: telnetlib3.TelnetServer, host=bind_host, port=unused_tcp_port
    ):
        pass


async def test_create_server_on_connect(bind_host, unused_tcp_port):
    """Test on_connect() anonymous function callback of create_server."""
    call_tracker = {"called": False, "transport": None}

    class TrackingProtocol(asyncio.Protocol):
        def __init__(self):
            call_tracker["called"] = True

        def connection_made(self, transport):
            call_tracker["transport"] = transport

    async with create_server(
        protocol_factory=TrackingProtocol, host=bind_host, port=unused_tcp_port
    ):
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            await asyncio.sleep(0.01)
            assert call_tracker["called"]
        if call_tracker["transport"]:
            call_tracker["transport"].close()
            await asyncio.sleep(0)


async def test_telnet_server_open_close(bind_host, unused_tcp_port):
    """Test telnetlib3.TelnetServer() instantiation and connection_made()."""
    async with create_server(host=bind_host, port=unused_tcp_port) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (stream_reader, stream_writer):
            stream_writer.write(IAC + WONT + TTYPE + b"bye\r")
            srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5)
            srv_instance.writer.write("Goodbye!")
            srv_instance.writer.close()
            await srv_instance.writer.wait_closed()
            assert srv_instance.writer.is_closing()
            result = await stream_reader.read()
            assert result == b"\xff\xfd\x18Goodbye!"


async def test_telnet_client_open_close_by_write(bind_host, unused_tcp_port):
    """Exercise BaseClient.connection_lost() on writer closed."""
    async with asyncio_server(asyncio.Protocol, bind_host, unused_tcp_port):
        async with open_connection(host=bind_host, port=unused_tcp_port) as (reader, writer):
            writer.close()
            await writer.wait_closed()
            assert not await reader.read()
            assert writer.is_closing()


async def test_telnet_client_open_closed_by_peer(bind_host, unused_tcp_port):
    """Exercise BaseClient.connection_lost()."""

    class DisconnecterProtocol(asyncio.Protocol):
        def connection_made(self, transport):
            transport.close()

    async with asyncio_server(DisconnecterProtocol, bind_host, unused_tcp_port):
        async with open_connection(host=bind_host, port=unused_tcp_port) as (reader, writer):
            assert not await reader.read()


async def test_telnet_server_advanced_negotiation(bind_host, unused_tcp_port):
    """Test telnetlib3.TelnetServer() advanced negotiation."""
    _waiter = asyncio.Future()

    class ServerTestAdvanced(telnetlib3.TelnetServer):
        def begin_advanced_negotiation(self):
            super().begin_advanced_negotiation()
            _waiter.set_result(self)

    async with create_server(
        protocol_factory=ServerTestAdvanced, host=bind_host, port=unused_tcp_port
    ):
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            writer.write(IAC + WILL + TTYPE)
            srv_instance = await asyncio.wait_for(_waiter, 0.5)

            assert srv_instance.writer.remote_option[TTYPE] is True
            assert srv_instance.writer.pending_option == {
                DO + TTYPE: False,
                SB + TTYPE: True,
                DO + CHARSET: True,
                DO + NAWS: True,
                WILL + SGA: True,
                WILL + BINARY: True,
            }


@pytest.mark.parametrize("option,trigger_echo", [(SGA, False), (ECHO, True)])
async def test_line_mode_skips_will_option(bind_host, unused_tcp_port, option, trigger_echo):
    """Server with line_mode=True does not send WILL SGA or WILL ECHO."""
    _waiter = asyncio.Future()

    class ServerTestLineMode(telnetlib3.TelnetServer):
        def begin_advanced_negotiation(self):
            super().begin_advanced_negotiation()
            _waiter.set_result(self)

    async with create_server(
        protocol_factory=ServerTestLineMode, host=bind_host, port=unused_tcp_port, line_mode=True
    ):
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            writer.write(IAC + WILL + TTYPE)
            srv_instance = await asyncio.wait_for(_waiter, 0.5)

            if trigger_echo:
                srv_instance._negotiate_echo()

            pending = srv_instance.writer.pending_option
            assert WILL + option not in pending


async def test_default_sends_will_sga(bind_host, unused_tcp_port):
    """Default server (line_mode=False) sends WILL SGA."""
    _waiter = asyncio.Future()

    class ServerTestDefault(telnetlib3.TelnetServer):
        def begin_advanced_negotiation(self):
            super().begin_advanced_negotiation()
            _waiter.set_result(self)

    async with create_server(
        protocol_factory=ServerTestDefault, host=bind_host, port=unused_tcp_port
    ):
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            writer.write(IAC + WILL + TTYPE)
            srv_instance = await asyncio.wait_for(_waiter, 0.5)

            pending = srv_instance.writer.pending_option
            assert WILL + SGA in pending
            assert pending[WILL + SGA] is True


@pytest.mark.parametrize("use_eof", [False, True])
async def test_telnet_server_disconnect_by_client(bind_host, unused_tcp_port, use_eof):
    """Exercise TelnetServer.connection_lost and eof_received."""
    async with create_server(host=bind_host, port=unused_tcp_port) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            expect_hello = IAC + DO + TTYPE
            hello = await reader.readexactly(len(expect_hello))
            assert hello == expect_hello
            writer.write(IAC + WONT + TTYPE)

            srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5)

            assert srv_instance.writer.remote_option[TTYPE] is False
            assert srv_instance.writer.pending_option.get(TTYPE) is not True

            if use_eof:
                writer.write_eof()
            else:
                writer.close()
                await writer.wait_closed()

            await asyncio.sleep(0.05)
            assert srv_instance._closing


async def test_telnet_server_closed_by_server(bind_host, unused_tcp_port):
    """Exercise TelnetServer.connection_lost by close()."""
    async with create_server(host=bind_host, port=unused_tcp_port) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            expect_hello = IAC + DO + TTYPE
            hello_reply = IAC + WONT + TTYPE + b"quit" + b"\r\n"

            hello = await reader.readexactly(len(expect_hello))
            assert hello == expect_hello

            writer.write(hello_reply)
            srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5)

            assert srv_instance.writer.remote_option[TTYPE] is False
            assert srv_instance.writer.pending_option.get(TTYPE) is not True

            data = await asyncio.wait_for(srv_instance.reader.readline(), 0.5)
            assert data == "quit\r\n"

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

            await asyncio.sleep(0.05)
            assert srv_instance._closing


async def test_telnet_server_idle_duration(bind_host, unused_tcp_port):
    """Exercise TelnetServer.idle property."""
    async with create_server(host=bind_host, port=unused_tcp_port) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            writer.write(IAC + WONT + TTYPE)
            srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5)

            assert 0 <= srv_instance.idle <= 0.5
            assert 0 <= srv_instance.duration <= 0.5


async def test_telnet_client_idle_duration_minwait(bind_host, unused_tcp_port):
    """Exercise TelnetClient.idle property and minimum connection time."""
    async with asyncio_server(asyncio.Protocol, bind_host, unused_tcp_port):
        given_minwait = 0.100

        stime = time.time()
        async with open_connection(
            host=bind_host,
            port=unused_tcp_port,
            connect_minwait=given_minwait,
            connect_maxwait=given_minwait,
        ) as (reader, writer):
            elapsed_ms = int((time.time() - stime) * 1e3)
            expected_ms = int(given_minwait * 1e3)
            assert expected_ms <= elapsed_ms <= expected_ms + 150

            assert 0 <= writer.protocol.idle <= 0.5
            assert 0 <= writer.protocol.duration <= 0.5


async def test_telnet_server_closed_by_error(bind_host, unused_tcp_port):
    """Exercise TelnetServer.connection_lost by exception."""
    async with create_server(host=bind_host, port=unused_tcp_port) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            writer.write(IAC + WONT + TTYPE)
            srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5)

            class CustomException(Exception):
                pass

            srv_instance.writer.write("Bye!")
            srv_instance.connection_lost(CustomException("blah!"))

            with pytest.raises(CustomException):
                await srv_instance.reader.read()


async def test_telnet_client_open_close_by_error(bind_host, unused_tcp_port):
    """Exercise BaseClient.connection_lost() on error."""

    class GivenException(Exception):
        pass

    async with asyncio_server(asyncio.Protocol, bind_host, unused_tcp_port):
        async with open_connection(host=bind_host, port=unused_tcp_port) as (reader, writer):
            writer.protocol.connection_lost(GivenException("candy corn 4 everyone"))
            with pytest.raises(GivenException):
                await reader.read()


async def test_telnet_server_negotiation_fail(bind_host, unused_tcp_port):
    """Test telnetlib3.TelnetServer() negotiation failure with client."""
    async with create_server(host=bind_host, port=unused_tcp_port, connect_maxwait=0.5) as server:
        async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
            await reader.readexactly(3)  # IAC DO TTYPE, we ignore it!

            srv_instance = await asyncio.wait_for(server.wait_for_client(), 1.0)

            assert srv_instance.negotiation_should_advance() is False
            assert srv_instance.writer.pending_option[DO + TTYPE]

            assert repr(srv_instance.writer) == (
                "<TelnetWriter server "
                "mode:local +lineflow -xon_any +slc_sim "
                "failed-reply:DO TTYPE>"
            )


async def test_telnet_client_negotiation_fail(bind_host, unused_tcp_port):
    """Test telnetlib3.TelnetCLient() negotiation failure with server."""

    class ClientNegotiationFail(telnetlib3.TelnetClient):
        def connection_made(self, transport):
            super().connection_made(transport)
            self.writer.iac(WILL, TTYPE)

    async with asyncio_server(asyncio.Protocol, bind_host, unused_tcp_port):
        given_minwait = 0.05
        given_maxwait = 0.100

        stime = time.time()
        async with open_connection(
            client_factory=ClientNegotiationFail,
            host=bind_host,
            port=unused_tcp_port,
            connect_minwait=given_minwait,
            connect_maxwait=given_maxwait,
        ) as (reader, writer):
            elapsed_ms = int((time.time() - stime) * 1e3)
            expected_ms = int(given_maxwait * 1e3)
            assert expected_ms <= elapsed_ms <= expected_ms + 150


async def test_telnet_server_as_module():
    """Test __main__ hook, when executing python -m telnetlib3.server --help."""
    prog = sys.executable
    args = [prog, "-m", "telnetlib3.server", "--help"]
    proc = await asyncio.create_subprocess_exec(
        *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
    )

    help_output, _ = await proc.communicate()
    assert b"usage:" in help_output and b"server" in help_output
    await proc.wait()


@pytest.mark.skipif(sys.platform == "win32", reason="Signal handlers not supported on Windows")
async def test_telnet_server_cmdline(bind_host, unused_tcp_port):
    """Test executing telnetlib3/server.py as server."""
    prog = pexpect.which("telnetlib3-server")
    args = [prog, bind_host, str(unused_tcp_port), "--loglevel=info", "--connect-maxwait=0.05"]
    proc = await asyncio.create_subprocess_exec(*args, stderr=asyncio.subprocess.PIPE)

    seen = b""
    while True:
        line = await asyncio.wait_for(proc.stderr.readline(), 1.5)
        if b"Server ready" in line:
            break
        seen += line
        assert line, seen.decode()

    async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer):
        await reader.readexactly(3)  # IAC DO TTYPE, we ignore it!

    seen = b""
    while True:
        line = await asyncio.wait_for(proc.stderr.readline(), 1.5)
        if b"Connection closed" in line:
            break
        seen += line
        assert line, seen.decode()

    proc.terminate()

    await proc.communicate()
    await proc.wait()


async def test_telnet_client_as_module():
    """Test __main__ hook, when executing python -m telnetlib3.client --help."""
    prog = sys.executable
    args = [prog, "-m", "telnetlib3.client", "--help"]
    proc = await asyncio.create_subprocess_exec(
        *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
    )

    help_output, _ = await proc.communicate()
    assert b"usage:" in help_output and b"client" in help_output
    await proc.wait()


@pytest.mark.skipif(sys.platform == "win32", reason="Client shell not implemented on Windows")
async def test_telnet_client_cmdline(bind_host, unused_tcp_port):
    """Test executing telnetlib3/client.py as client."""
    prog = pexpect.which("telnetlib3-client")
    args = [prog, bind_host, str(unused_tcp_port), "--loglevel=info", "--connect-maxwait=0.05"]

    class HelloServer(asyncio.Protocol):
        def connection_made(self, transport):
            super().connection_made(transport)
            transport.write(b"hello, space cadet.\r\n")
            asyncio.get_event_loop().call_soon(transport.close)

    async with asyncio_server(HelloServer, bind_host, unused_tcp_port):
        proc = await asyncio.create_subprocess_exec(
            *args,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        line = await asyncio.wait_for(proc.stdout.readline(), 1.5)
        assert line.strip() == b"Escape character is '^]'."

        line = await asyncio.wait_for(proc.stdout.readline(), 1.5)
        assert line.strip() == b"hello, space cadet."

        out, err = await asyncio.wait_for(proc.communicate(), 1)
        assert out == b"\x1b[m\nConnection closed by foreign host.\n"
        stderr_text = err.decode()
        assert "Connected to <Peer" in stderr_text
        assert "Connection closed to <Peer" in stderr_text


@pytest.mark.skipif(sys.platform == "win32", reason="pexpect.spawn requires Unix PTY")
@pytest.mark.skipif(
    tuple(map(int, platform.python_version_tuple()[:2])) > (3, 10),
    reason="those shabby pexpect maintainers still use @asyncio.coroutine",
)
async def test_telnet_client_tty_cmdline(bind_host, unused_tcp_port):
    """Test executing telnetlib3/client.py as client using a tty (pexpect)"""
    prog, args = "telnetlib3-client", [
        bind_host,
        str(unused_tcp_port),
        "--loglevel=warning",
        "--connect-maxwait=0.05",
    ]

    class HelloServer(asyncio.Protocol):
        def connection_made(self, transport):
            super().connection_made(transport)
            transport.write(b"hello, space cadet.\r\n")
            asyncio.get_event_loop().call_soon(transport.close)

    async with asyncio_server(HelloServer, bind_host, unused_tcp_port):
        proc = pexpect.spawn(prog, args)
        await proc.expect(pexpect.EOF, async_=True, timeout=5)
        normalized = proc.before.replace(b"\r\r\n", b"\r\n")
        assert normalized == (
            b"Escape character is '^]'.\r\n"
            b"hello, space cadet.\r\n"
            b"\x1b[m\r\n"
            b"Connection closed by foreign host.\r\n"
        )


@pytest.mark.skipif(sys.platform == "win32", reason="Client shell not implemented on Windows")
async def test_telnet_client_cmdline_stdin_pipe(bind_host, unused_tcp_port):
    """Test sending data through command-line client (by os PIPE)."""
    prog = pexpect.which("telnetlib3-client")
    fd, logfile = tempfile.mkstemp(prefix="telnetlib3", suffix=".log")
    os.close(fd)

    args = [
        prog,
        bind_host,
        str(unused_tcp_port),
        "--loglevel=info",
        "--connect-maxwait=0.15",
        f"--logfile={logfile}",
    ]

    async def shell(reader, writer):
        writer.write("Press Return to continue:")
        inp = await reader.readline()
        if inp:
            writer.write("\ngoodbye.\n")
        await writer.drain()
        writer.close()
        await writer.wait_closed()

    async with create_server(
        host=bind_host, port=unused_tcp_port, shell=shell, connect_maxwait=0.5
    ):
        proc = await asyncio.create_subprocess_exec(
            *args,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        stdout, stderr = await asyncio.wait_for(proc.communicate(b"\r"), 2)

        with open(logfile, encoding="utf-8") as f:
            logfile_output = f.read().splitlines()
        assert stdout == (
            b"Escape character is '^]'.\n"
            b"Press Return to continue:\ngoodbye.\n"
            b"\x1b[m\nConnection closed by foreign host.\n"
        )
        assert len(logfile_output) in (2, 3), logfile
        assert "Connected to <Peer" in logfile_output[0], logfile
        assert any("Connection closed to <Peer" in line for line in logfile_output[1:]), logfile
        os.unlink(logfile)
