#!/usr/pkg/bin/python3.4


from io import StringIO
import unittest
import os
import posixpath
import sys
import threading
from uuid import UUID
from dropbox import session, client
import datetime
from dropbox.rest import ErrorResponse

try:
    import json
except ImportError:
    import simplejson as json

PY3 = sys.version_info[0] == 3

class BaseClientTests(object):

    @classmethod
    def setUpClass(cls):
        """Creates the API client and decides on a test directory."""
        cls.client = cls._create_client()

        cls.test_dir = "/" + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")

        test_file_dir = os.path.join(os.path.dirname(__file__), "..", "testfiles")
        test_file_dir = os.path.realpath(test_file_dir)
        cls.foo = os.path.join(test_file_dir, "foo.txt")
        cls.frog = os.path.join(test_file_dir, "Costa Rican Frog.jpg")
        cls.song = os.path.join(test_file_dir, "dropbox_song.mp3")

    @classmethod
    def tearDownClass(cls):
        cls.client.file_delete(cls.test_dir)

    def setUp(self):
        # Hack to run setUpClass() for Python 2.6.
        # (It's not worth running tearDownClass(), so you'll have some
        # garbage in your app folder.)
        if not hasattr(self, 'client'):
            self.setUpClass()

    def upload_file(self, src, target, **kwargs):
        with open(src, 'rb') as f:
            return self.client.put_file(target, f, **kwargs)

    def dict_has(self, dictionary, *args, **kwargs):
        """Convenience method to check if a dictionary contains the specified
        keys and key-value pairs"""
        for key in args:
            self.assertTrue(key in dictionary)
        for (key, value) in list(kwargs.items()):
            self.assertEqual(value, dictionary[key])

    def assert_file(self, dictionary, filename, *args, **kwargs):
        import os
        defaults = dict(
            bytes = os.path.getsize(filename),
            is_dir = False
        )
        combined = dict(list(defaults.items()) + list(kwargs.items()))
        self.dict_has(dictionary, *args,
            **combined
        )

    def test_account_info(self):
        """Tests if the account_info returns the expected fields."""
        account_info = self.client.account_info()
        self.dict_has(account_info,
            "country",
            "display_name",
            "referral_link",
            "quota_info",
            "uid"
        )

    def test_put_file(self):
        """Tests if put_file returns the expected metadata"""
        def test_put(file, path):
            file_path = posixpath.join(self.test_dir, path)
            f = open(file, "rb")
            metadata = self.client.put_file(file_path, f)
            self.assert_file(metadata, file, path = file_path)
        test_put(self.foo, "put_foo.txt")
        test_put(self.song, "put_song.mp3")
        test_put(self.frog, "put_frog.jpg")

    def test_put_file_overwrite(self):
        """Tests if put_file with overwrite=true returns the expected metadata"""
        path = posixpath.join(self.test_dir, "foo_overwrite.txt")
        self.upload_file(self.foo, path)
        f = StringIO("This Overwrites")
        metadata = self.client.put_file(path, f, overwrite=True)
        self.dict_has(metadata,
            size = "15 bytes",
            bytes = 15,
            is_dir = False,
            path = path,
            mime_type = "text/plain"
        )

    def test_get_file(self):
        """Tests if storing and retrieving a file returns the same file"""
        def test_get(file, path):
            file_path = posixpath.join(self.test_dir, path)
            self.upload_file(file, file_path)
            downloaded = self.client.get_file(file_path).read()
            local = open(file, "rb").read()
            self.assertEqual(len(downloaded), len(local))
            self.assertEqual(downloaded, local)
        test_get(self.foo, "get_foo.txt")
        test_get(self.frog, "get_frog.txt")
        test_get(self.song, "get_song.txt")

    def test_get_partial_file(self):
        """Tests if storing a file and retrieving part of it returns the correct part"""
        def test_get(file, path, start_frac, download_frac):
            file_path = posixpath.join(self.test_dir, path)
            self.upload_file(file, file_path)
            local = open(file, "rb").read()
            local_len = len(local)

            download_start = int(start_frac * local_len) if start_frac is not None else None
            download_length = int(download_frac * local_len) if download_frac is not None else None
            downloaded = self.client.get_file(file_path, start=download_start,
                                              length=download_length).read()

            local_file = open(file, "rb")
            if download_start:
                local_file.seek(download_start)
                if download_length is None:
                    local_partial = local_file.read()
                else:
                    local_partial = local_file.read(download_length)
            elif download_length:
                local_file.seek(-1 * download_length, 2)
                local_partial = local_file.read(download_length)

            self.assertEqual(len(downloaded), len(local_partial))
            self.assertEqual(downloaded, local_partial)
        test_get(self.foo, "get_foo.txt", 0.25, 0.5)
        test_get(self.frog, "get_frog.txt", None, 0.5)
        test_get(self.song, "get_song.txt", 0.25, None)

    def test_metadata(self):
        """Tests if metadata returns the expected values for a files uploaded earlier"""
        path = posixpath.join(self.test_dir, "foo_upload.txt")
        self.upload_file(self.foo, path)
        metadata = self.client.metadata(path)
        self.assert_file(metadata, self.foo, path = path)

    def test_metadata_bad(self):
        """Tests if metadata returns an error for nonexistent file"""
        self.assertRaises(
            ErrorResponse,
            lambda: self.client.metadata(posixpath.join(self.test_dir, "foo_does_not_exist.txt"))
        )

    def test_create_folder(self):
        """Tests if creating a folder works"""
        path = posixpath.join(self.test_dir, "new_fold\xe9r")
        metadata = self.client.file_create_folder(path)
        self.dict_has(metadata,
            size = "0 bytes",
            bytes = 0,
            is_dir = True,
            path = path
        )

    def test_create_folder_dupe(self):
        """Tests if creating a folder fails correctly if one already exists"""
        path = posixpath.join(self.test_dir,  "new_fold\xe9r_dupe")
        metadata = self.client.file_create_folder(path)
        self.assertRaises(
            ErrorResponse,
            lambda: self.client.file_create_folder(path)
        )

    def test_delete(self):
        """Tests if deleting a file really makes it disappear"""
        path = posixpath.join(self.test_dir, "d\xe9lfoo.txt")
        self.upload_file(self.foo, path)
        metadata = self.client.metadata(path)
        self.assert_file(metadata, self.foo, path = path)
        self.client.file_delete(path)

        metadata = self.client.metadata(path)
        self.assert_file(metadata, self.foo,
            path = path,
            bytes = 0,
            size = "0 bytes",
            is_deleted = True
        )

    def test_copy(self):
        """Tests copying a file, to ensure that two copies exist after the operation"""
        path = posixpath.join(self.test_dir, "copyfoo.txt")
        path2 = posixpath.join(self.test_dir, "copyfoo2.txt")
        self.upload_file(self.foo, path)
        self.client.file_copy(path, path2)
        metadata = self.client.metadata(path)
        metadata2 = self.client.metadata(path2)
        self.assert_file(metadata, self.foo, path = path)
        self.assert_file(metadata2, self.foo, path = path2)

    def test_move(self):
        """Tests moving a file, to ensure the new copy exists and the old copy is removed"""
        path = posixpath.join(self.test_dir, "movefoo.txt")
        path2 = posixpath.join(self.test_dir, "movefoo2.txt")
        self.upload_file(self.foo, path)
        self.client.file_move(path, path2)

        metadata = self.client.metadata(path)
        self.assert_file(metadata, self.foo, path = path, is_deleted = True, size = "0 bytes", bytes = 0)

        metadata = self.client.metadata(path2)
        self.assert_file(metadata, self.foo, path = path2)

    def test_thumbnail(self):
        path = posixpath.join(self.test_dir, "frog.jpeg")
        orig_md = self.upload_file(self.frog, path)
        path = orig_md['path']

        modes = (
            ('xs', 32, 32),
            ('s', 64, 64),
            ('m', 128, 128),
            ('l', 640, 480),
            ('xl', 1024, 768),
        )

        for fmt in ('JPEG', 'PNG'):
            prev_len = 0
            for ident in ('xs', 's', 'm', 'l', 'xl'):
                with self.client.thumbnail(path, ident, fmt) as r:
                    data1 = r.read()
                r, md = self.client.thumbnail_and_metadata(path, ident, fmt)
                with r:
                    data2 = r.read()
                self.assertEqual(md, orig_md)
                self.assertEqual(data1, data2)
                # Make sure the amount of data returned increases as we increase the size.
                self.assertTrue(len(data1) > prev_len)
                prev_len = len(data1)

        # Make sure the default is 'm'
        with self.client.thumbnail(path, 'm') as r:
            data_m = r.read()
        with self.client.thumbnail(path) as r:
            data1 = r.read()
        r, md = self.client.thumbnail_and_metadata(path)
        with r:
            data2 = r.read()
        self.assertEqual(data_m, data1)
        self.assertEqual(data_m, data2)

    def test_stream(self):
        """Tests file streaming using the /media endpoint"""
        path = posixpath.join(self.test_dir, "stream_song.mp3")
        self.upload_file(self.song, path)
        link = self.client.media(path)
        self.dict_has(link,
            "url",
            "expires"
        )

    def test_share(self):
        """Tests file streaming using the /media endpoint"""
        path = posixpath.join(self.test_dir, "stream_song.mp3")
        self.upload_file(self.song, path)
        link = self.client.share(path)
        self.dict_has(link,
            "url",
            "expires"
        )

    def test_search(self):
        """Tests searching for a file in a folder"""
        path = posixpath.join(self.test_dir, "search/")

        j = posixpath.join
        self.upload_file(self.foo, j(path, "text.txt"))
        self.upload_file(self.foo, j(path, "t\xe9xt.txt"))
        self.upload_file(self.foo, j(path, "subFolder/text.txt"))
        self.upload_file(self.foo, j(path, "subFolder/cow.txt"))
        self.upload_file(self.frog, j(path, "frog.jpg"))
        self.upload_file(self.frog, j(path, "frog2.jpg"))
        self.upload_file(self.frog, j(path, "subFolder/frog2.jpg"))

        results = self.client.search(path, "sasdfasdf")
        self.assertEqual(results, [])

        results = self.client.search(path, "jpg")
        self.assertEqual(len(results), 3)
        for metadata in results:
            self.assert_file(metadata, self.frog)

        results = self.client.search(j(path, "subFolder"), "jpg")
        self.assertEqual(len(results), 1)
        self.assert_file(results[0], self.frog)

        all_tex_files = {j(path, n) for n in ["text.txt", "t\xe9xt.txt", "subFolder/text.txt"]}

        results = self.client.search(path, "tex")
        self.assertEqual({r["path"] for r in results}, all_tex_files)

        results = self.client.search(path, "t\xe9x")
        self.assertEqual({r["path"] for r in results}, all_tex_files)


    def test_revisions_restore(self):
        """Tests getting the old revisions of a file"""
        path = posixpath.join(self.test_dir, "foo_revs.txt")
        self.upload_file(self.foo, path)
        self.upload_file(self.frog, path, overwrite = True)
        self.upload_file(self.song, path, overwrite = True)
        revs = self.client.revisions(path)
        metadata = self.client.metadata(path)
        self.assert_file(metadata, self.song, path = path, mime_type = "text/plain")

        self.assertEqual(len(revs), 3)
        self.assert_file(revs[0], self.song, path = path, mime_type = "text/plain")
        self.assert_file(revs[1], self.frog, path = path, mime_type = "text/plain")
        self.assert_file(revs[2], self.foo, path = path, mime_type = "text/plain")

        metadata = self.client.restore(path, revs[2]["rev"])
        self.assert_file(metadata, self.foo, path = path, mime_type = "text/plain")
        metadata = self.client.metadata(path)
        self.assert_file(metadata, self.foo, path = path, mime_type = "text/plain")

    def test_copy_ref(self):
        """Tests using the /copy_ref endpoint to move data within a single dropbox"""
        path = posixpath.join(self.test_dir, "foo_copy_ref.txt")
        path2 = posixpath.join(self.test_dir, "foo_copy_ref_target.txt")

        self.upload_file(self.foo, path)
        copy_ref = self.client.create_copy_ref(path)
        self.dict_has(copy_ref,
            "expires",
            "copy_ref"
        )

        self.client.add_copy_ref(copy_ref["copy_ref"], path2)
        metadata = self.client.metadata(path2)
        self.assert_file(metadata, self.foo, path = path2)
        copied_foo = self.client.get_file(path2).read()
        local_foo = open(self.foo, "rb").read()
        self.assertEqual(len(copied_foo), len(local_foo))
        self.assertEqual(copied_foo, local_foo)

    def test_chunked_upload(self):
        target_path = posixpath.join(self.test_dir, 'chunked_upload_file.txt')
        chunk_size = 4 * 1024
        random_string1, random_data1 = make_random_data(chunk_size)
        random_string2, random_data2 = make_random_data(chunk_size)

        new_offset, upload_id = self.client.upload_chunk(StringIO(random_string1), 0)
        self.assertEqual(new_offset, chunk_size)
        self.assertIsNotNone(upload_id)

        new_offset, upload_id2 = self.client.upload_chunk(StringIO(random_string2), 0,
                                                          new_offset, upload_id)
        self.assertEqual(new_offset, chunk_size * 2)
        self.assertEqual(upload_id2, upload_id)

        metadata = self.client.commit_chunked_upload('/auto' + target_path, upload_id,
                                                     overwrite=True)
        self.dict_has(metadata, bytes=chunk_size * 2, path=target_path)

        downloaded = self.client.get_file(target_path).read()
        self.assertEqual(chunk_size * 2, len(downloaded))
        self.assertEqual(random_data1, downloaded[:chunk_size])
        self.assertEqual(random_data2, downloaded[chunk_size:])

    def test_chunked_uploader(self):
        path = posixpath.join(self.test_dir, "chunked_uploader_file.txt")
        size = 10 * 1024 * 1024
        chunk_size = 4 * 1024 * 1102
        random_string, random_data = make_random_data(size)
        uploader = self.client.get_chunked_uploader(StringIO(random_string), len(random_string))
        error_count = 0
        while uploader.offset < size and error_count < 5:
            try:
                upload = uploader.upload_chunked(chunk_size=chunk_size)
            except ErrorResponse as e:
                error_count += 1
        uploader.finish(path)
        downloaded = self.client.get_file(path).read()
        self.assertEqual(size, len(downloaded))
        self.assertEqual(random_data, downloaded)

    def test_delta(self):
        prefix = posixpath.join(self.test_dir, "delta")

        a = posixpath.join(prefix, "a.txt")
        self.upload_file(self.foo, a)
        b = posixpath.join(prefix, "b.txt")
        self.upload_file(self.foo, b)
        c = posixpath.join(prefix, "c")
        c_1 = posixpath.join(prefix, "c/1.txt")
        self.upload_file(self.foo, c_1)
        c_2 = posixpath.join(prefix, "c/2.txt")
        self.upload_file(self.foo, c_2)

        prefix_lc = prefix.lower()
        c_lc = c.lower()

        # /delta on everything
        expected = { p.lower() for p in (prefix, a, b, c, c_1, c_2) }
        entries = set()
        cursor = None
        while True:
            r = self.client.delta(cursor)
            if r['reset']: entries = set()
            for path_lc, md in r['entries']:
                if path_lc.startswith(prefix_lc+'/') or path_lc == prefix_lc:
                    assert md is not None, "we should never get deletes under 'prefix'"
                    entries.add(path_lc)
            if not r['has_more']: break
            cursor = r['cursor']

        self.assertEqual(expected, entries)

        # /delta where path_prefix=c
        expected = { p.lower() for p in (c, c_1, c_2) }
        entries = set()
        cursor = None
        while True:
            r = self.client.delta(cursor, path_prefix=c)
            if r['reset']: entries = set()
            for path_lc, md in r['entries']:
                assert path_lc.startswith(c_lc+'/') or path_lc == c_lc
                assert md is not None, "we should never get deletes"
                entries.add(path_lc)
            if not r['has_more']: break
            cursor = r['cursor']

        self.assertEqual(expected, entries)

    def test_longpoll_delta(self):
        cursor = self.client.delta()['cursor']

        def assert_longpoll():
            r = self.client.longpoll_delta(cursor)
            assert (r['changes'])

        t = threading.Thread(target=assert_longpoll)
        t.start()

        self.upload_file(self.foo, posixpath.join(self.test_dir, "foo.txt"))
        t.join()

