import nilmdb
from nilmdb.printf import *
from nilmdb.client import ClientError, ServerError

import datetime_tz

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

from test_helpers import *

testdb = "tests/client-testdb"

def setup_module():
    global test_server, test_db
    # Clear out DB
    recursive_unlink(testdb)

    # Start web app on a custom port
    test_db = nilmdb.NilmDB(testdb, sync = False)
    test_server = nilmdb.Server(test_db, host = "127.0.0.1",
                                port = 12380, stoppable = False,
                                fast_shutdown = True,
                                force_traceback = False)
    test_server.start(blocking = False)

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

class TestClient(object):

    def test_client_1_basic(self):
        # Test a fake host
        client = nilmdb.Client(url = "http://localhost:1/")
        with assert_raises(nilmdb.client.ServerError):
            client.version()

        # Trigger same error with a PUT request
        client = nilmdb.Client(url = "http://localhost:1/")
        with assert_raises(nilmdb.client.ServerError):
            client.version()

        # Then a fake URL on a real host
        client = nilmdb.Client(url = "http://localhost:12380/fake/")
        with assert_raises(nilmdb.client.ClientError):
            client.version()

        # Now a real URL with no http:// prefix
        client = nilmdb.Client(url = "localhost:12380")
        version = client.version()

        # Now use the real URL
        client = nilmdb.Client(url = "http://localhost:12380/")
        version = client.version()
        eq_(distutils.version.StrictVersion(version),
            distutils.version.StrictVersion(test_server.version))

    def test_client_2_nilmdb(self):
        # Basic stream tests, like those in test_nilmdb:test_stream
        client = nilmdb.Client(url = "http://localhost:12380/")

        # Database starts empty
        eq_(client.stream_list(), [])

        # Bad path
        with assert_raises(ClientError):
            client.stream_create("foo/bar/baz", "PrepData")
        with assert_raises(ClientError):
            client.stream_create("/foo", "PrepData")
        # Bad layout type
        with assert_raises(ClientError):
            client.stream_create("/newton/prep", "NoSuchLayout")
        client.stream_create("/newton/prep", "PrepData")
        client.stream_create("/newton/raw", "RawData")
        client.stream_create("/newton/zzz/rawnotch", "RawNotchedData")

        # Verify we got 3 streams
        eq_(client.stream_list(), [ ["/newton/prep", "PrepData"],
                                    ["/newton/raw", "RawData"],
                                    ["/newton/zzz/rawnotch", "RawNotchedData"]
                                    ])
        # Match just one type or one path
        eq_(client.stream_list(layout="RawData"), [ ["/newton/raw", "RawData"] ])
        eq_(client.stream_list(path="/newton/raw"), [ ["/newton/raw", "RawData"] ])

        # Set / get metadata
        eq_(client.stream_get_metadata("/newton/prep"), {})
        eq_(client.stream_get_metadata("/newton/raw"), {})
        meta1 = { "description": "The Data",
                  "v_scale": "1.234" }
        meta2 = { "description": "The Data" }
        meta3 = { "v_scale": "1.234" }
        client.stream_set_metadata("/newton/prep", meta1)
        client.stream_update_metadata("/newton/prep", {})
        client.stream_update_metadata("/newton/raw", meta2)
        client.stream_update_metadata("/newton/raw", meta3)
        eq_(client.stream_get_metadata("/newton/prep"), meta1)
        eq_(client.stream_get_metadata("/newton/raw"), meta1)
        eq_(client.stream_get_metadata("/newton/raw", [ "description" ] ), meta2)
        eq_(client.stream_get_metadata("/newton/raw", [ "description",
                                                        "v_scale" ] ), meta1)

        # missing key
        eq_(client.stream_get_metadata("/newton/raw", "descr"),
            { "descr": None })
        eq_(client.stream_get_metadata("/newton/raw", [ "descr" ]),
            { "descr": None })

        # test wrong types (list instead of dict)
        with assert_raises(ClientError):
            client.stream_set_metadata("/newton/prep", [1,2,3])
        with assert_raises(ClientError):
            client.stream_update_metadata("/newton/prep", [1,2,3])

    def test_client_3_insert(self):
        client = nilmdb.Client(url = "http://localhost:12380/")

        datetime_tz.localtz_set("America/New_York")

        testfile = "tests/data/prep-20120323T1000"
        start = datetime_tz.datetime_tz.smartparse("20120323T1000")
        rate = 120

        # First try a nonexistent path
        data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
        with assert_raises(ClientError) as e:
            result = client.stream_insert("/newton/no-such-path", data)
        in_("404 Not Found", str(e.exception))

        # Now try reversed timestamps
        data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
        data = reversed(list(data))
        with assert_raises(ClientError) as e:
            result = client.stream_insert("/newton/prep", data)
        in_("400 Bad Request", str(e.exception))
        in_("timestamp is not monotonically increasing", str(e.exception))

        # Now try empty data (no server request made)
        empty = cStringIO.StringIO("")
        data = nilmdb.timestamper.TimestamperRate(empty, start, 120)
        result = client.stream_insert("/newton/prep", data)
        eq_(result, None)

        # Try forcing a server request with empty data
        with assert_raises(ClientError) as e:
            client.http.put("stream/insert", "", { "path": "/newton/prep" })
        in_("400 Bad Request", str(e.exception))
        in_("no data provided", str(e.exception))

        # Now do the real load
        data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
        result = client.stream_insert("/newton/prep", data)
        eq_(result[0], "ok")

        # Try some overlapping data -- just insert it again
        data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
        with assert_raises(ClientError) as e:
            result = client.stream_insert("/newton/prep", data)
        in_("400 Bad Request", str(e.exception))
        in_("OverlapError", str(e.exception))

    def test_client_4_extract(self):
        # Misc tests for extract.  Most of them are in test_cmdline.
        client = nilmdb.Client(url = "http://localhost:12380/")

        for x in client.stream_extract("/newton/prep", 123, 123):
            raise Exception("shouldn't be any data for this request")

    def test_client_5_generators(self):
        # A lot of the client functionality is already tested by test_cmdline,
        # but this gets a bit more coverage that cmdline misses.
        client = nilmdb.Client(url = "http://localhost:12380/")

        # Trigger a client error in generator
        start = datetime_tz.datetime_tz.smartparse("20120323T2000")
        end = datetime_tz.datetime_tz.smartparse("20120323T1000")
        for function in [ client.stream_intervals, client.stream_extract ]:
            with assert_raises(ClientError) as e:
                function("/newton/prep",
                         start.totimestamp(),
                         end.totimestamp()).next()
            in_("400 Bad Request", str(e.exception))
            in_("end before start", str(e.exception))

        # Trigger a curl error in generator
        with assert_raises(ServerError) as e:
            client.http.get_gen("http://nosuchurl/").next()

        # Trigger a curl error in generator
        with assert_raises(ServerError) as e:
            client.http.get_gen("http://nosuchurl/").next()

        # Check non-json version of string output
        eq_(json.loads(client.http.get("/stream/list",retjson=False)),
            client.http.get("/stream/list",retjson=True))

        # Check non-json version of generator output
        for (a, b) in itertools.izip(
            client.http.get_gen("/stream/list",retjson=False),
            client.http.get_gen("/stream/list",retjson=True)):
            eq_(json.loads(a), b)

        # Check PUT with generator out
        with assert_raises(ClientError) as e:
            client.http.put_gen("stream/insert", "",
                                { "path": "/newton/prep" }).next()
        in_("400 Bad Request", str(e.exception))
        in_("no data provided", str(e.exception))

        # Check 404 for missing streams
        for function in [ client.stream_intervals, client.stream_extract ]:
            with assert_raises(ClientError) as e:
                function("/no/such/stream").next()
            in_("404 Not Found", str(e.exception))
            in_("No such stream", str(e.exception))

    def test_client_6_chunked(self):
        # Make sure that /stream/intervals and /stream/extract
        # properly return streaming, chunked response.  Pokes around
        # in client.http internals a bit to look at the response
        # headers.

        client = nilmdb.Client(url = "http://localhost:12380/")

        # Use a warning rather than returning a test failure, so that we can
        # still disable chunked responses for debugging.
        x = client.http.get("stream/intervals", { "path": "/newton/prep" },
                            retjson=False)
        eq_(x.count('\n'), 2)
        if "transfer-encoding: chunked" not in client.http._headers.lower():
            warnings.warn("Non-chunked HTTP response for /stream/intervals")

        x = client.http.get("stream/extract",
                            { "path": "/newton/prep",
                              "start": "123",
                              "end": "123" }, retjson=False)
        if "transfer-encoding: chunked" not in client.http._headers.lower():
            warnings.warn("Non-chunked HTTP response for /stream/extract")
