# -*- coding: utf-8 -*-

import nilmrun.server

from nilmdb.client.httpclient import HTTPClient, ClientError, ServerError

from nilmdb.utils.printf import *

from nose.plugins.skip import SkipTest
from nose.tools import *
from nose.tools import assert_raises

import itertools
import distutils.version
import os
import sys
import threading
import cStringIO
import simplejson as json
import unittest
import warnings
import time
import re
import urllib2
from urllib2 import urlopen, HTTPError
import requests
import pprint
import textwrap

from testutil.helpers import *

testurl = "http://localhost:32181/"
#testurl = "http://bucket.mit.edu/nilmrun/"

def setup_module():
    global test_server

    # Start web app on a custom port
    test_server = nilmrun.server.Server(host = "127.0.0.1",
                                        port = 32181,
                                        force_traceback = True)
    test_server.start(blocking = False)

def teardown_module():
    global test_server
    # Close web app
    test_server.stop()

class TestClient(object):

    def wait_kill(self, client, pid, timeout = 1):
        time.sleep(timeout)
        status = client.get("process/status", { "pid": pid })
        if not status["alive"]:
            raise AssertionError("died before we could kill it")
        status = client.post("process/remove", { "pid": pid })
        if status["alive"]:
            raise AssertionError("didn't get killed")
        return status

    def wait_end(self, client, pid, timeout = 5, remove = True):
        start = time.time()
        status = None
        while (time.time() - start) < timeout:
            status = client.get("process/status", { "pid": pid })
            if status["alive"] == False:
                break
        else:
            raise AssertionError("process " + str(pid) + " didn't die in " +
                                 str(timeout) + " seconds: " + repr(status))
        if remove:
            status = client.post("process/remove", { "pid": pid })
        return status

    def test_client_01_basic(self):
        client = HTTPClient(baseurl = testurl)
        version = client.get("version")
        eq_(distutils.version.LooseVersion(version),
            distutils.version.LooseVersion(nilmrun.__version__))

        in_("This is NilmRun", client.get(""))

        with assert_raises(ClientError):
            client.get("favicon.ico")

    def test_client_02_manager(self):
        client = HTTPClient(baseurl = testurl)

        eq_(client.get("process/list"), [])

        with assert_raises(ClientError) as e:
            client.get("process/status", { "pid": 12345 })
        in_("No such PID", str(e.exception))
        with assert_raises(ClientError):
            client.get("process/remove", { "pid": 12345 })
        in_("No such PID", str(e.exception))

    def test_client_03_run_command(self):
        client = HTTPClient(baseurl = testurl, post_json = True)
        eq_(client.get("process/list"), [])

        def do(argv, kill):
            pid = client.post("run/command", { "argv": argv } )
            eq_(client.get("process/list"), [pid])
            if kill:
                return self.wait_kill(client, pid)
            return self.wait_end(client, pid)

        # Simple command
        status = do(["pwd"], False)
        eq_(status["exitcode"], 0)
        eq_("/tmp\n", status["log"])

        # Command with args
        status = do(["expr", "1", "+", "2"], False)
        eq_(status["exitcode"], 0)
        eq_("3\n", status["log"])

        # Missing command
        with assert_raises(ClientError) as e:
            do(["/no-such-command-blah-blah"], False)
        in_("No such file or directory", str(e.exception))

        # Kill a slow command
        status = do(["sleep", "60"], True)
        ne_(status["exitcode"], 0)

    def _run_testfilter(self, client, args):
        code = textwrap.dedent("""
        import nilmrun.testfilter
        import simplejson as json
        import sys
        nilmrun.testfilter.test(json.loads(sys.argv[1]))
        """)
        jsonargs = json.dumps(args)
        return client.post("run/code", { "code": code, "args": [ jsonargs ] })

    def test_client_04_process_basic(self):
        client = HTTPClient(baseurl = testurl, post_json = True)

        # start dummy filter
        pid = self._run_testfilter(client, 30)
        eq_(client.get("process/list"), [pid])
        time.sleep(1)

        # Verify that status looks OK
        status = client.get("process/status", { "pid": pid, "clear": True })
        for x in [ "pid", "alive", "exitcode", "name", "start_time", "log" ]:
            in_(x, status)
        in_("dummy 0\ndummy 1\ndummy 2\ndummy 3\n", status["log"])
        eq_(status["alive"], True)
        eq_(status["exitcode"], None)

        # Check that the log got cleared
        status = client.get("process/status", { "pid": pid })
        nin_("dummy 0\ndummy 1\ndummy 2\ndummy 3\n", status["log"])

        # See that it ended properly
        status = self.wait_end(client, pid, remove = False)
        in_("dummy 27\ndummy 28\ndummy 29\n", status["log"])
        eq_(status["exitcode"], 0)

        # Remove it
        killstatus = client.post("process/remove", { "pid": pid })
        eq_(status, killstatus)
        eq_(client.get("process/list"), [])
        with assert_raises(ClientError) as e:
            client.post("process/remove", { "pid": pid })
        in_("No such PID", str(e.exception))

    def test_client_05_process_terminate(self):
        client = HTTPClient(baseurl = testurl, post_json = True)

        # Trigger exception in filter
        pid = self._run_testfilter(client, -1)
        time.sleep(0.5)
        status = client.get("process/status", { "pid": pid })
        eq_(status["alive"], False)
        eq_(status["exitcode"], 1)
        in_("Exception: test exception", status["log"])
        client.post("process/remove", { "pid": pid })

        # Kill a running filter by removing it early
        newpid = self._run_testfilter(client, 50)
        ne_(newpid, pid)
        time.sleep(0.5)
        start = time.time()
        status = client.post("process/remove", { "pid": newpid })
        elapsed = time.time() - start
        # Should have died in slightly over 1 second
        assert(0.5 < elapsed < 2)
        eq_(status["alive"], False)
        ne_(status["exitcode"], 0)

        # No more
        eq_(client.get("process/list"), [])

        # Try to remove a running filter that ignored SIGTERM
        pid = self._run_testfilter(client, 0)
        start = time.time()
        status = client.post("process/remove", { "pid": pid })
        elapsed = time.time() - start
        # Should have died in slightly over 2 seconds
        assert(1.5 < elapsed < 3)
        eq_(status["alive"], False)
        ne_(status["exitcode"], 0)

    @unittest.skip("needs a running nilmdb; trainola moved to nilmtools")
    def test_client_06_trainola(self):
        client = HTTPClient(baseurl = testurl, post_json = True)
        data = { "url": "http://bucket.mit.edu/nilmdb",
                 "dest_stream": "/sharon/prep-a-matches",
                 "stream": "/sharon/prep-a",
                 "start": 1366111383280463,
                 "end": 1366126163457797,
                 "columns": [ { "name": "P1", "index": 0 },
                              { "name": "Q1", "index": 1 },
                              { "name": "P3", "index": 2 } ],
                 "exemplars": [
                     { "name": "Boiler Pump ON",
                       "url": "http://bucket.mit.edu/nilmdb",
                       "stream": "/sharon/prep-a",
                       "start": 1366260494269078,
                       "end": 1366260608185031,
                       "dest_column": 0,
                       "columns": [ { "name": "P1", "index": 0 },
                                    { "name": "Q1", "index": 1 }
                                    ]
                       },
                     { "name": "Boiler Pump OFF",
                       "url": "http://bucket.mit.edu/nilmdb",
                       "stream": "/sharon/prep-a",
                       "start": 1366260864215764,
                       "end": 1366260870882998,
                       "dest_column": 1,
                       "columns": [ { "name": "P1", "index": 0 },
                                    { "name": "Q1", "index": 1 }
                                    ]
                       }
                     ]
                 }
        pid = client.post("run/code", { "code": "import nilmtools.trainola\n" +
                                         "nilmtools.trainola.main()",
                                         "args": [ data ] })
        while True:
            status = client.get("process/status", { "pid": pid, "clear": 1 })
            sys.stdout.write(status["log"])
            sys.stdout.flush()
            if status["alive"] == False:
                break
        status = client.post("process/remove", { "pid": pid })
        os._exit(int(status["exitcode"]))

    def test_client_07_run_code(self):
        client = HTTPClient(baseurl = testurl, post_json = True)
        eq_(client.get("process/list"), [])

        def do(code, args, kill):
            pid = client.post("run/code", { "code": code, "args": args } )
            eq_(client.get("process/list"), [pid])
            if kill:
                return self.wait_kill(client, pid)
            return self.wait_end(client, pid)

        # basic code snippet
        code = textwrap.dedent("""
        print 'hello'
        def foo(arg):
            print 'world'
        """)
        status = do(code, [], False)
        eq_("hello\n", status["log"])
        eq_(status["exitcode"], 0)

        # compile error
        code = textwrap.dedent("""
        def foo(arg:
            print 'hello'
        """)
        status = do(code, [], False)
        in_("SyntaxError", status["log"])
        eq_(status["exitcode"], 1)

        # traceback in user code should be formatted nicely
        code = textwrap.dedent("""
        def foo(arg):
            raise Exception(arg)
        foo(123)
        """)
        status = do(code, [], False)
        cleaned_log = re.sub('File "[^"]*",', 'File "",', status["log"])
        eq_('Traceback (most recent call last):\n' +
        '  File "", line 4, in <module>\n' +
        '    foo(123)\n' +
        '  File "", line 3, in foo\n' +
        '    raise Exception(arg)\n' +
        'Exception: 123\n', cleaned_log)
        eq_(status["exitcode"], 1)

        # argument handling (strings come in as unicode)
        code = textwrap.dedent("""
        import sys
        print sys.argv[1].encode('ascii'), sys.argv[2]
        sys.exit(0)  # also test raising SystemExit
        """)
        with assert_raises(ClientError) as e:
	    do(code, ["hello", 123], False)
        in_("400 Bad Request", str(e.exception))
        status = do(code, ["hello", "123"], False)
        eq_(status["log"], "hello 123\n")
        eq_(status["exitcode"], 0)

        # try killing a long-running process
        code = textwrap.dedent("""
        import time
        print 'hello'
        time.sleep(60)
        print 'world'
        """)
        status = do(code, [], True)
        eq_(status["log"], "hello\n")
        ne_(status["exitcode"], 0)

    def test_client_08_bad_types(self):
        client = HTTPClient(baseurl = testurl, post_json = True)

        with assert_raises(ClientError) as e:
            client.post("run/code", { "code": "asdf", "args": "qwer" })
        in_("must be a list", str(e.exception))

        with assert_raises(ClientError) as e:
            client.post("run/command", { "argv": "asdf" })
        in_("must be a list", str(e.exception))