# no unicode_literals until twisted update
from click.testing import CliRunner
from twisted.application import internet, service
from twisted.internet import defer, endpoints, reactor, task, protocol
from twisted.python import log

from unittest import mock
from wormhole_mailbox_server.database import create_channel_db, create_usage_db
from wormhole_mailbox_server.server import make_server
from wormhole_mailbox_server.web import make_web_server
from wormhole_transit_relay.transit_server import Transit, TransitConnection
from wormhole_transit_relay.usage import create_usage_tracker

from ..cli import cli
from ..transit import allocate_tcp_port


class MyInternetService(service.Service, object):
    # like StreamServerEndpointService, but you can retrieve the port
    def __init__(self, endpoint, factory):
        self.endpoint = endpoint
        self.factory = factory
        self._port_d = defer.Deferred()
        self._lp = None

    def startService(self):
        super(MyInternetService, self).startService()
        d = self.endpoint.listen(self.factory)

        def good(lp):
            self._lp = lp
            self._port_d.callback(lp.getHost().port)

        def bad(f):
            log.err(f)
            self._port_d.errback(f)

        d.addCallbacks(good, bad)

    @defer.inlineCallbacks
    def stopService(self):
        if self._lp:
            yield self._lp.stopListening()

    def getPort(self):  # only call once!
        return self._port_d


class ServerBase:
    @defer.inlineCallbacks
    def setUp(self):
        yield self._setup_relay(None)

    @defer.inlineCallbacks
    def _setup_relay(self, error, advertise_version=None):
        self.sp = service.MultiService()
        self.sp.startService()
        # need to talk to twisted team about only using unicode in
        # endpoints.serverFromString
        db = create_channel_db(":memory:")
        self._usage_db = create_usage_db(":memory:")
        self._rendezvous = make_server(
            db,
            advertise_version=advertise_version,
            signal_error=error,
            usage_db=self._usage_db)
        ep = endpoints.TCP4ServerEndpoint(reactor, 0, interface="127.0.0.1")
        site = make_web_server(self._rendezvous, log_requests=False)
        # self._lp = yield ep.listen(site)
        s = MyInternetService(ep, site)
        s.setServiceParent(self.sp)
        self.rdv_ws_port = yield s.getPort()
        self._relay_server = s
        # self._rendezvous = s._rendezvous
        self.relayurl = u"ws://127.0.0.1:%d/v1" % self.rdv_ws_port
        # ws://127.0.0.1:%d/wormhole-relay/ws

        self.transitport = allocate_tcp_port()
        ep = endpoints.serverFromString(
            reactor, "tcp:%d:interface=127.0.0.1" % self.transitport)

        usage = create_usage_tracker(blur_usage=None, log_file=None, usage_db=None)
        self._transit_server = protocol.ServerFactory()
        self._transit_server.protocol = TransitConnection
        self._transit_server.log_requests = False
        self._transit_server.transit = Transit(usage, reactor.seconds)


        internet.StreamServerEndpointService(ep, self._transit_server).setServiceParent(self.sp)
        self.transit = u"tcp:127.0.0.1:%d" % self.transitport

    @defer.inlineCallbacks
    def tearDown(self):
        # Unit tests that spawn a (blocking) client in a thread might still
        # have threads running at this point, if one is stuck waiting for a
        # message from a companion which has exited with an error. Our
        # relay's .stopService() drops all connections, which ought to
        # encourage those threads to terminate soon. If they don't, print a
        # warning to ease debugging.

        # XXX FIXME there's something in _noclobber test that's not
        # waiting for a close, I think -- was pretty relieably getting
        # unclean-reactor, but adding a slight pause here stops it...

        tp = reactor.getThreadPool()
        if not tp.working:
            yield self.sp.stopService()
            yield task.deferLater(reactor, 0.1, lambda: None)
            defer.returnValue(None)
        # disconnect all callers
        d = defer.maybeDeferred(self.sp.stopService)
        d.addBoth(lambda _: self._transit_server.stopFactory())
        # wait a second, then check to see if it worked
        yield task.deferLater(reactor, 1.0, lambda: None)
        if len(tp.working):
            log.msg("wormhole.test.common.ServerBase.tearDown:"
                    " I was unable to convince all threads to exit.")
            tp.dumpStats()
            print("tearDown warning: threads are still active")
            print("This test will probably hang until one of the"
                  " clients gives up of their own accord.")
        else:
            log.msg("wormhole.test.common.ServerBase.tearDown:"
                    " I convinced all threads to exit.")
        yield d


def config(*argv):
    r = CliRunner()
    with mock.patch("wormhole.cli.cli.go") as go:
        res = r.invoke(cli.wormhole, argv, catch_exceptions=False)
        if res.exit_code != 0:
            print(res.exit_code)
            print(res.output)
            print(res)
            assert 0
        cfg = go.call_args[0][1]
    return cfg


@defer.inlineCallbacks
def poll_until(predicate):
    # return a Deferred that won't fire until the predicate is True
    while not predicate():
        d = defer.Deferred()
        reactor.callLater(0.001, d.callback, None)
        yield d
