import yaml
import json
import datetime

from twisted.trial import unittest

from twisted.internet import defer, reactor, error
from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol

from rhumba import service, client

test_config = {
    'redis_host': 'localhost',
    'queues': [{
        'id': 0, 'max_jobs': 1,
        'name': 'testqueue',
        'plugin': 'rhumba.tests.plugin'
    }],
    'redis_port': 6379
}

class FakeRedis(object):
    """Fake redis client object
    """
    def __init__(self):
        self.kv = {}

    def lpush(self, key, val, expire=None):
        if key in self.kv:
            self.kv[key].insert(0, val)
        else:
            self.kv[key] = [val]

    def set(self, key, val, expire=None):
        self.kv[key] = val

    def get(self, key):
        return self.kv.get(key, None)

    def rpop(self, key):
        if (key in self.kv) and self.kv[key]:
            return self.kv[key].pop(-1)
        else:
            return None

class AsyncWrapper(object):
    def __init__(self, c):
        self.c = c

    def lpush(self, *a, **kw):
        return defer.maybeDeferred(self.c.lpush, *a)

    def rpop(self, *a, **kw):
        return defer.maybeDeferred(self.c.rpop, *a)

    def set(self, *a, **kw):
        return defer.maybeDeferred(self.c.set, *a)

    def get(self, *a, **kw):
        return defer.maybeDeferred(self.c.get, *a)


class RhumbaTest(unittest.TestCase):
    def setUp(self):
        self.service = service.RhumbaService(yaml.dump(test_config))
        self.client = FakeRedis()
        self.service.setupQueues()
        self.service.client = AsyncWrapper(self.client)

        self.c = client.RhumbaClient()
        self.c._get_client = lambda : self.client


class TestClient(client.AsyncRhumbaClient):
    def __init__(self, client, *a, **kw):
        self.client = client

    def connect(self):
        pass

class TestService(RhumbaTest):

    @defer.inlineCallbacks
    def test_async_client(self):
        client = TestClient(self.service.client)

        yield client.queue('testqueue', 'test')
        
        message = self.service.client.c.kv['rhumba.q.testqueue'][0]

        self.assertIn('test', message)

    @defer.inlineCallbacks
    def test_heartbeat(self):
        yield self.service.heartbeat()

        hb = self.client.kv.get("rhumba.server.%s.heartbeat" % self.service.hostname)

        self.assertTrue(hb is not None)
    
    @defer.inlineCallbacks
    def test_status(self):
        yield self.service.setStatus('test')

        hb = self.client.kv.get("rhumba.server.%s.status" % self.service.hostname)
        self.assertEquals(hb, 'test')

    def test_queues(self):
        self.assertTrue('testqueue' in self.service.queues)

    def _wait_for(self, queue, uid):
        d = defer.Deferred()
        def poll(d):
            result = self.c.getResult(queue, uid)

            if result:
                d.callback(result)
            else:
                reactor.callLater(0.1, poll, d)

        reactor.callLater(0.1, poll, d)
        return d

    @defer.inlineCallbacks
    def test_call(self):
        queue = self.service.queues['testqueue']

        uuid1 = self.c.queue('testqueue', 'test', {'count': 1, 'delay': 2})
        uuid2 = self.c.queue('testqueue', 'test', {'count': 2, 'delay': 1})

        reactor.callLater(0, queue.queueRun)
        reactor.callLater(1, queue.queueRun)

        result = yield self._wait_for('testqueue', uuid1)
        self.assertEquals(result['result'], None)

        result = self.c.getResult('testqueue', uuid2)
        self.assertEquals(result, None)

        reactor.callLater(0, queue.queueRun)

        result = yield self._wait_for('testqueue', uuid2)
        
        self.assertEquals(result['result'], None)


    def setUp(self):
        self.service = service.RhumbaService(yaml.dump(test_config))
        self.client = FakeRedis()
        self.service.setupQueues()
        self.service.client = AsyncWrapper(self.client)

        self.c = client.RhumbaClient()
        self.c._get_client = lambda : self.client


