
import os, re, itertools, yaml
from base64 import b32decode
import simplejson

from twisted.trial import unittest
from twisted.internet import defer, address
from twisted.python import log
from twisted.python.filepath import FilePath

from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue
from twisted.application import service
from allmydata.interfaces import InsufficientVersionError
from allmydata.introducer.client import IntroducerClient, \
     WrapV2ClientInV1Interface
from allmydata.introducer.server import IntroducerService, FurlFileConflictError
from allmydata.introducer.common import get_tubid_string_from_ann, \
     get_tubid_string, sign_to_foolscap, unsign_from_foolscap, \
     UnknownKeyError
from allmydata.introducer import old
# test compatibility with old introducer .tac files
from allmydata.introducer import IntroducerNode
from allmydata.web import introweb
from allmydata.client import Client as TahoeClient
from allmydata.util import pollmixin, keyutil, idlib, fileutil, iputil
import allmydata.test.common_util as testutil

class LoggingMultiService(service.MultiService):
    def log(self, msg, **kw):
        log.msg(msg, **kw)

class Node(testutil.SignalMixin, testutil.ReallyEqualMixin, unittest.TestCase):
    def test_furl(self):
        basedir = "introducer.IntroducerNode.test_furl"
        os.mkdir(basedir)
        public_fn = os.path.join(basedir, "introducer.furl")
        private_fn = os.path.join(basedir, "private", "introducer.furl")

        q1 = IntroducerNode(basedir)
        del q1
        # new nodes create unguessable furls in private/introducer.furl
        ifurl = fileutil.read(private_fn)
        self.failUnless(ifurl)
        ifurl = ifurl.strip()
        self.failIf(ifurl.endswith("/introducer"), ifurl)

        # old nodes created guessable furls in BASEDIR/introducer.furl
        guessable = ifurl[:ifurl.rfind("/")] + "/introducer"
        fileutil.write(public_fn, guessable+"\n", mode="w") # text

        # if we see both files, throw an error
        self.failUnlessRaises(FurlFileConflictError,
                              IntroducerNode, basedir)

        # when we see only the public one, move it to private/ and use
        # the existing furl instead of creating a new one
        os.unlink(private_fn)

        q2 = IntroducerNode(basedir)
        del q2
        self.failIf(os.path.exists(public_fn))
        ifurl2 = fileutil.read(private_fn)
        self.failUnless(ifurl2)
        self.failUnlessEqual(ifurl2.strip(), guessable)

    def test_web_static(self):
        basedir = u"introducer.Node.test_web_static"
        os.mkdir(basedir)
        fileutil.write(os.path.join(basedir, "tahoe.cfg"),
                       "[node]\n" +
                       "web.port = tcp:0:interface=127.0.0.1\n" +
                       "web.static = relative\n")
        c = IntroducerNode(basedir)
        w = c.getServiceNamed("webish")
        abs_basedir = fileutil.abspath_expanduser_unicode(basedir)
        expected = fileutil.abspath_expanduser_unicode(u"relative", abs_basedir)
        self.failUnlessReallyEqual(w.staticdir, expected)


class ServiceMixin:
    def setUp(self):
        self.parent = LoggingMultiService()
        self.parent.startService()
    def tearDown(self):
        log.msg("TestIntroducer.tearDown")
        d = defer.succeed(None)
        d.addCallback(lambda res: self.parent.stopService())
        d.addCallback(flushEventualQueue)
        return d

