from zope.interface import Interface, implements

from axiom.store import Store
from axiom.item import Item
from axiom.attributes import integer
from axiom.test.util import QueryCounter

from xmantissa import sharing

from twisted.trial import unittest

from twisted.python.components import registerAdapter

class IPrivateThing(Interface):
    def mutateSomeState():
        pass

class IReadOnly(Interface):
    def retrieveSomeState():
        """
        Retrieve the data.
        """


class PrivateThing(Item):
    implements(IPrivateThing, IReadOnly)
    publicData = integer()
    typeName = 'test_sharing_private_thing'
    schemaVersion = 1

    def mutateSomeState(self):
        self.publicData += 5

    def retrieveSomeState(self):
        """
        Retrieve my public data.
        """
        return self.publicData



class IPublicThing(Interface):
    def callMethod():
        pass

    def isMethodAvailable(self):
        pass

class PublicFacingAdapter(object):

    implements(IPublicThing)

    def __init__(self, thunk):
        self.thunk = thunk

    def isMethodAvailable(self):
        return IPrivateThing.providedBy(self.thunk)

    def callMethod(self):
        return self.thunk.mutateSomeState()

registerAdapter(PublicFacingAdapter, IReadOnly, IPublicThing)


class SimpleSharing(unittest.TestCase):


    def setUp(self):
        self.store = Store()


    def test_differentUserSameID(self):
        """
        Verify that if different facets of the same item are shared to different
        users with the same shareID, each user will receive the correct
        respective facet with only the correct methods exposed.
        """
        t = PrivateThing(store=self.store, publicData=789)
        toBob = sharing.shareItem(t, toName=u'bob@example.com',
                                  interfaces=[IReadOnly])
        toAlice = sharing.shareItem(t, toName=u'alice@example.com',
                                    shareID=toBob.shareID,
                                    interfaces=[IPrivateThing])
        # Sanity check.
        self.assertEquals(toBob.shareID, toAlice.shareID)
        asBob = sharing.getShare(self.store,
                                 sharing.getPrimaryRole(
                self.store, u'bob@example.com'),
                                 toBob.shareID)
        asAlice = sharing.getShare(self.store,
                                 sharing.getPrimaryRole(
                self.store, u'alice@example.com'),
                                 toBob.shareID)
        self.assertEquals(asBob.retrieveSomeState(), 789)
        self.assertRaises(AttributeError, lambda : asBob.mutateSomeState)
        self.assertRaises(AttributeError, lambda : asAlice.retrieveSomeState)
        asAlice.mutateSomeState()
        # Make sure they're both seeing the same item.
        self.assertEquals(asBob.retrieveSomeState(), 789+5)


    def test_simpleShare(self):
        """
        Verify that an item which is shared with shareItem can be retrieved and
        manipulated with getShare.
        """
        t = PrivateThing(store=self.store, publicData=456)
        shareItemResult = sharing.shareItem(t, toName=u'bob@example.com')
        gotShare = sharing.getShare(self.store,
                                    sharing.getPrimaryRole(self.store,
                                                           u'bob@example.com'),
                                    shareItemResult.shareID)
        gotShare.mutateSomeState()
        self.assertEquals(t.publicData, 456 + 5)


    def test_invalidShareID(self):
        """
        Verify that NoSuchShare is raised when getShare is called without sharing
        anything first.
        """
        self.assertRaises(sharing.NoSuchShare,
                          sharing.getShare,
                          self.store,
                          sharing.getPrimaryRole(self.store,
                                                 u'nobody@example.com'),
                          u"not a valid shareID")

    def test_unauthorizedAccessNoShare(self):
        """
        Verify that NoSuchShare is raised when getShare is called with a user who
        is not allowed to access a shared item.
        """
        t = PrivateThing(store=self.store, publicData=345)
        theShare = sharing.shareItem(t, toName=u'somebody@example.com')
        self.assertRaises(sharing.NoSuchShare,
                          sharing.getShare,
                          self.store,
                          sharing.getPrimaryRole(self.store,
                                                 u'nobody@example.com'),
                          theShare.shareID)


    def test_shareAndAdapt(self):
        """
        Verify that when an item is shared to a particular user with a particular
        interface, retrieiving it for that user results in methods on the given
        interface being callable and other methods being restricted.
        """
        t = PrivateThing(store=self.store, publicData=789)

        # Sanity check.
        self.failUnless(IPublicThing(t).isMethodAvailable())

        shared = sharing.shareItem(t, toName=u'testshare', interfaces=[IReadOnly])
        proxy = sharing.getShare(self.store,
                                 sharing.getPrimaryRole(self.store, u'testshare'),
                                 shared.shareID)
        self.failIf(IPublicThing(proxy).isMethodAvailable())
        self.assertRaises(AttributeError, IPublicThing(proxy).callMethod)