class TestCron(RhumbaTest):
    def _messages(self):
        """ Retrieve a list of only messages in the queue """
        q = [json.loads(i)['message'] for i in self.client.kv.get(
            'rhumba.q.testqueue', [])]
        return q

    def _flush(self):
        self.client.kv['rhumba.q.testqueue'] = []

    @defer.inlineCallbacks
    def test_cron(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime.now())
        self.assertIn('rhumba.crons.testqueue.call_everysecond', self.client.kv)
        self.assertIn('everysecond', self._messages())

    @defer.inlineCallbacks
    def test_cron_repeat(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime.now())
        self.assertIn('everysecond', self._messages())

    @defer.inlineCallbacks
    def test_cron_backwards_clock(self):
        queue = self.service.queues['testqueue']
        # Clock goes backwards
        yield self.service.checkCrons(datetime.datetime(2015, 3, 3, 5, 3, 0))
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 2, 3, 5, 3, 2))
        self.assertNotIn('everytwentythsecond', self._messages())

        yield self.service.checkCrons(datetime.datetime(2015, 1, 3, 5, 3, 20))
        self.assertIn('everytwentythsecond', self._messages())


    @defer.inlineCallbacks
    def test_cron_seconds(self):
        queue = self.service.queues['testqueue']

        now = datetime.datetime(2015, 3, 3, 4, 1, 1)

        yield self.service.checkCrons(now)
        self.assertNotIn('everytwentythsecond', self._messages())
        self._flush()

        # 20 second mark
        yield self.service.checkCrons(datetime.datetime(2015, 3, 3, 4, 1, 20))
        self.assertIn('everytwentythsecond', self._messages())
        self._flush()
        
        yield self.service.checkCrons(datetime.datetime(2015, 3, 3, 4, 1, 21))
        self.assertNotIn('everytwentythsecond', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 3, 3, 4, 1, 32))
        self.assertNotIn('everytwentythsecond', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 4, 3, 20))
        self.assertIn('everytwentythsecond', self._messages())

    @defer.inlineCallbacks
    def test_cron_mins(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 4, 2, 1))
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 4, 5, 1))
        self.assertNotIn('everytenminutes', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 4, 12, 1))
        self.assertIn('everytenminutes', self._messages())

    @defer.inlineCallbacks
    def test_cron_hour(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 10, 2, 1))
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 11, 2, 2))
        self.assertIn('everyhour', self._messages())
        self.assertNotIn('atlunch', self._messages())

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 12, 12, 1))
        self.assertIn('everyhour', self._messages())
        self.assertIn('atlunch', self._messages())

    @defer.inlineCallbacks
    def test_cron_mins_hour(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 10, 2, 1))
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 14, 0, 0))
        self.assertIn('everytenminutesat2pm', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 14, 12, 0))
        self.assertIn('everytenminutesat2pm', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 14, 14, 0))
        self.assertNotIn('everytenminutesat2pm', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 15, 14, 0))
        self.assertNotIn('everytenminutesat2pm', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 15, 25, 0))
        self.assertNotIn('everytenminutesat2pm', self._messages())
        self._flush()

    @defer.inlineCallbacks
    def test_cron_multi(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 10, 2, 1))
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 6, 10, 30, 0))
        self.assertIn('everyhour', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 6, 12, 32, 1))
        self.assertIn('everytwohoursontuesday', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 6, 12, 33, 1))
        self.assertNotIn('everytwohoursontuesday', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 6, 14, 34, 1))
        self.assertIn('everytwohoursontuesday', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 6, 14, 30, 20))
        self.assertNotIn('everytwohoursontuesday', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 7, 14, 12, 1))
        self.assertNotIn('everytwohoursontuesday', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 10, 7, 14, 30, 1))
        self.assertNotIn('everytwohoursontuesday', self._messages())
        self._flush()

    @defer.inlineCallbacks
    def test_cron_month(self):
        queue = self.service.queues['testqueue']

        yield self.service.checkCrons(datetime.datetime(2015, 4, 3, 10, 2, 1))

        self.assertNotIn('december', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 12, 1, 0, 0, 0))
        self.assertIn('december', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 12, 2, 0, 0, 0))
        self.assertNotIn('december', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2015, 12, 31, 23, 59, 59))
        self.assertNotIn('december', self._messages())
        self._flush()

        yield self.service.checkCrons(datetime.datetime(2016, 12, 1, 0, 0, 0))
        self.assertIn('december', self._messages())
