##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""PathIndex unit tests.
"""

import unittest


class Dummy(object):

    def __init__(self, path):
        self.path = path

    def getPhysicalPath(self):
        return self.path.split('/')


DUMMIES = {
    1: Dummy("/aa/aa/aa/1.html"),
    2: Dummy("/aa/aa/bb/2.html"),
    3: Dummy("/aa/aa/cc/3.html"),
    4: Dummy("/aa/bb/aa/4.html"),
    5: Dummy("/aa/bb/bb/5.html"),
    6: Dummy("/aa/bb/cc/6.html"),
    7: Dummy("/aa/cc/aa/7.html"),
    8: Dummy("/aa/cc/bb/8.html"),
    9: Dummy("/aa/cc/cc/9.html"),
    10: Dummy("/bb/aa/aa/10.html"),
    11: Dummy("/bb/aa/bb/11.html"),
    12: Dummy("/bb/aa/cc/12.html"),
    13: Dummy("/bb/bb/aa/13.html"),
    14: Dummy("/bb/bb/bb/14.html"),
    15: Dummy("/bb/bb/cc/15.html"),
    16: Dummy("/bb/cc/aa/16.html"),
    17: Dummy("/bb/cc/bb/17.html"),
    18: Dummy("/bb/cc/cc/18.html")
}

_marker = object()


def _populateIndex(index):
    for k, v in DUMMIES.items():
        index.index_object(k, v)


class PathIndexTests(unittest.TestCase):
    """ Test PathIndex objects """

    def _getTargetClass(self):
        from Products.PluginIndexes.PathIndex.PathIndex import PathIndex
        return PathIndex

    def _makeOne(self, id='path', caller=_marker):
        if caller is not _marker:
            return self._getTargetClass()(id, caller)
        return self._getTargetClass()(id)

    def test_class_conforms_to_IPluggableIndex(self):
        from zope.interface.verify import verifyClass

        from Products.PluginIndexes.interfaces import IPluggableIndex
        verifyClass(IPluggableIndex, self._getTargetClass())

    def test_instance_conforms_to_IPluggableIndex(self):
        from zope.interface.verify import verifyObject

        from Products.PluginIndexes.interfaces import IPluggableIndex
        verifyObject(IPluggableIndex, self._makeOne())

    def test_class_conforms_to_IUniqueValueIndex(self):
        from zope.interface.verify import verifyClass

        from Products.PluginIndexes.interfaces import IUniqueValueIndex
        verifyClass(IUniqueValueIndex, self._getTargetClass())

    def test_instance_conforms_to_IUniqueValueIndex(self):
        from zope.interface.verify import verifyObject

        from Products.PluginIndexes.interfaces import IUniqueValueIndex
        verifyObject(IUniqueValueIndex, self._makeOne())

    def test_class_conforms_to_ISortIndex(self):
        from zope.interface.verify import verifyClass

        from Products.PluginIndexes.interfaces import ISortIndex
        verifyClass(ISortIndex, self._getTargetClass())

    def test_instance_conforms_to_ISortIndex(self):
        from zope.interface.verify import verifyObject

        from Products.PluginIndexes.interfaces import ISortIndex
        verifyObject(ISortIndex, self._makeOne())

    def test_class_conforms_to_IPathIndex(self):
        from zope.interface.verify import verifyClass

        from Products.PluginIndexes.interfaces import IPathIndex
        verifyClass(IPathIndex, self._getTargetClass())

    def test_instance_conforms_to_IPathIndex(self):
        from zope.interface.verify import verifyObject

        from Products.PluginIndexes.interfaces import IPathIndex
        verifyObject(IPathIndex, self._makeOne())

    def test_ctor(self):
        index = self._makeOne()
        self.assertEqual(index.id, 'path')
        self.assertEqual(index.operators, ('or', 'and'))
        self.assertEqual(index.useOperator, 'or')
        self.assertEqual(len(index), 0)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 0)
        self.assertEqual(len(index._unindex), 0)
        self.assertEqual(index._length(), 0)

    def test_getEntryForObject_miss_no_default(self):
        index = self._makeOne()
        self.assertEqual(index.getEntryForObject(1234), None)

    def test_getEntryForObject_miss_w_default(self):
        index = self._makeOne()
        default = object()
        self.assertTrue(index.getEntryForObject(1234, default) is default)

    def test_getEntryForObject_hit(self):
        index = self._makeOne()
        _populateIndex(index)
        self.assertEqual(index.getEntryForObject(1), DUMMIES[1].path)

    def test_getIndexSourceNames(self):
        index = self._makeOne('foo')
        self.assertEqual(list(index.getIndexSourceNames()),
                         ['foo', 'getPhysicalPath'])

    def test_index_object_broken_path_raises_TypeError(self):
        index = self._makeOne()
        doc = Dummy({})
        self.assertRaises(TypeError, index.index_object, 1, doc)

    def test_index_object_broken_callable(self):
        index = self._makeOne()
        doc = Dummy(lambda: self.nonesuch)
        rc = index.index_object(1, doc)
        self.assertEqual(rc, 0)
        self.assertEqual(len(index), 0)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 0)
        self.assertEqual(len(index._unindex), 0)
        self.assertEqual(index._length(), 0)

    def test_index_object_at_root(self):
        index = self._makeOne()
        doc = Dummy('/xx')
        rc = index.index_object(1, doc)
        self.assertEqual(len(index), 1)
        self.assertEqual(rc, 1)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(list(index._index['xx'][0]), [1])
        self.assertEqual(len(index._unindex), 1)
        self.assertEqual(index._unindex[1], '/xx')
        self.assertEqual(index._length(), 1)

    def test_index_object_at_root_callable_attr(self):
        index = self._makeOne()
        doc = Dummy(lambda: '/xx')
        rc = index.index_object(1, doc)
        self.assertEqual(len(index), 1)
        self.assertEqual(rc, 1)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(list(index._index['xx'][0]), [1])
        self.assertEqual(len(index._unindex), 1)
        self.assertEqual(index._unindex[1], '/xx')
        self.assertEqual(index._length(), 1)

    def test_index_object_at_root_no_attr_but_getPhysicalPath(self):
        class Other:
            def getPhysicalPath(self):
                return '/xx'
        index = self._makeOne()
        doc = Other()
        rc = index.index_object(1, doc)
        self.assertEqual(rc, 1)
        self.assertEqual(len(index), 1)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(list(index._index['xx'][0]), [1])
        self.assertEqual(len(index._unindex), 1)
        self.assertEqual(index._unindex[1], '/xx')
        self.assertEqual(index._length(), 1)

    def test_index_object_at_root_attr_as_tuple(self):
        index = self._makeOne()
        doc = Dummy(('', 'xx'))
        rc = index.index_object(1, doc)
        self.assertEqual(rc, 1)
        self.assertEqual(len(index), 1)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(list(index._index['xx'][0]), [1])
        self.assertEqual(len(index._unindex), 1)
        self.assertEqual(index._unindex[1], '/xx')
        self.assertEqual(index._length(), 1)

    def test_index_object_strips_empty_path_elements(self):
        index = self._makeOne()
        doc = Dummy('////xx//')
        rc = index.index_object(1, doc)
        self.assertEqual(rc, 1)
        self.assertEqual(len(index), 1)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(list(index._index['xx'][0]), [1])
        self.assertEqual(len(index._unindex), 1)
        self.assertEqual(index._unindex[1], '////xx//')
        self.assertEqual(index._length(), 1)

    def test_index_object_below_root(self):
        index = self._makeOne()
        doc = Dummy('/xx/yy/zz')
        rc = index.index_object(1, doc)
        self.assertEqual(rc, 1)
        self.assertEqual(len(index), 1)
        self.assertEqual(index._depth, 2)
        self.assertEqual(len(index._index), 3)
        self.assertEqual(list(index._index['xx'][0]), [1])
        self.assertEqual(list(index._index['yy'][1]), [1])
        self.assertEqual(list(index._index['zz'][2]), [1])
        self.assertEqual(len(index._unindex), 1)
        self.assertEqual(index._unindex[1], '/xx/yy/zz')
        self.assertEqual(index._length(), 1)

    def test_index_object_again(self):
        index = self._makeOne()
        o = Dummy('/foo/bar')
        index.index_object(1234, o)
        self.assertEqual(len(index), 1)
        self.assertEqual(index.numObjects(), 1)
        index.index_object(1234, o)
        self.assertEqual(len(index), 1)
        self.assertEqual(index.numObjects(), 1)

    def test_unindex_object_nonesuch(self):
        index = self._makeOne()
        index.unindex_object(1234)  # nothrow

    def test_unindex_object_broken_path(self):
        index = self._makeOne()
        _populateIndex(index)
        index._unindex[1] = "/broken/thing"
        index.unindex_object(1)  # nothrow

    def test_unindex_object_found(self):
        index = self._makeOne()
        _populateIndex(index)

        for k in DUMMIES.keys():
            index.unindex_object(k)

        self.assertEqual(index.numObjects(), 0)
        self.assertEqual(len(index._index), 0)
        self.assertEqual(len(index._unindex), 0)

    def test__apply_index_no_match_in_query(self):
        index = self._makeOne()
        self.assertEqual(index._apply_index({'foo': 'xxx'}), None)

    def test__apply_index_nonesuch(self):
        index = self._makeOne()
        res = index._apply_index({'path': 'xxx'})
        self.assertEqual(len(res[0]), 0)
        self.assertEqual(res[1], ('path',))

    def test___apply_index_root_levelO_dict(self):
        index = self._makeOne()
        _populateIndex(index)
        query = {'path': {'query': '/', 'level': 0}}
        res = index._apply_index(query)
        self.assertEqual(list(res[0].keys()), list(range(1, 19)))

    def test___apply_index_root_levelO_tuple(self):
        index = self._makeOne()
        _populateIndex(index)
        query = {'path': (('/', 0),)}
        res = index._apply_index(query)
        self.assertEqual(list(res[0].keys()), list(range(1, 19)))

    def test__apply_index_simple(self):
        index = self._makeOne()
        _populateIndex(index)
        tests = [
            # component, level, expected results
            ("aa", 0, [1, 2, 3, 4, 5, 6, 7, 8, 9]),
            ("aa", 1, [1, 2, 3, 10, 11, 12]),
            ("bb", 0, [10, 11, 12, 13, 14, 15, 16, 17, 18]),
            ("bb", 1, [4, 5, 6, 13, 14, 15]),
            ("bb/cc", 0, [16, 17, 18]),
            ("bb/cc", 1, [6, 15]),
            ("bb/aa", 0, [10, 11, 12]),
            ("bb/aa", 1, [4, 13]),
            ("aa/cc", -1, [3, 7, 8, 9, 12]),
            ("bb/bb", -1, [5, 13, 14, 15]),
            ("18.html", 3, [18]),
            ("18.html", -1, [18]),
            ("cc/18.html", -1, [18]),
            ("cc/18.html", 2, [18]),
        ]

        for comp, level, expected in tests:
            for path in [comp, "/" + comp, "/" + comp + "/"]:
                # Test with the level passed in as separate parameter
                query = {'path': {'query': path, 'level': level}}
                res = index._apply_index(query)
                self.assertEqual(list(res[0].keys()), expected)

                # Test with the level passed in as part of the path parameter
                query = {'path': ((path, level),)}
                res = index._apply_index(query)
                self.assertEqual(list(res[0].keys()), expected)

    def test__apply_index_ComplexOrTests(self):
        index = self._makeOne()
        _populateIndex(index)
        tests = [
            (['aa', 'bb'], 1,
                [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15]),
            (['aa', 'bb', 'xx'], 1,
                [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15]),
            ([('cc', 1), ('cc', 2)], 0,
                [3, 6, 7, 8, 9, 12, 15, 16, 17, 18]),
        ]

        for lst, level, expected in tests:
            query = {'path': {'query': lst, 'level': level, 'operator': 'or'}}
            res = index._apply_index(query)
            lst = list(res[0].keys())
            self.assertEqual(lst, expected)

    def test__apply_index_ComplexANDTests(self):
        index = self._makeOne()
        _populateIndex(index)
        tests = [
            # Path query (as list or (path, level) tuple), level, expected
            (['aa', 'bb'], 1, []),
            ([('aa', 0), ('bb', 1)], 0, [4, 5, 6]),
            ([('aa', 0), ('cc', 2)], 0, [3, 6, 9]),
        ]

        for lst, level, expected in tests:
            query = {'path': {'query': lst, 'level': level, 'operator': 'and'}}
            res = index._apply_index(query)
            lst = list(res[0].keys())
            self.assertEqual(lst, expected)

    def test__apply_index_QueryPathReturnedInResult(self):
        index = self._makeOne()
        index.index_object(1, Dummy("/ff"))
        index.index_object(2, Dummy("/ff/gg"))
        index.index_object(3, Dummy("/ff/gg/3.html"))
        index.index_object(4, Dummy("/ff/gg/4.html"))
        res = index._apply_index({'path': {'query': '/ff/gg'}})
        lst = list(res[0].keys())
        self.assertEqual(lst, [2, 3, 4])

    def test__apply_index_when_path_changes(self):
        index = self._makeOne()
        doc = Dummy('/aa')
        index.index_object(0, doc)
        res = index._apply_index(dict(path='aa'))
        self.assertEqual(list(res[0].keys()), [0])

        doc.path = '/bb'
        index.index_object(0, doc)
        res = index._apply_index(dict(path='bb'))
        self.assertEqual(list(res[0].keys()), [0])

        res = index._apply_index(dict(path='aa'))
        self.assertEqual(list(res[0].keys()), [])

    def test_numObjects_empty(self):
        index = self._makeOne()
        self.assertEqual(index.numObjects(), 0)

    def test_numObjects_filled(self):
        index = self._makeOne()
        _populateIndex(index)
        self.assertEqual(index.numObjects(), len(DUMMIES))

    def test_indexSize_empty(self):
        index = self._makeOne()
        self.assertEqual(index.indexSize(), 0)

    def test_indexSize_filled(self):
        index = self._makeOne()
        _populateIndex(index)
        self.assertEqual(index.indexSize(), len(DUMMIES))

    def test_indexSize_multiple_items_same_path(self):
        index = self._makeOne()
        doc1 = Dummy('/shared')
        doc2 = Dummy('/shared')
        index.index_object(1, doc1)
        index.index_object(2, doc2)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(len(index), 2)
        self.assertEqual(index.numObjects(), 2)
        self.assertEqual(index.indexSize(), 2)

    def test_clear(self):
        index = self._makeOne()
        _populateIndex(index)
        index.clear()
        self.assertEqual(len(index), 0)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 0)
        self.assertEqual(len(index._unindex), 0)
        self.assertEqual(index._length(), 0)

    def test_hasUniqueValuesFor_miss(self):
        index = self._makeOne()
        self.assertFalse(index.hasUniqueValuesFor('miss'))

    def test_hasUniqueValuesFor_hit(self):
        index = self._makeOne()
        self.assertTrue(index.hasUniqueValuesFor('path'))

    def test_uniqueValues_empty(self):
        index = self._makeOne()
        self.assertEqual(len(list(index.uniqueValues())), 0)

    def test_uniqueValues_miss(self):
        index = self._makeOne('foo')
        _populateIndex(index)
        self.assertEqual(len(list(index.uniqueValues('bar'))), 0)

    def test_uniqueValues_hit(self):
        index = self._makeOne('foo')
        _populateIndex(index)
        self.assertEqual(len(list(index.uniqueValues('foo'))),
                         len(DUMMIES) + 3)

    def test_uniqueValues_hit_w_withLength(self):
        index = self._makeOne('foo')
        _populateIndex(index)
        results = dict(index.uniqueValues('foo', True))
        self.assertEqual(len(results), len(DUMMIES) + 3)
        for i in range(1, 19):
            self.assertEqual(results['%s.html' % i], 1)
        self.assertEqual(results['aa'],
                         len([x for x in DUMMIES.values() if 'aa' in x.path]))
        self.assertEqual(results['bb'],
                         len([x for x in DUMMIES.values() if 'bb' in x.path]))
        self.assertEqual(results['cc'],
                         len([x for x in DUMMIES.values() if 'cc' in x.path]))

    def test_keyForDocument_miss(self):
        index = self._makeOne()
        self.assertEqual(index.keyForDocument(1), None)

    def test_keyForDocument_hit(self):
        index = self._makeOne()
        _populateIndex(index)
        self.assertEqual(index.keyForDocument(1), DUMMIES[1].path)

    def test_documentToKeyMap_empty(self):
        index = self._makeOne()
        self.assertEqual(dict(index.documentToKeyMap()), {})

    def test_documentToKeyMap_filled(self):
        index = self._makeOne()
        _populateIndex(index)
        self.assertEqual(dict(index.documentToKeyMap()),
                         dict([(k, v.path) for k, v in DUMMIES.items()]))

    def test_insertEntry_empty_depth_0(self):
        index = self._makeOne()
        index.insertEntry('xx', 123, level=0)
        self.assertEqual(index._depth, 0)
        self.assertEqual(len(index._index), 1)
        self.assertEqual(list(index._index['xx'][0]), [123])

        # insertEntry oesn't update the length or the reverse index.
        self.assertEqual(len(index), 0)
        self.assertEqual(len(index._unindex), 0)
        self.assertEqual(index._length(), 0)

    def test_insertEntry_empty_depth_1(self):
        index = self._makeOne()
        index.insertEntry('xx', 123, level=0)
        index.insertEntry('yy', 123, level=1)
        self.assertEqual(index._depth, 1)
        self.assertEqual(len(index._index), 2)
        self.assertEqual(list(index._index['xx'][0]), [123])
        self.assertEqual(list(index._index['yy'][1]), [123])

    def test_insertEntry_multiple(self):
        index = self._makeOne()
        index.insertEntry('xx', 123, level=0)
        index.insertEntry('yy', 123, level=1)
        index.insertEntry('aa', 456, level=0)
        index.insertEntry('bb', 456, level=1)
        index.insertEntry('cc', 456, level=2)
        self.assertEqual(index._depth, 2)
        self.assertEqual(len(index._index), 5)
        self.assertEqual(list(index._index['xx'][0]), [123])
        self.assertEqual(list(index._index['yy'][1]), [123])
        self.assertEqual(list(index._index['aa'][0]), [456])
        self.assertEqual(list(index._index['bb'][1]), [456])
        self.assertEqual(list(index._index['cc'][2]), [456])

    def test__search_empty_index_string_query(self):
        index = self._makeOne()
        self.assertEqual(list(index._search('/xxx')), [])

    def test__search_empty_index_tuple_query(self):
        index = self._makeOne()
        self.assertEqual(list(index._search(('/xxx', 0))), [])

    def test__search_empty_path(self):
        index = self._makeOne()
        doc = Dummy('/aa')
        index.index_object(1, doc)
        self.assertEqual(list(index._search('/')), [1])

    def test__search_matching_path(self):
        index = self._makeOne()
        doc = Dummy('/aa')
        index.index_object(1, doc)
        self.assertEqual(list(index._search('/aa')), [1])

    def test__search_mismatched_path(self):
        index = self._makeOne()
        doc = Dummy('/aa')
        index.index_object(1, doc)
        self.assertEqual(list(index._search('/bb')), [])

    def test__search_w_level_0(self):
        index = self._makeOne()
        doc = Dummy('/aa/bb')
        index.index_object(1, doc)
        self.assertEqual(list(index._search('aa', 0)), [1])
        self.assertEqual(list(index._search('aa', 1)), [])
        self.assertEqual(list(index._search('bb', 1)), [1])
        self.assertEqual(list(index._search('aa/bb', 0)), [1])
        self.assertEqual(list(index._search('aa/bb', 1)), [])
