"""Tests for reply.py – Reply and ListenReply."""

from __future__ import annotations

import threading
import time

from routeros.errors import DeviceError, UnknownReplyError
from routeros.reply import ListenReply, Reply
from routeros.sentence import Sentence

# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------


def make_sentence(word: str, **kv) -> Sentence:
    words = [word] + [f"={k}={v}" for k, v in kv.items()]
    return Sentence.from_words(words)


# ---------------------------------------------------------------------------
# Reply state machine
# ---------------------------------------------------------------------------


class TestReplyProcessSentence:
    def test_re_appended(self):
        r = Reply()
        done, err = r.process_sentence(make_sentence("!re", address="1.2.3.4"))
        assert not done
        assert err is None
        assert len(r.re) == 1
        assert r.re[0].map["address"] == "1.2.3.4"

    def test_done_completes(self):
        r = Reply()
        done, err = r.process_sentence(make_sentence("!done"))
        assert done
        assert err is None
        assert r.done is not None

    def test_trap_not_done_error(self):
        r = Reply()
        done, err = r.process_sentence(make_sentence("!trap", message="bad cmd"))
        assert not done
        assert isinstance(err, DeviceError)
        assert "bad cmd" in str(err)

    def test_fatal_done_with_error(self):
        r = Reply()
        done, err = r.process_sentence(make_sentence("!fatal", message="fatal"))
        assert done
        assert isinstance(err, DeviceError)

    def test_empty_ignored(self):
        r = Reply()
        done, err = r.process_sentence(make_sentence(""))
        assert not done
        assert err is None

    def test_empty_word_ignored(self):
        r = Reply()
        done, err = r.process_sentence(make_sentence("!empty"))
        assert not done
        assert err is None

    def test_unknown_word(self):
        r = Reply()
        sen = Sentence(word="!unknown")
        done, err = r.process_sentence(sen)
        assert done
        assert isinstance(err, UnknownReplyError)

    def test_multiple_re_then_done(self):
        r = Reply()
        for i in range(3):
            r.process_sentence(make_sentence("!re", n=str(i)))
        done, err = r.process_sentence(make_sentence("!done"))
        assert done and err is None
        assert len(r.re) == 3

    def test_str(self):
        r = Reply()
        r.process_sentence(make_sentence("!re", a="1"))
        r.process_sentence(make_sentence("!done"))
        assert "!re" in str(r)
        assert "!done" in str(r)


# ---------------------------------------------------------------------------
# ListenReply
# ---------------------------------------------------------------------------


class _FakeClient:
    """Minimal stand-in for Client used by ListenReply.cancel()."""

    def __init__(self):
        self.cancel_called_with: list = []

    def run_args(self, words):
        self.cancel_called_with.extend(words)
        return Reply()


class TestListenReply:
    def test_re_placed_on_queue(self):
        lr = ListenReply(_FakeClient(), tag="l1")
        sen = make_sentence("!re", x="1")
        done, err = lr.process_sentence(sen)
        assert not done and err is None
        item = lr.chan().get_nowait()
        assert item.map["x"] == "1"

    def test_done_closes_stream(self):
        lr = ListenReply(_FakeClient(), tag="l1")
        lr.process_sentence(make_sentence("!done"))
        lr.close(None)
        items = list(lr)  # should yield nothing after sentinel
        assert items == []
        assert lr.done is not None

    def test_iter_yields_re_sentences(self):
        lr = ListenReply(_FakeClient(), tag="l1")

        def _feed():
            time.sleep(0.01)
            lr.process_sentence(make_sentence("!re", n="1"))
            lr.process_sentence(make_sentence("!re", n="2"))
            lr.close(None)

        threading.Thread(target=_feed, daemon=True).start()
        results = list(lr)
        assert len(results) == 2
        assert [s.map["n"] for s in results] == ["1", "2"]

    def test_cancel_calls_client(self):
        fc = _FakeClient()
        lr = ListenReply(fc, tag="l5")
        lr.cancel()
        assert "/cancel" in fc.cancel_called_with
        assert "=tag=l5" in fc.cancel_called_with

    def test_err_stored_on_close(self):
        lr = ListenReply(_FakeClient(), tag="l1")
        exc = RuntimeError("test")
        lr.close(exc)
        assert lr.err is exc

    def test_category_2_trap_is_normal_cancel(self):
        """!trap with category=2 means 'interrupted' – not an error."""
        lr = ListenReply(_FakeClient(), tag="l1")
        done, err = lr.process_sentence(
            make_sentence("!trap", category="2", message="interrupted")
        )
        assert done and err is None
        assert lr.done is not None

    def test_trap_without_category_2_is_error(self):
        lr = ListenReply(_FakeClient(), tag="l1")
        done, err = lr.process_sentence(make_sentence("!trap", message="oops"))
        assert done
        assert isinstance(err, DeviceError)
