"""Full client tests using real in-process socket pairs."""

from __future__ import annotations

import hashlib
import threading
import time

import pytest

from routeros.client import Client
from routeros.errors import (
    AlreadyAsyncError,
    DeviceError,
    UnknownReplyError,
)
from tests.unit.conftest import run_server

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_client(sock) -> Client:
    return Client(sock)


# ---------------------------------------------------------------------------
# Synchronous Run
# ---------------------------------------------------------------------------


class TestSyncRun:
    def test_run_returns_re_and_done(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()  # consume command
            srv.write_sentence("!re", "=address=192.168.1.1/24")
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        reply = c.run("/ip/address/print")
        assert len(reply.re) == 1
        assert reply.re[0].map["address"] == "192.168.1.1/24"
        assert reply.done is not None

    def test_run_multiple_re(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()
            for i in range(3):
                srv.write_sentence("!re", f"=n={i}")
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        reply = c.run("/ip/address/print")
        assert len(reply.re) == 3
        assert [s.map["n"] for s in reply.re] == ["0", "1", "2"]

    def test_run_empty_result(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        reply = c.run("/ip/address/print")
        assert reply.re == []

    def test_run_empty_sentence_ignored(self, socket_pair):
        """!empty sentences should be silently ignored."""
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()
            srv.write_sentence("!empty")
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        reply = c.run("/test")
        assert reply.re == []
        assert reply.done is not None

    def test_run_raises_on_fatal(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()
            srv.write_sentence("!fatal", "=message=fatal error")

        run_server(server)
        c = make_client(client_sock)
        with pytest.raises(DeviceError, match="fatal error"):
            c.run("/bad/command")

    def test_run_raises_on_trap(self, socket_pair):
        """!trap followed by !done → raises DeviceError (last_err)."""
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()
            srv.write_sentence("!trap", "=message=no such command")
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        with pytest.raises(DeviceError, match="no such command"):
            c.run("/nonexistent")

    def test_run_raises_on_unknown_word(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            srv.read_sentence()
            # Send an invalid sentence word.
            from routeros.proto import Writer
            w = Writer(srv._sock)
            w.write_sentence(["!bogus"])

        run_server(server)
        c = make_client(client_sock)
        with pytest.raises(UnknownReplyError):
            c.run("/test")

    def test_run_args(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            sen = srv.read_sentence()
            assert sen.word == "/ip/address/print"
            assert sen.map.get(".proplist") == "address"
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        c.run_args(["/ip/address/print", "=.proplist=address"])


# ---------------------------------------------------------------------------
# Async Run
# ---------------------------------------------------------------------------


class TestAsyncRun:
    def test_async_run_basic(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            # Read command sent by client.
            sen = srv.read_sentence()
            tag = sen.tag
            srv.write_sentence("!re", f".tag={tag}", "=x=hello")
            srv.write_sentence("!done", f".tag={tag}")

        run_server(server)
        c = make_client(client_sock)
        c.async_start()
        reply = c.run("/test")
        assert reply.re[0].map["x"] == "hello"

    def test_async_run_multiple_concurrent(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            # Read both commands, capture their tags.
            s1 = srv.read_sentence()
            s2 = srv.read_sentence()
            # Respond in reverse order to prove tag routing works.
            srv.write_sentence("!re", f".tag={s2.tag}", "=n=2")
            srv.write_sentence("!done", f".tag={s2.tag}")
            srv.write_sentence("!re", f".tag={s1.tag}", "=n=1")
            srv.write_sentence("!done", f".tag={s1.tag}")

        run_server(server)
        c = make_client(client_sock)
        c.async_start()

        results = []

        def do_run(word):
            r = c.run(word)
            results.append(r.re[0].map["n"])

        t1 = threading.Thread(target=do_run, args=("/cmd1",))
        t2 = threading.Thread(target=do_run, args=("/cmd2",))
        t1.start(); t2.start()
        t1.join(timeout=5); t2.join(timeout=5)

        assert sorted(results) == ["1", "2"]

    def test_async_start_twice_raises(self, socket_pair):
        client_sock, _ = socket_pair
        c = make_client(client_sock)
        c.async_start()
        with pytest.raises(AlreadyAsyncError):
            c.async_start()


# ---------------------------------------------------------------------------
# Login – post-6.43 (single stage)
# ---------------------------------------------------------------------------


class TestLoginPostV643:
    def test_successful_login(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            sen = srv.read_sentence()
            assert sen.word == "/login"
            assert sen.map["name"] == "admin"
            assert sen.map["password"] == "secret"
            # No 'ret' field → post-6.43 single stage
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        c._login("admin", "secret")


# ---------------------------------------------------------------------------
# Login – pre-6.43 (two stage, MD5 challenge)
# ---------------------------------------------------------------------------


class TestLoginPreV643:
    def _make_challenge(self, password: str) -> tuple[str, str]:
        """Return (hex_challenge, expected_response)."""
        challenge = b"\xab\xcd\xef\x01\x23\x45\x67\x89" * 2  # 16-byte fake challenge
        hex_challenge = challenge.hex()
        h = hashlib.md5()
        h.update(b"\x00")
        h.update(password.encode())
        h.update(challenge)
        expected = "00" + h.hexdigest()
        return hex_challenge, expected

    def test_md5_challenge_response(self, socket_pair):
        client_sock, srv = socket_pair
        password = "admin123"
        hex_challenge, expected_response = self._make_challenge(password)

        def server():
            # Stage 1: receive /login, send challenge
            srv.read_sentence()
            srv.write_sentence("!done", f"=ret={hex_challenge}")
            # Stage 2: receive /login with response
            sen2 = srv.read_sentence()
            assert sen2.map.get("response") == expected_response
            srv.write_sentence("!done")

        run_server(server)
        c = make_client(client_sock)
        c._login("admin", password)


# ---------------------------------------------------------------------------
# Listener
# ---------------------------------------------------------------------------


class TestListener:
    def test_listen_streams_re_sentences(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            sen = srv.read_sentence()
            tag = sen.tag
            for i in range(3):
                srv.write_sentence("!re", f".tag={tag}", f"=n={i}")
            srv.write_sentence("!done", f".tag={tag}")

        run_server(server)
        c = make_client(client_sock)
        lr = c.listen("/test/listen")

        # Drain the listener.
        received = []
        for sentence in lr:
            received.append(sentence.map["n"])

        assert received == ["0", "1", "2"]
        assert lr.done is not None

    def test_listen_cancel(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            # First command: the listen command
            listen_sen = srv.read_sentence()
            listen_tag = listen_sen.tag

            # Send a few !re sentences
            for i in range(2):
                srv.write_sentence("!re", f".tag={listen_tag}", f"=n={i}")

            # Wait for /cancel command
            cancel_sen = srv.read_sentence()
            assert cancel_sen.word == "/cancel"

            # Respond to the cancel command
            cancel_tag = cancel_sen.tag
            srv.write_sentence("!done", f".tag={cancel_tag}")

            # Close the listen stream with category=2 trap
            srv.write_sentence(
                "!trap", f".tag={listen_tag}", "=category=2", "=message=interrupted"
            )

        run_server(server)
        c = make_client(client_sock)
        lr = c.listen("/test/stream")

        # Consume the two !re sentences (best-effort peek via CPython internals)
        if hasattr(lr.chan(), "queue"):
            next(iter(lr.chan().queue), None)

        # Cancel the stream
        lr.cancel()

        # The listener should close
        list(lr)  # drain remaining items
        assert lr.done is not None

    def test_listen_auto_enables_async(self, socket_pair):
        client_sock, srv = socket_pair

        def server():
            sen = srv.read_sentence()
            tag = sen.tag
            srv.write_sentence("!done", f".tag={tag}")

        run_server(server)
        c = make_client(client_sock)
        assert not c._async
        lr = c.listen("/test")
        assert c._async

        # Drain
        list(lr)


# ---------------------------------------------------------------------------
# Context manager
# ---------------------------------------------------------------------------


class TestContextManager:
    def test_with_statement_closes(self, socket_pair):
        client_sock, srv = socket_pair
        with make_client(client_sock) as c:
            assert not c._closing
        assert c._closing


# ---------------------------------------------------------------------------
# Connection close behaviour
# ---------------------------------------------------------------------------


class TestConnectionClose:
    def test_close_unblocks_async_waiters(self, socket_pair):
        """Closing the socket should unblock any pending async waiter."""
        client_sock, srv = socket_pair
        c = make_client(client_sock)

        errors = []

        def do_run():
            try:
                c.async_start()
                c.run("/long/command")
            except Exception as exc:
                errors.append(exc)

        # Start async run in background thread – it will block waiting for reply.
        t = threading.Thread(target=do_run, daemon=True)
        t.start()

        # Give run a moment to register the tag.
        time.sleep(0.05)

        # Server closes the connection.
        srv.close()

        t.join(timeout=3)
        # The thread must have exited (either with an error or cleanly).
        assert not t.is_alive()