def get_string_field(j, field_name):
    if field_name not in j:
        raise ValueError("missing field: %r" % (field_name,))
    v = j[field_name]
    if not isinstance(v, str):
        raise ValueError("field %r: expecting string, but got %r" + (v,))
    return v

oauth1_auth_info = None
oauth2_auth_info = None

load_oauth1_auth_info_result = None
load_oauth2_auth_info_result = None

def unittest_skip(msg):
    # Fake unittest.skip() for Python 2.6.
    # This only works as a class decorator.
    if hasattr(unittest, 'skip'):
        return unittest.skip(msg)
    else:
        print(msg)
        return lambda cls: object

def skip_if_missing_oauth1_auth_info():
    global load_oauth1_auth_info_result, oauth1_auth_info
    if load_oauth1_auth_info_result is None:
        fn = "tests/oauth1.auth"
        try:
            with open(fn, 'r') as f:
                j = json.load(f)
            app_key = get_string_field(j, "app_key")
            app_secret = get_string_field(j, "app_secret")
            access_key = get_string_field(j, "access_key")
            access_secret = get_string_field(j, "access_secret")
            oauth1_auth_info = app_key, app_secret, access_key, access_secret
            load_oauth1_auth_info_result = (True, "")
        except Exception as e:
            load_oauth1_auth_info_result = (False, "error reading \"%s\": %s" % (fn, e,))

    loaded, msg = load_oauth1_auth_info_result
    if loaded:
        return lambda f: f
    else:
        return unittest_skip(msg)

