#!/usr/bin/python -tt
# vim: ai ts=4 sts=4 et sw=4
#
# Copyright (C) 2010, 2011, 2012, 2013 Intel, Inc.
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of the GNU General Public License
#    as published by the Free Software Foundation; version 2 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

"""Unit tests for class BackednDB"""

import unittest
import re
import json

from mock import patch
from StringIO import StringIO

import redis

from common.backenddb import BackendDB, Entity, BackendDBError

REPOS = """
Repositories:
    -   Name: Repo1
        Link: latest
        Release: "release"
        TopLevel: /srv/trunk
        Project: Project:Repo1
        ProjectConfig: yes
        SnapshotDir: snapshots/trunk
        PrereleaseDir: prerelease/trunk
        Targets:
            - Name: standard
              Architectures:
                  - ia32

    -   Name: Repo2
        Link: latest
        Release: "release"
        TopLevel: /srv/trunk
        Project: Project:Repo2
        DependsOn: Repo1
        SnapshotDir: snapshots/trunk
        PrereleaseDir: prerelease/trunk
        Targets:
            - Name: standard
              Architectures:
                - ia32
                - armv7l

    -   Name: Repo3
        Link: latest
        Release: "tizen"
        TopLevel: /srv/trunk
        Project: Tizen:non-oss
        SnapshotDir: snapshots/trunk
        PrereleaseDir: prerelease/trunk
        Targets:
            - Name: standard
              Architectures:
                - ia32

"""


class RedisMock(object):
    """Mock minimal Redis functionality, required for this testing."""

    def __init__(self, host='localhost', port=6379):
        self.host = host
        self.prot = port
        self._dict = {}
        if port != 6379:
            raise redis.RedisError("Connect error!")

    def delete(self, key):
        """Delete key from db."""
        self._dict.pop(key)

    def exists(self, key):
        """Check if key exists in the db."""
        return key in self._dict

    def keys(self, pattern):
        """Get list of keys from db."""
        return [key for key in self._dict \
                    if re.match("^%s" % pattern, str(key))]

    def hmset(self, key, value):
        """Set hash fields to values."""
        if key not in self._dict:
            self._dict[key] = {}
        for akey, avalue in value.items():
            self._dict[key][akey] = str(avalue)
        return True

    def hgetall(self, key):
        """Get all the fields and values in a hash."""
        return self._dict.get(key, {})

    def hdel(self, name, *keys):
        """Delete keys from hash name."""
        for key in keys:
            self._dict[name].pop(key)

    def rpush(self, key, item):
        """Add item to the list."""
        if key not in self._dict:
            self._dict[key] = []
        self._dict[key].append(item)

    def lrange(self, key, starti, endi):
        """Get items from the list."""
        length = len(self._dict[key])
        if starti < length:
            starti = 0
        if endi == -1 or endi > length:
            endi = length + 1
        return self._dict[key][starti:endi]

    def type(self, key):
        """Returns the type of key ``key``"""
        if isinstance(self._dict[key], dict):
            return 'hash'
        elif isinstance(self._dict[key], list):
            return 'list'
        elif isinstance(self._dict[key], str):
            return 'string'

    def set(self, key, value):
        """Set value for the key."""
        self._dict[key] = value

    def get(self, key):
        """Get value for the key."""
        return self._dict[key]

