# noqa:D100
from __future__ import annotations

import anyio
import copy
import logging
import time
from contextlib import AsyncExitStack, asynccontextmanager, suppress
from functools import partial
from unittest import mock

import trio
from asyncscope import main_scope, scope

from moat.util import NotGiven, attrdict, combine_dict
from moat.kv.mock import S
from moat.kv.server import Server
from moat.mqtt.broker import create_broker

from . import CFG

logger = logging.getLogger(__name__)

otm = time.time


@asynccontextmanager
async def stdtest(n=1, run=True, ssl=False, tocks=20, **kw):  # noqa:D103
    C_OUT = CFG.get("_stdout", NotGiven)
    if C_OUT is not NotGiven:
        del CFG["_stdout"]
    TESTCFG = copy.deepcopy(CFG["kv"])
    TESTCFG.server.port = None
    TESTCFG.root = "test"
    if C_OUT is not NotGiven:
        TESTCFG["_stdout"] = C_OUT

    from anyio.pytest_plugin import FreePortFactory  # noqa: PLC0415
    from socket import SOCK_STREAM  # noqa: PLC0415

    PORT = FreePortFactory(SOCK_STREAM)()
    broker_cfg = {
        "listeners": {"default": {"type": "tcp", "bind": f"127.0.0.1:{PORT}"}},
        "timeout-disconnect-delay": 2,
        "auth": {"allow-anonymous": True, "password-file": None},
    }
    URI = f"mqtt://127.0.0.1:{PORT}/"

    if ssl:
        import ssl  # noqa:PLC0415,I001
        import trustme  # noqa: PLC0415

        ca = trustme.CA()
        cert = ca.issue_server_cert("127.0.0.1")
        server_ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
        client_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
        ca.configure_trust(client_ctx)
        cert.configure_cert(server_ctx)
    else:
        server_ctx = client_ctx = False

    clock = trio.lowlevel.current_clock()
    with suppress(Exception):
        clock.autojump_threshold = 0.02  # networking

    async def mock_get_host_port(st, host):
        i = int(host[host.rindex("_") + 1 :])
        s = st.s[i]
        await s.is_serving
        for host, port, *_ in s.ports:
            if host == "::" or host[0] != ":":
                return host, port

    def tm():
        try:
            return trio.current_time()
        except RuntimeError:
            return otm()

    async def mock_set_tock(self, old):
        assert self._tock < tocks, "Test didn't terminate. Limit:" + str(tocks)
        await old()

    done = False
    async with main_scope("moat.kv.test.mqtt") as scp:
        tg = scp._tg  # noqa:SLF001
        st = S(tg, client_ctx)
        async with AsyncExitStack() as ex:
            st.ex = ex  # pylint: disable=attribute-defined-outside-init
            ex.enter_context(mock.patch("time.time", new=tm))
            ex.enter_context(mock.patch("time.monotonic", new=tm))
            logging._startTime = tm()  # noqa:SLF001

            async def run_broker(cfg):
                async with create_broker(config=cfg) as srv:
                    # NB: some services use "async with await …"
                    scope.register(srv)
                    await scope.no_more_dependents()

            async def with_broker(s, *a, **k):
                await scope.service("moat.mqtt.broker", run_broker, broker_cfg)
                s._scope = scope.get()  # noqa:SLF001
                return await s._scoped_serve(*a, **k)  # noqa:SLF001

            args_def = kw.get("args", attrdict())
            for i in range(n):
                name = "test_" + str(i)
                args = kw.get(name, args_def)
                args = combine_dict(
                    args,
                    args_def,
                    {
                        "cfg": {
                            "conn": {"ssl": client_ctx},
                            "server": {
                                "bind_default": {
                                    "host": "127.0.0.1",
                                    "port": i + PORT + 1,
                                    "ssl": server_ctx,
                                },
                                "backend": "mqtt",
                                "mqtt": {"uri": URI},
                            },
                        },
                    },
                    {"cfg": TESTCFG},
                )
                args_def.pop("init", None)
                s = Server(name, **args)
                ex.enter_context(
                    mock.patch.object(s, "_set_tock", new=partial(mock_set_tock, s, s._set_tock)),  # noqa:SLF001
                )
                ex.enter_context(
                    mock.patch.object(s, "_get_host_port", new=partial(mock_get_host_port, st)),
                )
                st.s.append(s)

            evts = []
            for i in range(n):
                if kw.get(f"run_{i}", run):
                    evt = anyio.Event()
                    await scp.spawn_service(with_broker, st.s[i], ready_evt=evt)
                    evts.append(evt)
                else:
                    setattr(st, f"run_{i}", partial(scp.spawn_service, with_broker, st.s[i]))

            for e in evts:
                await e.wait()
            try:
                done = True
                yield st
            finally:
                logger.info("Runtime: %s", clock.current_time())
                tg.cancel_scope.cancel()
    if not done:
        yield None
