from __future__ import print_function
import sys, json
import requests
from six.moves.urllib_parse import urlencode
from twisted.trial import unittest
from twisted.internet import reactor, defer
from twisted.internet.threads import deferToThread
from twisted.web.client import getPage, Agent, readBody
from .. import __version__
from .common import ServerBase
from ..servers import relay_server
from ..twisted.eventsource_twisted import EventSource

class Reachable(ServerBase, unittest.TestCase):

    def test_getPage(self):
        # client.getPage requires str/unicode URL, returns bytes
        url = self.relayurl.replace("wormhole-relay/", "").encode("ascii")
        d = getPage(url)
        def _got(res):
            self.failUnlessEqual(res, b"Wormhole Relay\n")
        d.addCallback(_got)
        return d

    def test_agent(self):
        # client.Agent is not yet ported: it wants URLs to be both unicode
        # and bytes at the same time.
        # https://twistedmatrix.com/trac/ticket/7407
        if sys.version_info[0] > 2:
            raise unittest.SkipTest("twisted.web.client.Agent does not yet support py3")
        url = self.relayurl.replace("wormhole-relay/", "").encode("ascii")
        agent = Agent(reactor)
        d = agent.request("GET", url)
        def _check(resp):
            self.failUnlessEqual(resp.code, 200)
            return readBody(resp)
        d.addCallback(_check)
        def _got(res):
            self.failUnlessEqual(res, b"Wormhole Relay\n")
        d.addCallback(_got)
        return d

    def test_requests(self):
        # requests requires bytes URL, returns unicode
        url = self.relayurl.replace("wormhole-relay/", "")
        def _get(url):
            r = requests.get(url)
            r.raise_for_status()
            return r.text
        d = deferToThread(_get, url)
        def _got(res):
            self.failUnlessEqual(res, "Wormhole Relay\n")
        d.addCallback(_got)
        return d

def unjson(data):
    return json.loads(data.decode("utf-8"))

