import itertools
from twisted.internet import defer
from twisted.trial import unittest
from kontiki import persist
from kontiki.test.common import dropResult


firstEntries = [persist.LogEntry(term=0, command='first1'),
                persist.LogEntry(term=0, command='first2'),
                persist.LogEntry(term=1, command='first3')]
appendedEntries = [persist.LogEntry(term=2, command='append1'),
                   persist.LogEntry(term=3, command='append2')]


class SQLitePersistTestCase(unittest.TestCase):

    def setUp(self):
        self.persister = persist.SQLitePersist(':memory:')
        self.persister.connect()

    def test_setAndGetCurrentTerm(self):
        '''getCurrentTerm should return the current term; if the persister is
        new, the term should be 0.  setcurrentTerm should durably set
        the current term, and it should be retrievable by
        getCurrentTerm.

        '''
        results = self.persister.getCurrentTerm()

        zeroFailureMessage = 'getCurrentTerm for new persister should return 0'
        results.addCallback(self.assertEqual, 0, msg=zeroFailureMessage)

        results.addCallback(
            dropResult(self.persister.setCurre
ntTerm, 123))

        setCurrentTermFailureMessage = 'setCurrentTerm did not set the term'
        results.addCallback(self.assertEqual, 123,
                            msg=setCu
rrentTermFailureMessage)

        results.addCallback(dropResult(self.persister.getCurrentTerm))

        getCurrentTermFailureMessage = ('getCurrentTerm did not agree'
                                        ' with setCurrentTerm')
        results.addCallback(self.assertEqual, 123,
                            msg=getCurrentTermFailureMessage)
        return results

    def test_incrementCurrentTerm(self):
        '''setCurrentTerm should durably increment the currentTerm and return
        it.

        '''
        results = defer.succeed(None)

        for expected in (1, 2, 3):
            results.addCallback(dropResult(self.persister.setCurrentTerm,
                                           None, increment=True))

            incrementFailureMessage = ('setCurrentTerm incremented term'
                                       ' incorrectly')
            results.addCallback(self.assertEqual, expected,
                                msg=incrementFailureMessage)

            results.addCallback(dropResult(self.persister.getCurrentTerm))

            getCurrentTermFailureMessage = ('getCurrentTerm disagreed with'
                                            ' incremented term')
            results.addCallback(self.assertEqual, expected,
                                msg=getCurrentTermFailureMessage)

        return results

    def test_votedForAndvoteFor(self):
        '''votedFor should return the identity of the candidate voted for most
        recently; if the persister is new, it should return None.

        voteFor should durably set the identity of the candidate voted
        for, and that identity should be retrievable by votedFor

        '''
        results = self.persister.votedFor()

        noneVotedForFailure = 'votedFor for a empty persister should be None'

        results.addCallback(self.assertEqual, None, msg=noneVotedForFailure)

        for identity in ('first id', 'second id'):
            results.addCallback(dropResult(self.persister.voteFor, identity))

            voteForFailureMessage = ('voteFor did not correctly set the'
                                     ' candidate identity')
            results.addCallback(self.assertEqual, identity,
                                msg=voteForFailureMessage)

            results.addCallback(dropResult(self.persister.votedFor))

            votedForFailureMessage = 'votedFor disagreed with voteFor'
            results.addCallback(self.assertEqual, identity,
                                msg=votedForFailureMessage)

        return results

    def test_getLastTerm(self):
        '''getLastTerm should return the term of the last log entry; an empty
        persister's getLastTerm should return 0.

        '''
        results = self.persister.getLastTerm()

        zeroFailureMessage = "getLastTerm for an empty persister should return 0"
        results.addCallback(self.assertEqual, 0, msg=zeroFailureMessage)

        for entries in (firstEntries, appendedEntries):
            results.addCallback(
                dropResult(self.persister.addNewEntries, entries))

            results.addCallback(
                dropResult(self.persister.getLastTerm))

            failureMessage = 'getLastTerm returned wrong term'

            results.addCallback(self.assertEqual, entries[-1].term,
                                msg=failureMessage)
        return results

    def test_getLastIndex(self):
        '''getLastIndex should return the index of the last log entry; an empty
        persister's getLastIndex should return -1.

        '''
        results = self.persister.getLastIndex()

        negOneFailureMessage = ('getLastTerm for an empty persister'
                                ' should return -1')
        results.addCallback(self.assertEqual, -1, msg=negOneFailureMessage)

        lastIndex = -1
        for entries in (firstEntries, appendedEntries):
            lastIndex += len(entries)
            results.addCallback(
                dropResult(self.persister.addNewEntries, entries))

            results.addCallback(
                dropResult(self.persister.getLastIndex))

            failureMessage = 'getLastIndex returned the wrong index'
            results.addCallback(self.assertEqual, lastIndex,
                                msg=failureMessage)
        return results

    def test_committableLogEntries(self):
        '''committableLogEntries should return log entries with indices in the
        range (lastApplied, commitIndex].  An empty persister has no
        committable log entries.

        '''
        results = self.persister.committableLogEntries(0, 100)

        emptyFailureMessage = ('committableLogEntries for an empty'
                               ' persister should return an empty list')
        results.addCallback(self.assertEqual, [], msg=emptyFailureMessage)

        results.addCallback(
            dropResult(self.persister.addNewEntries, firstEntries))

        indices = range(-1, len(firstEntries))
        for lastApplied, commitIndex in itertools.combinations(indices, 2):
            results.addCallback(
                dropResult(self.persister.committableLogEntries,
                           lastApplied, commitIndex))

            def assertEntries(result,
                              lastApplied=lastApplied,
                              commitIndex=commitIndex):
                # convert (,] interval to [,)
                start, stop = lastApplied + 1, commitIndex + 1
                expected = firstEntries[start:stop]

                failureMessage = ('committableLogEntries returned wrong'
                                  ' entries for lastApplied %d and'
                                  ' commitIndex %d' % (lastApplied,
                                                       commitIndex))

                self.assertEqual(result, expected, msg=failureMessage)

            results.addCallback(assertEntries)

        return results

    def test_disjointMatchAndAppendNewLogEntries(self):
        '''matchAndAppendNewLogEntries should append disjoint logs.

        '''
        results = self.persister.matchAndAppendNewLogEntries(-1, firstEntries)

        results.addCallback(dropResult(self.persister.getLastIndex))

        indexWrongFailure = ('getLastIndex claims'
                             ' matchAndAppendNewLogEntries'
                             ' failed to append entries')

        firstEntriesNewIndex = len(firstEntries) - 1

        results.addCallback(self.assertEqual, firstEntriesNewIndex,
                            msg=indexWrongFailure)

        results.addCallback(dropResult(self.persister._logSlice, 0, -1))

        logSliceWrongFailure = ('logSlice claims matchAndAppendNewLogEntries'
                                ' failed to append entries')

        re