@patch('redis.Redis', RedisMock) # pylint: disable=R0904
class BackendDBTest(unittest.TestCase):
    '''Tests for BackendDB functionality.'''

    def test_connect_exception(self):
        """Pass incorrect port to raise connect exception."""
        self.assertRaises(BackendDBError, BackendDB, port=65535)

    def test_read_repos(self):
        """Read repos from the yaml string."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        repos = bdb.get_repos()
        maps = bdb.get_obs_repo_map()
        self.assertEqual(repos.keys(), set(['Repo1', 'Repo3', 'Repo2']))
        self.assertEqual(maps.keys(), set(['Tizen:non-oss',
                                           'Project:Repo1',
                                           'Project:Repo2']))
        self.assertEqual(maps['Tizen:non-oss'], 'Repo3')
        self.assertEqual(maps['Project:Repo1'], 'Repo1')
        self.assertEqual(maps['Project:Repo2'], 'Repo2')

    def test_repos_cleanup(self):
        """Set some repos and see if they'll be cleaned up by read_repos."""
        bdb = BackendDB()
        # Clean all repos and maps
        repos = bdb.get_repos()
        for repo in repos:
            repos.delete(repo)
        maps = bdb.get_obs_repo_map()
        for mapping in maps:
            maps.delete(mapping)
        # check if they're cleaned
        self.assertEqual(repos.keys(), set([]))
        self.assertEqual(maps.keys(), set([]))
        # set some junk repos and maps
        repos['tobedeletedrepo'] = {'Targets': []}
        maps['tobedeletedobs'] =  'tobedeleletdrepo'
        # set junk fields for result repo
        repos['Repo1'] = {'tobedeleted1': 'del',
                          'tobedeleted2': 'del',
                          'Targets': []}
        # read repos
        bdb.read_repos(REPOS)
        # check if junk repos and maps have been cleaned up
        self.assertFalse('tobedeletedrepo' in repos)
        self.assertFalse('tobedeletedobs' in maps)
        # check if junk field has been cleaned up
        self.assertFalse('tobedeleted1' in repos['Repo1'])
        self.assertFalse('tobedeleted2' in repos['Repo1'])

    def test_reading_repos_from_fileobj(self):
        """Read repos from file object."""
        bdb = BackendDB()
        bdb.read_repos(StringIO(REPOS))
        repos = bdb.get_repos()
        self.assertEqual(repos.keys(), set(['Repo1', 'Repo3', 'Repo2']))

    def test_read_exception(self):
        """Raising exception by providing incorrect yaml to read_repos."""
        bdb = BackendDB()
        self.assertRaises(BackendDBError, bdb.read_repos, 'bla')

    def test_get_repo(self):
        """Getting repo data."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        data = {'ProjectConfig': 'True',
                'PrereleaseDir': 'prerelease/trunk',
                'Project': 'Project:Repo1',
                'TopLevel': '/srv/trunk',
                'SnapshotDir': 'snapshots/trunk',
                'Link': 'latest',
                'Release': 'release',
                'Targets': [{u'Name': u'standard',
                             u'Architectures': [u'ia32']
                            }]
                }
        repos = bdb.get_repos()
        self.assertEqual(repos["Repo1"], data)

    def test_set_repo(self):
        """Setting repo data."""
        data = {'field1': 'value1',
                'Targets':  [{u'Name': u'standard',
                            u'Architectures': [u'ia32']
                            }]
                }
        bdb = BackendDB()
        repos = bdb.get_repos()
        repos['Test'] = data
        self.assertEqual(repos['Test'], data)

    def test_get_repos_list(self):
        """Get list of repos."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        repos = bdb.get_repos()
        self.assertEqual(repos.keys(), set(['Repo1', 'Repo3', 'Repo2']))

    def test_resetting_repos(self):
        """Set repos 2 times to check if old repos are removed."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        repos = bdb.get_repos()
        repos.delete('Repo1')
        repos['Test'] = {'key': 'value',
                         'Targets': [{u'Name': u'standard',
                                    u'Architectures': [u'ia32']
                                    }]
                        }
        self.assertEqual(repos.keys(), set(['Test', 'Repo3', 'Repo2']))
        bdb.read_repos(REPOS)
        self.assertEqual(repos.keys(), set(['Repo1', 'Repo3', 'Repo2']))

    def test_repo_exists(self):
        """Test if repo exists in the database."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        repos = bdb.get_repos()
        self.assertTrue('Repo1' in repos)
        self.assertFalse('bla' in repos)

    def test_iteration(self):
        """Test iteration over the repos."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        repos = bdb.get_repos()
        self.assertEqual(sorted([repo for repo in repos]),
                         ['Repo1', 'Repo2', 'Repo3'])

    def test_len(self):
        """Test getting length(amount of repos in db)."""
        bdb = BackendDB()
        bdb.read_repos(REPOS)
        repos = bdb.get_repos()
        self.assertEqual(len(repos), 3)

    def test_get_release_ids(self):
        """Test getting release ids."""
        bdb = BackendDB()
        ids = bdb.get_release_ids()
        ids[0] = 'release1'
        self.assertEqual(ids[0], 'release1')

    def test_get_obs_repo_map(self):
        """Test getting mapping between obs project and repo."""
        bdb = BackendDB()
        obsrepomap = bdb.get_obs_repo_map()
        obsrepomap["Tizen:Main"] = "Tizen-main"
        self.assertEqual(obsrepomap["Tizen:Main"], "Tizen-main")

    def test_get_releases(self):
        """Test getting mapping between obs project and repo."""
        bdb = BackendDB()
        releases = bdb.get_releases()
        releases["tizen-ivi"] = "20130627.14"
        releases["tizen-mobile"] = "20130627.4"
        self.assertEqual(releases["tizen-ivi"], "20130627.14")
        self.assertEqual(releases["tizen-mobile"], "20130627.4")


@patch('redis.Redis', RedisMock) # pylint: disable=R0904
class EntityTest(unittest.TestCase):
    '''Tests Entity functionality.'''

    def test_list_value(self):
        """Test using list as value."""
        entity = Entity(redis.Redis(), 'prefix:')
        value = [1, 2, 3]
        entity['list'] = value
        self.assertEqual(entity['list'], value)

    def test_dict_value(self):
        """Test using dict as value."""
        entity = Entity(redis.Redis(), 'prefix:')
        value = {'field1': 'val1', 'field2': 'val2'}
        entity['dict'] = value
        self.assertEqual(entity['dict'], value)

    def test_string_value(self):
        """Test using string as value."""
        entity = Entity(redis.Redis(), 'prefix:')
        value = 'data'
        entity['string'] = value
        self.assertEqual(entity['string'], value)

    def test_jsoned_dict_fields(self):
        """
        Test using dict as a value and list as a value of that dict.
        list should be automatically jsoned and unjsoned when storing/retriving

        """
        entity = Entity(redis.Redis(), 'prefix:', ['field'])
        value = {'field': [1, 2, 3]}
        entity['dict'] = value
        self.assertEqual(entity['dict'], value)

    def test_jsoned_list_items(self):
        """
        Test using nested list as a value.
        2nd level list should be automatically jsoned and unjsoned
        when storing/retriving

        """
        entity = Entity(redis.Redis(), 'prefix:', [0])
        value = [[1, 2, 3], 2, 3]
        entity['list'] = value
        self.assertEqual(entity['list'], value)

    def test_jsoned_string_value(self):
        """
        Test using jsoned string as a value.
        value should be automatically unjsoned when retriving

        """
        entity = Entity(redis.Redis(), 'prefix:', True)
        value = [0, [1, 2, 3], {'4': 5}, 'end']
        entity['string'] = json.dumps(value)
        self.assertEqual(entity['string'], value)
