#!/usr/bin/env python

"""Unittest for kegbot module"""

import commands
import datetime
import time
import logging
import socket
import unittest
import kegbot

from django.test import TestCase

from pykeg.core import backend
from pykeg.core import defaults
from pykeg.core import kbevent
from pykeg.core import models
from pykeg.core import kb_common
from pykeg.core import units
from pykeg.core.net import kegnet
from pykeg.proto import protolib

from pykeg.beerdb import models as bdb_models

LOGGER = logging.getLogger('unittest')

class KegbotTestCase(TestCase):
  def setUp(self):
    del logging.root.handlers[:]
    if not defaults.db_is_installed():
      defaults.set_defaults()

    models.Drink.objects.all().delete()

    self.site, _ = models.KegbotSite.objects.get_or_create(name='default')

    self.keg_size = models.KegSize.objects.create(name='10Liter Keg',
        volume_ml=units.Quantity(10000))

    self.beer_style = bdb_models.BeerStyle.objects.create(name='test style')
    self.brewer = bdb_models.Brewer.objects.create(name='test brewer')
    self.beer_type = bdb_models.BeerType.objects.create(name='test beer type',
        brewer=self.brewer, style=self.beer_style, calories_oz=10.0,
        carbs_oz=2.0, abv=5.0)

    self.test_keg = models.Keg.objects.create(site=self.site,
        type=self.beer_type, size=self.keg_size, status='online',
        description='None')

    self.test_tap = models.KegTap.objects.create(site=self.site,
        name='Test Tap', meter_name='test_meter_name',
        ml_per_tick=(1000.0/2200.0), current_keg=self.test_keg)

    self.kegbot = kegbot.KegbotCoreApp(local_backend=True)
    self.env = self.kegbot._env
    self.backend = self.env.GetBackend()

    self.test_user = self.backend.CreateNewUser('tester')
    self.test_token = self.backend.CreateAuthToken('core.onewire',
        '0000111122223333', 'tester')

    self.test_user_2 = self.backend.CreateNewUser('tester_2')
    self.test_token_2 = self.backend.CreateAuthToken('core.onewire',
        'baadcafebeeff00d', 'tester_2')
    kb_common.AUTH_DEVICE_MAX_IDLE_SECS['core.onewire'] = 2

    # Kill the kegbot flow processing thread so we can single step it.
    self.service_thread = self.env._service_thread
    self.env._threads.remove(self.service_thread)

    self.kegbot._Setup()
    self.kegbot._StartThreads()

    self.client = kegnet.KegnetClient()
    while True:
      if self.client.Reconnect():
        break

  def tearDown(self):
    msg = ''
    for thr in self.env.GetThreads():
      if not thr.isAlive():
        msg += "thread %s died unexpectedly \n" % thr.getName()
    self.kegbot.Quit()
    for thr in self.env.GetThreads():
      if thr.isAlive():
        msg += "thread %s stuck \n" % thr.getName()
    del self.kegbot
    del self.env
    if msg:
      self.fail(msg)

  def testSimpleFlow(self):
    # Synthesize a 2200-tick flow. The FlowManager should zero on the initial
    # reading of 1000.
    meter_name = self.test_tap.meter_name
    self.client.SendFlowStart(meter_name)
    self.client.SendMeterUpdate(meter_name, 1000)
    count = 0
    while count < 2200:
      count += 100
      self.client.SendMeterUpdate(meter_name, 1000+count)

    self.client.SendFlowStop(meter_name)
    self.service_thread._FlushEvents()

    drinks = self.test_keg.drinks.valid()
    self.assertEquals(len(drinks), 1)
    last_drink = drinks[0]

    LOGGER.info('last drink: %s' % (last_drink,))
    self.assertEquals(last_drink.ticks, 2200)

    self.assertEquals(last_drink.keg, self.test_keg)

    self.assertEquals(last_drink.user, None)

  def testAuthenticatedFlow(self):
    meter_name = self.test_tap.meter_name

    # Start a flow by pouring a few ticks.
    self.client.SendMeterUpdate(meter_name, 100)
    self.client.SendMeterUpdate(meter_name, 200)
    time.sleep(1.0)
    self.service_thread._FlushEvents()

    # The flow should be anonymous, since no user is authenticated.
    flow_mgr = self.env.GetFlowManager()
    flows = flow_mgr.GetActiveFlows()
    self.assertEquals(len(flows), 1)
    flow = flows[0]
    self.assertEquals(flow.GetUsername(), '')

    # Now authenticate the user.
    # TODO(mikey): should use tap name rather than meter name.
    self.client.SendAuthTokenAdd(self.test_tap.meter_name,
        auth_device_name=self.test_token.auth_device,
        token_value=self.test_token.token_value)
    time.sleep(1.0) # TODO(mikey): need a synchronous wait
    self.service_thread._FlushEvents()
    self.assertEquals(flow.GetUsername(), self.test_user.username)

    # If another user comes along, he takes over the flow.
    self.client.SendAuthTokenAdd(self.test_tap.meter_name,
        auth_device_name=self.test_token_2.auth_device,
        token_value=self.test_token_2.token_value)
    time.sleep(1.0) # TODO(mikey): need a synchronous wait
    self.service_thread._FlushEvents()

    flows = flow_mgr.GetActiveFlows()
    self.assertEquals(len(flows), 1)
    flow = flows[0]
    self.assertEquals(flow.GetUsername(), self.test_user_2.username)

    self.client.SendMeterUpdate(meter_name, 300)
    time.sleep(1.0) # TODO(mikey): need a synchronous wait
    self.service_thread._FlushEvents()
    self.client.SendFlowStop(meter_name)
    time.sleep(1.0) # TODO(mikey): need a synchronous wait
    self.service_thread._FlushEvents()

    drinks = self.test_keg.drinks.valid().order_by('id')
    self.assertEquals(len(drinks), 2)
    self.assertEquals(protolib.ToProto(drinks[0].user), self.test_user)
    self.assertEquals(protolib.ToProto(drinks[1].user), self.test_user_2)

  def testIdleFlow(self):
    meter_name = self.test_tap.meter_name
    self.client.SendFlowStart(meter_name)
    self.client.SendMeterUpdate(meter_name, 0)
    self.client.SendMeterUpdate(meter_name, 100)
    time.sleep(1.0)
    self.service_thread._FlushEvents()

    flows = self.env.GetFlowManager().GetActiveFlows()
    self.assertEquals(len(flows), 1)

    # Rewind the flow activity clocks to simulate idleness.
    flows[0]._start_time -= datetime.timedelta(minutes=10)
    flows[0]._end_time = flows[0]._start_time

    # Wait for the heartbeat event to kick in.
    time.sleep(1.0)
    self.service_thread._FlushEvents()

    # Verify that the flow has been ended.
    flows = self.env.GetFlowManager().GetActiveFlows()
    self.assertEquals(len(flows), 0)

  def testTokenDebounce(self):
    meter_name = self.test_tap.meter_name
    self.client.SendFlowStart(meter_name)
    self.client.SendMeterUpdate(meter_name, 0)
    self.client.SendMeterUpdate(meter_name, 100)
    time.sleep(1.0)
    self.service_thread._FlushEvents()

    self.client.SendAuthTokenAdd(self.test_tap.meter_name,
        auth_device_name=self.test_token.auth_device,
        token_value=self.test_token.token_value)
    time.sleep(1.0) # TODO(mikey): need a synchronous wait
    self.service_thread._FlushEvents()

    flows = self.env.GetFlowManager().GetActiveFlows()
    self.assertEquals(len(flows), 1)
    flow = flows[0]
    self.assertEquals(flow.GetUsername(), self.test_user.username)
    original_flow_id = flow.GetId()

    LOGGER.info('Removing token...')
    self.client.SendAuthTokenRemove(self.test_tap.meter_name,
        auth_device_name=self.test_token.auth_device,
        token_value=self.test_token.token_value)
    time.sleep(0.5)
    self.service_thread._FlushEvents()

    # The flow should still be active.
    flows = self.env.GetFlowManager().GetActiveFlows()
    self.assertEquals(len(flows), 1)
    flow = flows[0]
    self.assertEquals(flow.GetId(), original_flow_id)
    self.assertEquals(flow.GetState(), kbevent.FlowUpdate.FlowState.ACTIVE)

    # Re-add the token; should be unchanged.
    self.client.SendAuthTokenAdd(self.test_tap.meter_name,
        auth_device_name=self.test_token.auth_device,
        token_value=self.test_token.token_value)
    time.sleep(0.5)
    self.service_thread._FlushEvents()

    # No change in flow
    flows = self.env.GetFlowManager().GetActiveFlows()
    self.assertEquals(len(flows), 1)
    flow = flows[0]
    self.assertEquals(flow.GetId(), original_flow_id)
    self.assertEquals(flow.GetState(), kbevent.FlowUpdate.FlowState.ACTIVE)

    # Idle out. TODO(mikey): shift clock instead of sleeping.
    time.sleep(2.5)
    self.service_thread._FlushEvents()
    flows = self.env.GetFlowManager().GetActiveFlows()
    self.assertEquals(len(flows), 0)

if __name__ == '__main__':
  unittest.main()
