import logging
import os.path
import shutil
import tempfile
import unittest

import git
import ruamel.yaml

import lesana
from . import utils


class testEntries(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        utils.copytree('tests/data/simple', self.tmpdir, dirs_exist_ok=True)
        self.collection = lesana.Collection(self.tmpdir)
        self.basepath = self.collection.itemdir
        self.filenames = []

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_simple(self):
        fname = '085682ed-6792-499d-a3ab-9aebd683c011.yaml'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        entry = lesana.Entry(self.collection, data=data, fname=fname)
        self.assertEqual(entry.idterm, 'Q' + data['eid'])
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        eid = '11189ee47ddf4796b718a483b379f976'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        entry = lesana.Entry(self.collection, data=data, fname=fname)
        self.assertEqual(entry.idterm, 'Q' + eid)
        self.assertEqual(entry.short_id, eid[:8])

    def test_write_new(self):
        new_entry = lesana.Entry(self.collection)
        self.collection.save_entries(entries=[new_entry])
        entry_fname = os.path.join(self.basepath, new_entry.fname)
        with open(entry_fname) as fp:
            text = fp.read()
        self.assertIn('quantity (integer): how many items are there', text)
        self.assertIn('other (yaml):', text)
        self.assertNotIn('position (string)', text)
        written = ruamel.yaml.safe_load(text)
        self.assertIsInstance(written['quantity'], int)
        self.assertIsInstance(written['name'], str)

    def test_entry_representation(self):
        eid = '11189ee47ddf4796b718a483b379f976'
        entry = self.collection.entry_from_eid(eid)
        self.assertEqual(str(entry), eid)
        label = '{{ eid }}: {{ name }}'
        self.collection.settings['entry_label'] = label
        self.assertEqual(
            str(entry), '{eid}: {name}'.format(eid=eid, name='Another item')
        )

    def test_entry_creation_eid_but_no_filename(self):
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        data['eid'] = '11189ee47ddf4796b718a483b379f976'
        entry = lesana.Entry(self.collection, data=data)
        self.assertEqual(entry.fname, fname)

    def test_entry_creation_no_eid_no_filename(self):
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        entry = lesana.Entry(self.collection, data=data)
        self.assertIsNotNone(entry.eid)
        self.assertIsNotNone(entry.fname)

    def test_entry_creation_filename_but_no_eid(self):
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        eid = '11189ee47ddf4796b718a483b379f976'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        entry = lesana.Entry(self.collection, data=data, fname=fname)
        self.assertEqual(entry.eid, eid)

    def test_entry_str_filename_and_eid(self):
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        data['eid'] = '11189ee47ddf4796b718a483b379f976'
        entry = lesana.Entry(self.collection, data=data)
        self.assertEqual(str(entry), data['eid'])
        self.collection.settings['entry_label'] = '{{ eid }}: {{ name }}'
        self.assertEqual(str(entry), data['eid'] + ': Another item')

    def test_entry_str_filename_no_eid(self):
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        entry = lesana.Entry(self.collection, data=data)
        eid = entry.eid
        self.assertEqual(str(entry), eid)
        self.collection.settings['entry_label'] = '{{ eid }}: {{ name }}'
        self.assertEqual(str(entry), eid + ': Another item')

    def test_render_entry(self):
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        with open(os.path.join(self.basepath, fname)) as fp:
            data = ruamel.yaml.safe_load(fp)
        entry = lesana.Entry(self.collection, data=data)
        eid = entry.eid
        res = entry.render('tests/data/simple/templates/trivial_template.txt')
        self.assertIn(eid, res)

    def test_empty_data(self):
        entry = lesana.Entry(self.collection)
        self.assertIn("name: ''", entry.yaml_data)
        self.assertIn('quantity: 0', entry.yaml_data)


class testEmptyCollection(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        utils.copytree('tests/data/empty', self.tmpdir, dirs_exist_ok=True)
        self.collection = lesana.Collection(self.tmpdir)

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_loaded(self):
        self.assertEqual(self.collection.settings, {})

        self.collection.update_cache()
        self.assertIsNotNone(self.collection.stemmer)


class testSimpleCollection(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        utils.copytree('tests/data/simple', self.tmpdir, dirs_exist_ok=True)
        self.collection = lesana.Collection(self.tmpdir)

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_loaded(self):
        self.assertIsNotNone(self.collection.settings)
        self.assertEqual(
            self.collection.settings['name'], "Simple lesana collection"
        )
        self.assertEqual(len(self.collection.settings['fields']), 7)
        self.assertEqual(len(self.collection.indexed_fields), 3)

        self.collection.update_cache()
        self.assertIsNotNone(self.collection.stemmer)

    def test_full_search(self):
        self.collection.start_search('Item')
        res = self.collection.get_all_search_results()
        matches = list(res)
        self.assertEqual(len(matches), 2)
        for m in matches:
            self.assertIsInstance(m, lesana.Entry)

    def test_search(self):
        self.collection.start_search('Item')
        res = self.collection.get_search_results()
        matches = list(res)
        self.assertEqual(len(matches), 2)
        for m in matches:
            self.assertIsInstance(m, lesana.Entry)

    def test_search_wildcard(self):
        self.collection.start_search('Ite*')
        res = self.collection.get_search_results()
        matches = list(res)
        self.assertEqual(len(matches), 2)
        for m in matches:
            self.assertIsInstance(m, lesana.Entry)

    def test_search_non_init(self):
        matches = list(self.collection.get_search_results())
        self.assertEqual(matches, [])
        matches = list(self.collection.get_all_search_results())
        self.assertEqual(matches, [])

    def test_all_entries(self):
        res = self.collection.get_all_documents()
        matches = list(res)
        self.assertEqual(len(matches), 3)
        for m in matches:
            self.assertIsInstance(m, lesana.Entry)

    def test_entry_from_eid(self):
        entry = self.collection.entry_from_eid(
            '11189ee47ddf4796b718a483b379f976'
        )
        self.assertEqual(entry.eid, '11189ee47ddf4796b718a483b379f976')
        self.collection.safe = True
        entry = self.collection.entry_from_eid(
            '11189ee47ddf4796b718a483b379f976'
        )
        self.assertEqual(entry.eid, '11189ee47ddf4796b718a483b379f976')

    def test_entry_from_short_eid(self):
        entries = self.collection.entries_from_short_eid('11189ee4')
        self.assertEqual(len(entries), 1)
        self.assertEqual(entries[0].eid, '11189ee47ddf4796b718a483b379f976')
        entries = self.collection.entries_from_short_eid(
            '11189ee47ddf4796b718a483b379f976'
        )
        self.assertEqual(len(entries), 1)
        self.assertEqual(entries[0].eid, '11189ee47ddf4796b718a483b379f976')
        entries = self.collection.entries_from_short_eid('12345678')
        self.assertEqual(len(entries), 0)

    def test_index_missing_file(self):
        with self.assertLogs(level=logging.WARNING) as cm:
            self.collection.update_cache(['non_existing_file'])
        self.assertEqual(len(cm.output), 1)
        self.assertIn("non_existing_file", cm.output[0])

    def test_index_reset(self):
        self.collection.update_cache(reset=True)

    def test_get_entry_missing_eid(self):
        entry = self.collection.entry_from_eid('this is not an eid')
        self.assertIsNone(entry)

    def test_render_collection(self):
        template = self.collection.get_template(
            'tests/data/simple/templates/collection_template.txt'
        )
        res = template.render(entries=self.collection.get_all_documents())
        self.assertIn('11189ee4: Another item', res)

    def test_update(self):
        self.collection.update_field('Item', field="position", value="new_pos")
        with open(
            os.path.join(
                self.collection.basedir,
                'items',
                '11189ee47ddf4796b718a483b379f976.yaml',
            )
        ) as fp:
            self.assertIn("new_pos", fp.read())
            pass
        self.assertEqual(
            self.collection.entry_from_eid(
                "11189ee47ddf4796b718a483b379f976"
            ).data['position'],
            "new_pos",
        )

        self.assertIsNone(
            self.collection.entry_from_eid(
                "8b69b063b2a64db7b5714294a69255c7"
            ).data['position']
        )


class testComplexCollection(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        utils.copytree('tests/data/complex', self.tmpdir, dirs_exist_ok=True)
        self.collection = lesana.Collection(self.tmpdir)

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_init(self):
        self.assertIsNotNone(self.collection.settings)
        self.assertEqual(
            self.collection.settings['name'],
            "Fully featured lesana collection",
        )
        self.assertEqual(len(self.collection.settings['fields']), 9)
        self.assertIsNotNone(self.collection.stemmer)
        self.assertEqual(len(self.collection.indexed_fields), 7)

    def test_index(self):
        self.collection.update_cache()

    def test_indexing_list(self):
        self.collection.update_cache(['73097121f1874a6ea2f927db7dc4f11e.yaml'])
        self.collection.start_search('tags:this')
        res = self.collection.get_search_results()
        matches = list(res)
        self.assertEqual(len(matches), 1)
        for m in matches:
            self.assertIsInstance(m, lesana.Entry)

    def test_boolean_field(self):
        entry = self.collection.entry_from_eid(
            '73097121f1874a6ea2f927db7dc4f11e'
        )
        self.assertIsInstance(entry.data['exists'], bool)

    def test_empty_data(self):
        entry = lesana.Entry(self.collection)
        self.assertIn("name: ''", entry.yaml_data)
        self.assertIn('with_default', entry.yaml_data)
        self.assertIn('amount: 0', entry.yaml_data)

    def test_load_field_loaders(self):
        # Check that all fields have been loaded, with the right types
        to_test = (
            ('name', lesana.types.LesanaString),
            ('description', lesana.types.LesanaText),
            ('position', lesana.types.LesanaString),
            ('something', lesana.types.LesanaYAML),
            ('tags', lesana.types.LesanaList),
            ('keywords', lesana.types.LesanaList),
            ('exists', lesana.types.LesanaBoolean),
            ('with_default', lesana.types.LesanaString),
            ('amount', lesana.types.LesanaInt),
        )
        for f in to_test:
            self.assertIsInstance(self.collection.fields[f[0]], f[1])

    def test_comments_are_preserved(self):
        e = self.collection.entry_from_eid('5084bc6e94f24dc6976629282ef30419')
        yaml_data = e.yaml_data
        self.assertTrue(
            yaml_data.startswith("# This entry has a comment at the beginning")
        )
        self.assertTrue(
            yaml_data.endswith("# and a comment at the end\n")
        )

    def test_data_is_stored_as_written_on_file(self):
        e = self.collection.entry_from_eid('5084bc6e94f24dc6976629282ef30419')
        fname = 'tests/data/complex/items/' + \
            '5084bc6e94f24dc6976629282ef30419.yaml'
        with open(fname, 'r') as fp:
            self.assertEqual(e.yaml_data, fp.read())

    def test_sorted_search(self):
        # search in ascending order
        self.collection.start_search('Amount', sort_by=['amount'])
        res = self.collection.get_search_results()
        matches = list(res)
        self.assertEqual(len(matches), 4)
        self.assertEqual(matches[0].data['amount'], 2)
        self.assertEqual(matches[1].data['amount'], 10)
        self.assertEqual(matches[2].data['amount'], 15)
        self.assertEqual(matches[3].data['amount'], 20)

        # and in descending order
        self.collection.start_search('Amount', sort_by=['-amount'])
        res = self.collection.get_search_results()
        matches = list(res)
        self.assertEqual(len(matches), 4)
        self.assertEqual(matches[0].data['amount'], 20)
        self.assertEqual(matches[1].data['amount'], 15)
        self.assertEqual(matches[2].data['amount'], 10)
        self.assertEqual(matches[3].data['amount'], 2)


class testCollectionWithErrors(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        utils.copytree('tests/data/wrong', self.tmpdir, dirs_exist_ok=True)
        self.collection = lesana.Collection(self.tmpdir)

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_load_wrong_language(self):
        # We reload this collection, with an invalid value in lang, to
        # check that the log contains a warning.
        with self.assertLogs(level=logging.WARNING) as cm:
            self.collection = lesana.Collection(self.tmpdir)
        self.assertEqual(len(cm.output), 2)
        self.assertIn("Invalid language", cm.output[1])
        # The collection will default to english, but should still work.
        self.collection.update_cache()
        self.assertIsNotNone(self.collection.settings)
        self.assertIsNotNone(self.collection.stemmer)

    def test_no_index_for_one_field(self):
        # In the “wrong” collection, some of the entries have no "index"
        # field.
        self.collection.update_cache()
        self.assertIsNotNone(self.collection.settings)
        self.assertIsNotNone(self.collection.stemmer)
        # Fields with no "index" entry are not indexed
        self.assertEqual(len(self.collection.settings['fields']), 8)
        self.assertEqual(len(self.collection.indexed_fields), 3)

    def test_init(self):
        self.assertIsNotNone(self.collection.settings)
        self.assertEqual(
            self.collection.settings['name'],
            "Lesana collection with certain errors",
        )
        self.assertEqual(len(self.collection.settings['fields']), 8)
        self.assertIsNotNone(self.collection.stemmer)
        self.assertEqual(len(self.collection.indexed_fields), 3)

    def test_index(self):
        loaded = self.collection.update_cache()
        self.assertEqual(loaded, 0)


class testCollectionCreation(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_init(self):
        collection = lesana.Collection.init(self.tmpdir)
        self.assertIsInstance(collection, lesana.Collection)
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.git')))
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.lesana')))
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, 'settings.yaml'))
        )
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, '.gitignore'))
        )
        # and then run it twice on the same directory, nothing should break
        collection = lesana.Collection.init(self.tmpdir)
        self.assertIsInstance(collection, lesana.Collection)
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.git')))
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.lesana')))
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, 'settings.yaml'))
        )
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, '.gitignore'))
        )
        created = lesana.Collection(self.tmpdir)
        self.assertTrue(created.settings['git'])

    def _do_nothing(*args, **kwargs):
        # A function that does nothing instead of editing a file
        pass

    def test_init_edit_file(self):
        collection = lesana.Collection.init(
            self.tmpdir, edit_file=self._do_nothing
        )
        self.assertIsInstance(collection, lesana.Collection)
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.git')))
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.lesana')))
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, 'settings.yaml'))
        )
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, '.gitignore'))
        )

    def test_init_no_git(self):
        collection = lesana.Collection.init(self.tmpdir, git_enabled=False)
        self.assertIsInstance(collection, lesana.Collection)
        self.assertFalse(os.path.isdir(os.path.join(self.tmpdir, '.git')))
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.lesana')))
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, 'settings.yaml'))
        )
        self.assertFalse(
            os.path.isfile(os.path.join(self.tmpdir, '.gitignore'))
        )
        # and then run it twice on the same directory, nothing should break
        collection = lesana.Collection.init(self.tmpdir, git_enabled=False)
        self.assertIsInstance(collection, lesana.Collection)
        self.assertFalse(os.path.isdir(os.path.join(self.tmpdir, '.git')))
        self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, '.lesana')))
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, 'settings.yaml'))
        )
        self.assertFalse(
            os.path.isfile(os.path.join(self.tmpdir, '.gitignore'))
        )
        created = lesana.Collection(self.tmpdir)
        self.assertFalse(created.settings['git'])

    def test_deletion(self):
        shutil.copy('tests/data/simple/settings.yaml', self.tmpdir)
        utils.copytree(
            'tests/data/simple/items', os.path.join(self.tmpdir, 'items'),
        )
        collection = lesana.Collection.init(self.tmpdir)
        # We start with one item indexed with the term "another"
        collection.start_search('another')
        mset = collection._enquire.get_mset(0, 10)
        self.assertEqual(mset.get_matches_estimated(), 1)
        # Then delete it
        collection.remove_entries(['11189ee47ddf4796b718a483b379f976'])
        # An now we should have none
        self.assertFalse(
            os.path.exists(
                os.path.join(
                    self.tmpdir,
                    'items',
                    '11189ee47ddf4796b718a483b379f976.yaml',
                )
            )
        )
        collection.start_search('another')
        mset = collection._enquire.get_mset(0, 10)
        self.assertEqual(mset.get_matches_estimated(), 0)

    def test_partial_eid_deletion(self):
        shutil.copy('tests/data/simple/settings.yaml', self.tmpdir)
        utils.copytree(
            'tests/data/simple/items', os.path.join(self.tmpdir, 'items'),
        )
        collection = lesana.Collection.init(self.tmpdir)
        # We start with one item indexed with the term "another"
        collection.start_search('another')
        mset = collection._enquire.get_mset(0, 10)
        self.assertEqual(mset.get_matches_estimated(), 1)
        # Then delete it, using the short id
        collection.remove_entries(['11189ee4'])
        # An now we should have none
        self.assertFalse(
            os.path.exists(
                os.path.join(
                    self.tmpdir,
                    'items',
                    '11189ee47ddf4796b718a483b379f976.yaml',
                )
            )
        )
        collection.start_search('another')
        mset = collection._enquire.get_mset(0, 10)
        self.assertEqual(mset.get_matches_estimated(), 0)

    def _find_file_in_git_index(self, fname, index):
        found = False
        for (path, stage) in index.entries:
            if fname in path:
                found = True
                break
        return found

    def test_git_adding(self):
        shutil.copy('tests/data/simple/settings.yaml', self.tmpdir)
        utils.copytree(
            'tests/data/simple/items', os.path.join(self.tmpdir, 'items'),
        )
        collection = lesana.Collection.init(self.tmpdir)
        fname = '11189ee47ddf4796b718a483b379f976.yaml'
        repo = git.Repo(self.tmpdir)
        # By default, this collection doesn't have any git entry in the
        # settings (but there is a repo)
        collection.git_add_files([os.path.join(collection.itemdir, fname)])
        self.assertFalse(self._find_file_in_git_index(fname, repo.index))
        # Then we set it to false
        collection.settings['git'] = False
        collection.git_add_files([os.path.join(collection.itemdir, fname)])
        self.assertFalse(self._find_file_in_git_index(fname, repo.index))
        # And only when it's set to true we should find the file in the
        # staging area
        collection.settings['git'] = True
        collection.git_add_files([os.path.join(collection.itemdir, fname)])
        self.assertTrue(self._find_file_in_git_index(fname, repo.index))

    def test_init_custom_settings(self):
        collection = lesana.Collection.init(
            self.tmpdir,
            edit_file=self._do_nothing,
            settings={
                'name': 'A different name',
                'fields': [
                    {'name': 'title', 'type': 'string'},
                    {'name': 'author', 'type': 'string'},
                ],
            },
        )
        self.assertIsInstance(collection, lesana.Collection)
        self.assertTrue(
            os.path.isfile(os.path.join(self.tmpdir, 'settings.yaml'))
        )
        self.assertEqual(collection.settings['name'], 'A different name')
        self.assertEqual(len(collection.settings['fields']), 2)


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