def skip_if_missing_oauth2_auth_info():
    global load_oauth2_auth_info_result, oauth2_auth_info
    if load_oauth2_auth_info_result is None:
        fn = "tests/oauth2.auth"
        try:
            with open(fn, 'r') as f:
                j = json.load(f)
            access_token = get_string_field(j, "access_token")
            oauth2_auth_info = access_token
            load_oauth2_auth_info_result = (True, "")
        except Exception as e:
            load_oauth2_auth_info_result = (False, "error reading \"%s\": %s" % (fn, e,))

    loaded, msg = load_oauth2_auth_info_result
    if loaded:
        return lambda f: f
    else:
        return unittest_skip(msg)

#@unittest.skipIf(*load_oauth1_auth_info())
@skip_if_missing_oauth1_auth_info()
class TestClientOAuth1(BaseClientTests, unittest.TestCase):
    @classmethod
    def _create_client(cls):
        app_key, app_secret, access_key, access_secret = oauth1_auth_info
        sess = session.DropboxSession(app_key, app_secret)
        sess.set_token(access_key, access_secret)
        return client.DropboxClient(sess)

#@unittest.skipIf(*load_oauth2_auth_info())
@skip_if_missing_oauth2_auth_info()
class TestClientOAuth2(BaseClientTests, unittest.TestCase):
    @classmethod
    def _create_client(cls):
        access_token = oauth2_auth_info
        return client.DropboxClient(access_token)