class API(ServerBase, unittest.TestCase):
    def build_url(self, path, appid, channelid):
        url = self.relayurl+path
        queryargs = []
        if appid:
            queryargs.append(("appid", appid))
        if channelid:
            queryargs.append(("channelid", channelid))
        if queryargs:
            url += "?" + urlencode(queryargs)
        return url

    def get(self, path, appid=None, channelid=None):
        url = self.build_url(path, appid, channelid)
        d = getPage(url.encode("ascii"))
        d.addCallback(unjson)
        return d

    def post(self, path, data):
        url = self.relayurl+path
        d = getPage(url.encode("ascii"), method=b"POST",
                    postdata=json.dumps(data).encode("utf-8"))
        d.addCallback(unjson)
        return d

    def check_welcome(self, data):
        self.failUnlessIn("welcome", data)
        self.failUnlessEqual(data["welcome"], {"current_version": __version__})

    def test_allocate_1(self):
        d = self.get("list", "app1")
        def _check_list_1(data):
            self.check_welcome(data)
            self.failUnlessEqual(data["channelids"], [])
        d.addCallback(_check_list_1)

        d.addCallback(lambda _: self.post("allocate", {"appid": "app1",
                                                       "side": "abc"}))
        def _allocated(data):
            self.failUnlessEqual(set(data.keys()),
                                 set(["welcome", "channelid"]))
            self.failUnlessIsInstance(data["channelid"], int)
            self.cid = data["channelid"]
        d.addCallback(_allocated)

        d.addCallback(lambda _: self.get("list", "app1"))
        def _check_list_2(data):
            self.failUnlessEqual(data["channelids"], [self.cid])
        d.addCallback(_check_list_2)

        d.addCallback(lambda _: self.post("deallocate",
                                          {"appid": "app1",
                                           "channelid": str(self.cid),
                                           "side": "abc"}))
        def _check_deallocate(res):
            self.failUnlessEqual(res["status"], "deleted")
        d.addCallback(_check_deallocate)

        d.addCallback(lambda _: self.get("list", "app1"))
        def _check_list_3(data):
            self.failUnlessEqual(data["channelids"], [])
        d.addCallback(_check_list_3)

        return d

    def test_allocate_2(self):
        d = self.post("allocate", {"appid": "app1", "side": "abc"})
        def _allocated(data):
            self.cid = data["channelid"]
        d.addCallback(_allocated)

        # second caller increases the number of known sides to 2
        d.addCallback(lambda _: self.post("add",
                                          {"appid": "app1",
                                           "channelid": str(self.cid),
                                           "side": "def",
                                           "phase": "1",
                                           "body": ""}))

        d.addCallback(lambda _: self.get("list", "app1"))
        d.addCallback(lambda data:
                      self.failUnlessEqual(data["channelids"], [self.cid]))

        d.addCallback(lambda _: self.post("deallocate",
                                          {"appid": "app1",
                                           "channelid": str(self.cid),
                                           "side": "abc"}))
        d.addCallback(lambda res:
                      self.failUnlessEqual(res["status"], "waiting"))

        d.addCallback(lambda _: self.post("deallocate",
                                          {"appid": "app1",
                                           "channelid": str(self.cid),
                                           "side": "NOT"}))
        d.addCallback(lambda res:
                      self.failUnlessEqual(res["status"], "waiting"))

        d.addCallback(lambda _: self.post("deallocate",
                                          {"appid": "app1",
                                           "channelid": str(self.cid),
                                           "side": "def"}))
        d.addCallback(lambda res:
                      self.failUnlessEqual(res["status"], "deleted"))

        d.addCallback(lambda _: self.get("list", "app1"))
        d.addCallback(lambda data:
                      self.failUnlessEqual(data["channelids"], []))

        return d

    UPGRADE_ERROR = "Sorry, you must upgrade your client to use this server."
    def test_old_allocate(self):
        # 0.4.0 used "POST /allocate/SIDE".
        # 0.5.0 replaced it with "POST /allocate".
        # test that an old client gets a useful error message, not a 404.
        d = self.post("allocate/abc", {})
        def _check(data):
            self.failUnlessEqual(data["welcome"]["error"], self.UPGRADE_ERROR)
        d.addCallback(_check)
        return d

    def test_old_list(self):
        # 0.4.0 used "GET /list".
        # 0.5.0 replaced it with "GET /list?appid="
        d = self.get("list", {}) # no appid
        def _check(data):
            self.failUnlessEqual(data["welcome"]["error"], self.UPGRADE_ERROR)
        d.addCallback(_check)
        return d

    def test_old_post(self):
        # 0.4.0 used "POST /CID/SIDE/post/MSGNUM"
        # 0.5.0 replaced it with "POST /add (json body)"
        d = self.post("1/abc/post/pake", {})
        def _check(data):
            self.failUnlessEqual(data["welcome"]["error"], self.UPGRADE_ERROR)
        d.addCallback(_check)
        return d

    def add_message(self, message, side="abc", phase="1"):
        return self.post("add",
                         {"appid": "app1",
                          "channelid": str(self.cid),
                         "side": side,
                          "phase": phase,
                          "body": message})

    def parse_messages(self, messages):
        out = set()
        for m in messages:
            self.failUnlessEqual(sorted(m.keys()), sorted(["phase", "body"]))
            self.failUnlessIsInstance(m["phase"], type(u""))
            self.failUnlessIsInstance(m["body"], type(u""))
            out.add( (m["phase"], m["body"]) )
        return out

    def check_messages(self, one, two):
        # Comparing lists-of-dicts is non-trivial in python3 because we can
        # neither sort them (dicts are uncomparable), nor turn them into sets
        # (dicts are unhashable). This is close enough.
        self.failUnlessEqual(len(one), len(two), (one,two))
        for d in one:
            self.failUnlessIn(d, two)

    def test_message(self):
        # exercise POST /add
        d = self.post("allocate", {"appid": "app1", "side": "abc"})
        def _allocated(data):
            self.cid = data["channelid"]
        d.addCallback(_allocated)

        d.addCallback(lambda _: self.add_message("msg1A"))
        def _check1(data):
            self.check_welcome(data)
            self.failUnlessEqual(data["messages"],
                                 [{"phase": "1", "body": "msg1A"}])
        d.addCallback(_check1)
        d.addCallback(lambda _: self.get("get", "app1", str(self.cid)))
        d.addCallback(_check1)
        d.addCallback(lambda _: self.add_message("msg1B", side="def"))
        def _check2(data):
            self.check_welcome(data)
            self.failUnlessEqual(self.parse_messages(data["messages"]),
                                 set([("1", "msg1A"),
                                      ("1", "msg1B")]))
        d.addCallback(_check2)
        d.addCallback(lambda _: self.get("get", "app1", str(self.cid)))
        d.addCallback(_check2)

        # adding a duplicate message is not an error, is ignored by clients
        d.addCallback(lambda _: self.add_message("msg1B", side="def"))
        def _check3(data):
            self.check_welcome(data)
            self.failUnlessEqual(self.parse_messages(data["messages"]),
                                 set([("1", "msg1A"),
                                      ("1", "msg1B")]))
        d.addCallback(_check3)
        d.addCallback(lambda _: self.get("get", "app1", str(self.cid)))
        d.addCallback(_check3)

        d.addCallback(lambda _: self.add_message("msg2A", side="abc",
                                                 phase="2"))
        def _check4(data):
            self.check_welcome(data)
            self.failUnlessEqual(self.parse_messages(data["messages"]),
                                 set([("1", "msg1A"),
                                      ("1", "msg1B"),
                                      ("2", "msg2A"),
                                      ]))
        d.addCallback(_check4)
        d.addCallback(lambda _: self.get("get", "app1", str(self.cid)))
        d.addCallback(_check4)

        return d

    def test_watch_message(self):
        # exercise GET /get (the EventSource version)
        # this API is scheduled to be removed after 0.6.0
        return self._do_watch("get")

    def test_watch(self):
        # exercise GET /watch (the EventSource version)
        return self._do_watch("watch")

    def _do_watch(self, endpoint_name):
        if sys.version_info[0] >= 3:
            raise unittest.SkipTest("twisted vs py3")

        d = self.post("allocate", {"appid": "app1", "side": "abc"})
        def _allocated(data):
            self.cid = data["channelid"]
            url = self.build_url(endpoint_name, "app1", self.cid)
            self.o = OneEventAtATime(url, parser=json.loads)
            return self.o.wait_for_connection()
        d.addCallback(_allocated)
        d.addCallback(lambda _: self.o.wait_for_next_event())
        def _check_welcome(ev):
            eventtype, data = ev
            self.failUnlessEqual(eventtype, "welcome")
            self.failUnlessEqual(data, {"current_version": __version__})
        d.addCallback(_check_welcome)
        d.addCallback(lambda _: self.add_message("msg1A"))
        d.addCallback(lambda _: self.o.wait_for_next_event())
        def _check_msg1(ev):
            eventtype, data = ev
            self.failUnlessEqual(eventtype, "message")
            self.failUnlessEqual(data, {"phase": "1", "body": "msg1A"})
        d.addCallback(_check_msg1)

        d.addCallback(lambda _: self.add_message("msg1B"))
        d.addCallback(lambda _: self.add_message("msg2A", phase="2"))
        d.addCallback(lambda _: self.o.wait_for_next_event())
        def _check_msg2(ev):
            eventtype, data = ev
            self.failUnlessEqual(eventtype, "message")
            self.failUnlessEqual(data, {"phase": "1", "body": "msg1B"})
        d.addCallback(_check_msg2)
        d.addCallback(lambda _: self.o.wait_for_next_event())
        def _check_msg3(ev):
            eventtype, data = ev
            self.failUnlessEqual(eventtype, "message")
            self.failUnlessEqual(data, {"phase": "2", "body": "msg2A"})
        d.addCallback(_check_msg3)

        d.addCallback(lambda _: self.o.close())
        d.addCallback(lambda _: self.o.wait_for_disconnection())
        return d