class Introducer(ServiceMixin, unittest.TestCase, pollmixin.PollMixin):

    def test_create(self):
        ic = IntroducerClient(None, "introducer.furl", u"my_nickname",
                              "my_version", "oldest_version", {}, fakeseq,
                              FilePath(self.mktemp()))
        self.failUnless(isinstance(ic, IntroducerClient))

    def test_listen(self):
        i = IntroducerService()
        i.setServiceParent(self.parent)

    def test_duplicate_publish(self):
        i = IntroducerService()
        self.failUnlessEqual(len(i.get_announcements()), 0)
        self.failUnlessEqual(len(i.get_subscribers()), 0)
        furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@192.168.69.247:36106,127.0.0.1:36106/gydnpigj2ja2qr2srq4ikjwnl7xfgbra"
        furl2 = "pb://ttwwooyunnyhzs7r6vdonnm2hpi52w6y@192.168.69.247:36111,127.0.0.1:36106/ttwwoogj2ja2qr2srq4ikjwnl7xfgbra"
        ann1 = (furl1, "storage", "RIStorage", "nick1", "ver23", "ver0")
        ann1b = (furl1, "s
torage", "RIStorage", "nick1", "ver24", "ver0")
        ann2 = (furl2,
 "storage", "RIStorage", "nick2", "ver30", "ver0")
        i.remote_publish(ann1)
        self.failUnlessEqual(len(i.get_announcements()), 1)
        self.failUnlessEqual(len(i.get_subscribers()), 0)
        i.remote_publish(ann2)
        self.failUnlessEqual(len(i.get_announcements()), 2)
        self.failUnlessEqual(len(i.get_subscribers()), 0)
        i.remote_publish(ann1b)
        self.failUnlessEqual(len(i.get_announcements()), 2)
        self.failUnlessEqual(len(i.get_subscribers()), 0)

    def test_id_collision(self):
        # test replacement case where tubid equals a keyid (one should
        # not replace the other)
        i = IntroducerService()
        ic = IntroducerClient(None,
                              "introducer.furl", u"my_nickname",
                              "my_version", "oldest_version", {}, fakeseq,
                              FilePath(self.mktemp()))
        sk_s, vk_s = keyutil.make_keypair()
        sk, _ignored = keyutil.parse_privkey(sk_s)
        keyid = keyutil.remove_prefix(vk_s, "pub-v0-")
        furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
        ann_t = make_ann_t(ic, furl1, sk, 1)
        i.remote_publish_v2(ann_t, Referenceable())
        announcements = i.get_announcements()
        self.failUnlessEqual(len(announcements), 1)
        key1 = ("storage", "v0-"+keyid, None)
        self.failUnlessEqual(announcements[0].index, key1)
        ann1_out = announcements[0].announcement
        self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1)

        furl2 = "pb://%s@127.0.0.1:36106/swissnum" % keyid
        ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0")
        i.remote_publish(ann2)
        announcements = i.get_announcements()
        self.failUnlessEqual(len(announcements), 2)
        key2 = ("storage", None, keyid)
        wanted = [ad for ad in announcements if ad.index == key2]
        self.failUnlessEqual(len(wanted), 1)
        ann2_out = wanted[0].announcement
        self.failUnlessEqual(ann2_out["anonymous-storage-FURL"], furl2)


def fakeseq():
    return 1, "nonce"

seqnum_counter = itertools.count(1)
def realseq():
    return seqnum_counter.next(), str(os.randint(1,100000))

def make_ann(furl):
    ann = { "anonymous-storage-FURL": furl,
            "permutation-seed-base32": get_tubid_string(furl) }
    return ann

def make_ann_t(ic, furl, privkey, seqnum):
    ann_d = ic.create_announcement_dict("storage", make_ann(furl))
    ann_d["seqnum"] = seqnum
    ann_d["nonce"] = "nonce"
    ann_t = sign_to_foolscap(ann_d, privkey)
    return ann_t

class Client(unittest.TestCase):
    def test_duplicate_receive_v1(self):
        ic = IntroducerClient(None,
                              "introducer.furl", u"my_nickname",
                              "my_version", "oldest_version", {}, fakeseq,
                              FilePath(self.mktemp()))
        announcements = []
        ic.subscribe_to("storage",
                        lambda key_s,ann: announcements.append(ann))
        furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnpigj2ja2qr2srq4ikjwnl7xfgbra"
        ann1 = (furl1, "storage", "RIStorage", "nick1", "ver23", "ver0")
        ann1b = (furl1, "storage", "RIStorage", "nick1", "ver24", "ver0")
        ca = WrapV2ClientInV1Interface(ic)

        ca.remote_announce([ann1])
        d = fireEventually()
        def _then(ign):
            self.failUnlessEqual(len(announcements), 1)
            self.failUnlessEqual(announcements[0]["nickname"], u"nick1")
            self.failUnlessEqual(announcements[0]["my-version"], "ver23")
    