class TestClient(unittest.TestCase):
    def test_oauth2_token_format_check(self):
        bad_tokens = [
                '',
                '=',
                '=0123',
                '!AZaz09-_./~+',
                'AZaz09-_./~+=.',
                'abcdefg\n',
                'abcdefg\t',
                'abcdefg ',
                'abc\ndefg',
                'abc\tdefg',
                'abc defg',
                '\nabcdefg',
                '\tabcdefg',
                ' abcdefg',
            ]
        good_tokens = [
                '1=',
                '1',
                'abcdefg',
                'AZaz09-_./~+',
                'AZaz09-_./~+=',
                'AZaz09-_./~+==============',
                '.000000000000000000000000.',
            ]
        for t in bad_tokens:
            self.assertRaises(ValueError, client.DropboxClient, t)
        for t in good_tokens:
            client.DropboxClient(t)

    def test_chunked_uploader_bad_token(self):
        c = client.DropboxClient("abcdefg")
        random_string, random_data = make_random_data(10 * 1024 * 1024)
        chunk_size = 4 * 1024 * 1102
        uploader = c.get_chunked_uploader(StringIO(random_string), len(random_string))
        try:
            uploader.upload_chunked(chunk_size=chunk_size)
            assert False, "expected exception for bad token"
        except ErrorResponse as e:
            self.assertEqual(e.status, 401)

def make_random_data(size):
    random_data = os.urandom(size)
    if PY3:
        random_string = random_data.decode('latin1')
    else:
        random_string = random_data
    return random_string, random_data