class OneEventAtATime:
    def __init__(self, url, parser=lambda e: e):
        self.parser = parser
        self.d = None
        self._connected = False
        self.connected_d = defer.Deferred()
        self.disconnected_d = defer.Deferred()
        self.events = []
        self.es = EventSource(url, self.handler, when_connected=self.connected)
        d = self.es.start()
        d.addBoth(self.disconnected)

    def close(self):
        self.es.cancel()

    def wait_for_next_event(self):
        assert not self.d
        if self.events:
            event = self.events.pop(0)
            return defer.succeed(event)
        self.d = defer.Deferred()
        return self.d

    def handler(self, eventtype, data):
        event = (eventtype, self.parser(data))
        if self.d:
            assert not self.events
            d,self.d = self.d,None
            d.callback(event)
            return
        self.events.append(event)

    def wait_for_connection(self):
        return self.connected_d
    def connected(self):
        self._connected = True
        self.connected_d.callback(None)

    def wait_for_disconnection(self):
        return self.disconnected_d
    def disconnected(self, why):
        if not self._connected:
            self.connected_d.errback(why)
        self.disconnected_d.callback((why,))

class Summary(unittest.TestCase):
    def test_summarize(self):
        c = relay_server.Channel(None, None, None, None, None)
        A = relay_server.ALLOCATE
        D = relay_server.DEALLOCATE

        messages = [{"when": 1, "side": "a", "phase": A}]
        self.failUnlessEqual(c._summarize(messages, 2),
                             (1, "lonely", 1, None))

        messages = [{"when": 1, "side": "a", "phase": A},
                    {"when": 2, "side": "a", "phase": D, "body": "lonely"},
                    ]
        self.failUnlessEqual(c._summarize(messages, 3),
                             (1, "lonely", 2, None))

        messages = [{"when": 1, "side": "a", "phase": A},
                    {"when": 2, "side": "b", "phase": A},
                    {"when": 3, "side": "c", "phase": A},
                    ]
        self.failUnlessEqual(c._summarize(messages, 4),
                             (1, "crowded", 3, None))

        base = [{"when": 1, "side": "a", "phase": A},
                {"when": 2, "side": "a", "phase": "pake", "body": "msg1"},
                {"when": 10, "side": "b", "phase": "pake", "body": "msg2"},
                {"when": 11, "side": "b", "phase": "data", "body": "msg3"},
                {"when": 20, "side": "a", "phase": "data", "body": "msg4"},
                ]
        def make_moods(A_mood, B_mood):
            return base + [
                {"when": 21, "side": "a", "phase": D, "body": A_mood},
                {"when": 30, "side": "b", "phase": D, "body": B_mood},
                ]

        self.failUnlessEqual(c._summarize(make_moods("happy", "happy"), 41),
                             (1, "happy", 40, 9))

        self.failUnlessEqual(c._summarize(make_moods("scary", "happy"), 41),
                             (1, "scary", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods("happy", "scary"), 41),
                             (1, "scary", 40, 9))

        self.failUnlessEqual(c._summarize(make_moods("lonely", "happy"), 41),
                             (1, "lonely", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods("happy", "lonely"), 41),
                             (1, "lonely", 40, 9))

        self.failUnlessEqual(c._summarize(make_moods("errory", "happy"), 41),
                             (1, "errory", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods("happy", "errory"), 41),
                             (1, "errory", 40, 9))

        # scary trumps other moods
        self.failUnlessEqual(c._summarize(make_moods("scary", "lonely"), 41),
                             (1, "scary", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods("scary", "errory"), 41),
                             (1, "scary", 40, 9))

        # older clients don't send a mood
        self.failUnlessEqual(c._summarize(make_moods(None, None), 41),
                             (1, "quiet", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods(None, "happy"), 41),
                             (1, "quiet", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods(None, "happy"), 41),
                             (1, "quiet", 40, 9))
        self.failUnlessEqual(c._summarize(make_moods(None, "scary"), 41),
                             (1, "scary", 40, 9))