class AccessibilityQuery(unittest.TestCase):

    def setUp(self):
        self.i = 0
        self.store = Store()
        self.things = []
        self.bobThings = []
        self.aliceThings = []
        self.bob = sharing.getPrimaryRole(self.store, u'bob@example.com',
                                          createIfNotFound=True)
        self.alice = sharing.getPrimaryRole(self.store, u'alice@example.com',
                                            createIfNotFound=True)


    def test_twoInterfacesTwoGroups(self):
        """
        Verify that when an item is shared to two roles that a user is a member of,
        they will have access to both interfaces when it is retrieved with
        getShare.
        """
        self.addSomeThings()
        us = sharing.getPrimaryRole(self.store, u'us', True)
        them = sharing.getPrimaryRole(self.store, u'them', True)
        self.bob.becomeMemberOf(us)
        self.bob.becomeMemberOf(them)
        it = PrivateThing(store=self.store, publicData=1234)
        sharing.shareItem(it, toRole=us, shareID=u'q', interfaces=[IPrivateThing])
        sharing.shareItem(it, toRole=them, shareID=u'q', interfaces=[IReadOnly])
        that = sharing.getShare(self.store, self.bob, u'q')
        self.assertEquals(that.retrieveSomeState(), 1234)
        that.mutateSomeState()
        self.assertEquals(that.retrieveSomeState(), 1239)


    def test_twoInterfacesTwoGroupsQuery(self):
        """
        Verify that when an item is shared to two roles that a user is a member of,
        and then retrieved by an asAccessibleTo query, both interfaces will be
        accessible on each object in the query result, and the same number of
        items will be accessible in the query as were shared.
        """
        us = sharing.getPrimaryRole(self.store, u'us', True)
        them = sharing.getPrimaryRole(self.store, u'them', True)
        self.bob.becomeMemberOf(us)
        self.bob.becomeMemberOf(them)
        for x in range(3):
            it = PrivateThing(store=self.store, publicData=x)
            sharing.shareItem(it, toRole=us, shareID=u'q',
                              interfaces=[IPrivateThing])
            sharing.shareItem(it, toRole=them, shareID=u'q',
                              interfaces=[IReadOnly])
        # sanity check
        self.assertEquals(self.store.query(PrivateThing).count(), 3)
        aat = list(sharing.asAccessibleTo(self.bob, self.store.query(
                    PrivateThing, sort=PrivateThing.publicData.descending)))
        # sanity check x2
        for acc in aat:
            acc.mutateSomeState()
        expectedData = [x + 5 for x in reversed(range(3))]
        self.assertEquals([acc.retrieveSomeState() for acc in aat],
                          expectedData)


    def addSomeThings(self):
        t = PrivateThing(store=self.store, publicData=-self.i)
        self.i += 1
        self.things.append(t)
        self.bobThings.append(sharing.shareItem(
                t, toName=u'bob@example.com',
                interfaces=[IReadOnly]))
        self.aliceThings.append(sharing.shareItem(
                t,
                toName=u'alice@example.com',
                interfaces=[IPrivateThing]))


    def test_accessibilityQuery(self):
        """
        Ensure that asAccessibleTo returns only items actually accessible to
        the given role.
        """
        for i in range(10):
            self.addSomeThings()

        query = self.store.query(PrivateThing)
        aliceQuery = list(sharing.asAccessibleTo(self.alice, query))
        bobQuery = list(sharing.asAccessibleTo(self.bob, query))

        self.assertEqual(map(sharing.itemFromProxy, bobQuery),
                         map(lambda x: x.sharedItem, self.bobThings))
        self.assertEqual(map(sharing.itemFromProxy, aliceQuery),
                         map(lambda x: x.sharedItem, self.aliceThings))

        self.assertEqual([p.sharedInterfaces
                          for p in aliceQuery], [(IPrivateThing,)] * 10)
        self.assertEqual([p.sharedInterfaces
                          for p in bobQuery], [(IReadOnly,)] * 10)


    def test_sortOrdering(self):
        """
        Ensure that asAccessibleTo respects query sort order.
        """
        for i in range(10):
            self.addSomeThings()

        query = self.store.query(PrivateThing,
                                 sort=PrivateThing.publicData.ascending)
        # Sanity check.
        self.assertEquals([x.publicData for x in query], range(-9, 1, 1))
        bobQuery = list(sharing.asAccessibleTo(self.bob, query))
        self.assertEquals([x.retrieveSomeState() for x in bobQuery],
                          range(-9, 1, 1))
        query2 = self.store.query(PrivateThing,
                                  sort=PrivateThing.publicData.descending)
        # Sanity check #2
        self.assertEquals([x.publicData for x in query2], range(-9, 1, 1)[::-1])
        bobQuery2 = list(sharing.asAccessibleTo(self.bob, query2))
        self.assertEquals([x.retrieveSomeState() for x in bobQuery2], range(-9, 1, 1)[::-1])


    def test_limit(self):
        """
        Ensure that asAccessibleTo respects query limits.
        """
        for i in range(10):
            self.addSomeThings()

        query = self.store.query(PrivateThing, limit=3)
        bobQuery = list(sharing.asAccessibleTo(self.bob, query))
        self.assertEquals(len(bobQuery), 3)


    def test_limitEfficiency(self):
        """
        Verify that querying a limited number of shared items does not become
        slower as more items are shared.
        """
        zomg = QueryCounter(self.store)

        for i in range(10):
            self.addSomeThings()

        query = self.store.query(
            PrivateThing, limit=3, sort=PrivateThing.publicData.ascending)
        checkit = lambda : list(sharing.asAccessibleTo(self.bob, query))
        before = zomg.measure(checkit)

        for i in range(10):
            self.addSomeThings()

        after = zomg.measure(checkit)
        self.assertEquals(before, after)

    test_limitEfficiency.todo = (
        "An inherent limitation of the current implementation, we might be "
        "able to fix this by automatically duplicating colums or something.")


syntax highlighted by Code2HTML, v. 0.9.1