#!/usr/bin/env python
#
# Public Domain 2014-present MongoDB, Inc.
# Public Domain 2008-2014 WiredTiger, Inc.
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# test_cursor_tracker.py
#       Tracker for testing cursor operations.  Keys and values
#       are generated automatically based somewhat on position,
#       and are stored simultaneously in the WT table and
#       in a compact representation in python data structures
#       (self.bitlist, self.vers).  A set of convenience functions
#       allows us to insert/remove/update keys on a cursor,
#       navigate forward, back, etc. and verify K/V pairs in
#       the table.  Comprehensive tests can then be built up.
#
#       All keys and values are generated, based on a triple:
#       [major, minor, version].  The key generator is a pure function
#       K that returns a string and takes inputs (major, minor).
#       K is implemented as encode_key() below, its inverse is decode_key().
#       The value generator is a pure function V that returns a string
#       based on inputs (major, minor, version).  It is implemented
#       as encode_value(), its inverse being decode_value().
#
#       When a test starts, calling cur_initial_conditions, a set
#       of N K/V populates the table.  These correspond to major
#       numbers.  For example, with N==3, major/minor numbers of
#       [0,0], [0,1], [0,2] are inserted into the table.
#       The table is then closed (and session closed) to guarantee
#       that the associated data is written to disk before continuing.
#       The theory is that since changes in WT are stored in skip lists,
#       we want the test to be aware of preexisting values (those having
#       minor number == 0) so we can try all combinations of adding
#       new skip list entries, and removing/updating both skip list
#       and original values.
#
#       After initial conditions are set up, the test calls functions
#       such as cur_insert to insert values.  Since minor numbers
#       sort secondarily to major, they will take logical positions
#       in specific places relative to major (preexisting) K/V pairs.
#
#       Updating an existing value is detected in the python data
#       structures, and result in incrementing the version number.
#       Thus, the key remains unchanged, but the associated value
#       changes (the size of the value may be altered as well).
#
#       TODO: we need to separate the cursor tracking information
#       (the current position where we believe we are) from
#       the database information (what we think is in the data storage).
#       Once that's done, we can have multiple cursor tests
#       (though simulating transactions would probably be beyond what
#       we want to do here).

import hashlib
import wiredtiger, wttest

class TestCursorTracker(wttest.WiredTigerTestCase):
    table_name1 = 'test_cursor'
    DELETED = 0xffffffffffffffff
    TRACE_API = False    # a print output for each WT API call

    def config_string(self):
        """
        Return any additional configuration.
        This method may be overridden.
        """
        return ''

    def session_create(self, name, args):
        """
        session.create, but report errors more completely
        """
        try:
            self.session.create(name, args)
        except:
            print('**** ERROR in session.create("' + name + '","' + args + '") ***** ')
            raise

    def table_dump(self, name):
        cursor = self.session.open_cursor('table:' + name, None, None)
        self._dumpcursor(cursor)
        cursor.close()

    def __init__(self, testname):
        wttest.WiredTigerTestCase.__init__(self, testname)
        self.cur_initial_conditions(None, 0, None, None, None)

    # traceapi and friends are used internally in this module
    def traceapi(self, s):
        if self.TRACE_API:
            print('> ' + s)

    def traceapi_before(self, s):
        if self.TRACE_API:
            print('> ' + s + '...')

    def traceapi_after(self, s):
        if self.TRACE_API:
            print('  ==> ' + s)

    def setup_encoders_decoders(self):
        if self.tablekind == 'row':
            self.encode_key = self.encode_key_row
            self.decode_key = self.decode_key_row
            self.encode_value = self.encode_value_row_or_col
            self.decode_value = self.decode_value_row_or_col
        elif self.tablekind == 'col':
            self.encode_key = self.encode_key_col_or_fix
            self.decode_key = self.decode_key_col_or_fix
            self.encode_value = self.encode_value_row_or_col
            self.decode_value = self.decode_value_row_or_col
        else:
            self.encode_key = self.encode_key_col_or_fix
            self.decode_key = self.decode_key_col_or_fix
            self.encode_value = self.encode_value_fix
            self.decode_value = self.decode_value_fix

    def cur_initial_conditions(self, tablename, npairs, tablekind, keysizes, valuesizes, uri="table"):
        if npairs >= 0xffffffff:
            raise Exception('cur_initial_conditions: npairs too big')
        self.tablekind = tablekind
        self.isrow = (tablekind == 'row')
        self.setup_encoders_decoders()
        self.bitlist = [(x << 32) for x in range(npairs)]
        self.vers = dict((x << 32, 0) for x in range(npairs))
        self.nopos = True  # not positioned on a valid element
        self.curpos = -1
        self.curbits = 0xffffffffffff
        self.curremoved = False # K/V data in cursor does not correspond to active data
        self.keysizes = keysizes
        self.valuesizes = valuesizes
        if tablekind != None:
            cursor = self.session.open_cursor(uri + ':' + tablename, None, 'append')
            for i in range(npairs):
                wtkey = self.encode_key(i << 32)
                wtval = self.encode_value(i << 32)
                self.traceapi('cursor[' + str(wtkey) + '] = ' + str(wtval))
                cursor[wtkey] = wtval
            cursor.close()
            self.pr('reopening the connection')
            self.conn.close()
            self.conn = self.setUpConnectionOpen(".")
            self.session = self.setUpSessionOpen(self.conn)

    def bits_to_triple(self, bits):
        major = (bits >> 32) & 0xffffffff
        minor = (bits >> 16) & 0xffff
        version = (bits) & 0xffff
        return [major, minor, version]

    def triple_to_bits(self, major, minor, version):
        return ((major & 0xffffffff) << 32) | ((minor & 0xffff) << 16) | (version & 0xffff)

    def key_to_bits(self, key):
        pass

    def stretch_content(self, s, sizes):
        result = s
        if sizes != None:
            sha224 = hashlib.sha224(s.encode())
            md5 = sha224.digest()
            low = sizes[0] - len(s)
            if low < 0:
                low = 0
            high = sizes[1] - len(s)
            if high < 0:
                high = 0
            diff = high - low
            nextra = (md5[4] % (diff + 1)) + low
            extra = sha224.hexdigest()
            while len(extra) < nextra:
                extra = extra + extra
            result = s + extra[0:nextra]
        return result

    def check_content(self, s, sizes):
        if sizes != None:
            stretched = self.stretch_content(s[0:20], sizes)
            self.assertEquals(s, stretched)

    # There are variants of {encode,decode}_{key,value} to be
    # used with each table kind: 'row', 'col', 'fix'

    def encode_key_row(self, bits):
        # Prepend 0's to make the string exactly len 20
        # 64 bits fits in 20 decimal digits
        # Then, if we're configured to have a longer key
        # size, we'll append some additional length
        # that can be regenerated based on the first part of the key
        result = str(bits)
        result = '00000000000000000000'[len(result):] + result
        if self.keysizes != None:
            result = self.stretch_content(result, self.keysizes)
        return result

    def decode_key_row(self, s):
        self.check_content(s, self.keysizes)
        return int(s[0:20])

    def encode_value_row_or_col(self, bits):
        # We use the same scheme as key.  So the 20 digits will
        # be the same, but the last part may well be different
        # if the size configuration is different.
        result = str(bits)
        result = '00000000000000000000'[len(result):] + result
        if self.valuesizes != None:
            result = self.stretch_content(result, self.valuesizes)
        return result

    def decode_value_row_or_col(self, s):
        self.check_content(s, self.valuesizes)
        return int(s[0:20])

    def encode_key_col_or_fix(self, bits):
        # 64 bit key
        maj = ((bits >> 32) & 0xffffffff) + 1
        min = (bits >> 16) & 0xffff
        return self.recno((maj << 16) | min)

    def decode_key_col_or_fix(self, bits):
        maj = ((bits << 16) & 0xffffffff) - 1
        min = bits & 0xffff
        return ((maj << 32) | (min << 16))

    def encode_value_fix(self, bits):
        # can only encode only 8 bits
        maj = ((bits >> 32) & 0xff)
        min = (bits >> 16) & 0xff
        return (maj ^ min)

    def decode_value_fix(self, s):
        return int(s)

    def setpos(self, newpos, isforward):
        length = len(self.bitlist)
        while newpos >= 0 and newpos < length:
            if not self.isrow and self.bitlist[newpos] == self.DELETED:
                if isforward:
                    newpos = newpos + 1
                else:
                    newpos = newpos - 1
            else:
                self.curpos = newpos
                self.nopos = False
                self.curremoved = False
                self.curbits = self.bitlist[newpos]
                return True
        if newpos < 0:
            self.curpos = -1
        else:
            self.curpos = length - 1
        self.nopos = True
        self.curremoved = False
        self.curbits = 0xffffffffffff
        return False

    def cur_first(self, cursor, expect=0):
        self.setpos(0, True)
        self.traceapi('cursor.first()')
        self.assertEquals(0, cursor.reset())
        self.assertEquals(expect, cursor.next())
        self.curremoved = False

    def cur_last(self, cursor, expect=0):
        self.setpos(len(self.bitlist) - 1, False)
        self.traceapi('cursor.last()')
        self.assertEquals(0, cursor.reset())
        self.assertEquals(expect, cursor.prev())
        self.curremoved = False

    def cur_update(self, cursor, key):
        # TODO:
        pass

    def bitspos(self, bits):
        list = self.bitlist
        return next(i for i in range(len(list)) if list[i] == bits)

    def cur_insert(self, cursor, major, minor):
        bits = self.triple_to_bits(major, minor, 0)
        if bits not in self.vers:
            self.bitlist.append(bits)
            if self.isrow:
                #TODO: why doesn't self.bitlist.sort() work?
                self.bitlist = sorted(self.bitlist)
            self.vers[bits] = 0
        else:
            raise Exception('cur_insert: key already exists: ' + str(major) + ',' + str(minor))
        pos = self.bitspos(bits)
        self.setpos(pos, True)
        wtkey = self.encode_key(bits)
        wtval = self.encode_value(bits)
        self.traceapi('cursor[' + str(wtkey) + '] = ' + str(wtval))
        cursor[wtkey] = wtval

    def cur_remove_here(self, cursor):
        # TODO: handle the exception case
        if self.nopos:
            expectException = True
        else:
            expectException = False
            del self.vers[self.curbits & 0xffffffff0000]
            if self.isrow:
                self.bitlist.pop(self.curpos)
                self.setpos(self.curpos - 1, True)
                self.nopos = True
            else:
                self.bitlist[self.curpos] = self.DELETED
            self.curremoved = True
        self.traceapi('cursor.remove()')
        cursor.remove()

    def cur_recno_search(self, cursor, recno):
        wtkey = self.recno(recno)
        self.traceapi('cursor.set_key(' + str(wtkey) + ')')
        cursor.set_key(wtkey)
        if recno > 0 and recno <= len(self.bitlist):
            want = 0
        else:
            want = wiredtiger.WT_NOTFOUND
        self.traceapi('cursor.search()')
        self.check_cursor_ret(cursor.search(), want)

    def cur_search(self, cursor, major, minor):
        bits = self.triple_to_bits(major, minor, 0)
        wtkey = self.encode_key(bits)
        self.traceapi('cursor.set_key(' + str(wtkey) + ')')
        cursor.set_key(wtkey)
        if bits in self.vers:
            want = 0
        else:
            want = wiredtiger.WT_NOTFOUND
        self.traceapi('cursor.search()')
        self.check_cursor_ret(cursor.search(), want)

    def check_cursor_ret(self, ret, want):
        if ret != want:
            if ret == 0:
                self.fail('cursor did not return NOTFOUND')
            elif ret == wiredtiger.WT_NOTFOUND:
                self.fail('cursor returns NOTFOUND unexpectedly')
            else:
                self.fail('unexpected return from cursor: ' + ret)

    def cur_check_forward(self, cursor, n):
        if n < 0:
            n = len(self.bitlist)
        for i in range(n):
            self.cur_next(cursor)
            self.cur_check_here(cursor)
            if self.nopos:
                break

    def cur_next(self, cursor):
        # Note: asymmetric with cur_previous, nopos corresponds to 'half'
        if self.setpos(self.curpos + 1, True):
            wantret = 0
        else:
            wantret = wiredtiger.WT_NOTFOUND
        self.traceapi('cursor.next()')
        self.check_cursor_ret(cursor.next(), wantret)

    def cur_check_backward(self, cursor, n):
        if n < 0:
            n = len(self.bitlist)
        for i in range(n):
            self.cur_previous(cursor)
            self.cur_check_here(cursor)
            if self.nopos:
                break

    def cur_previous(self, cursor):
        if self.nopos:
            pos = self.curpos
        else:
            pos = self.curpos - 1
        if self.setpos(pos, False):
            wantret = 0
        else:
            wantret = wiredtiger.WT_NOTFOUND
        self.traceapi('cursor.prev()')
        self.check_cursor_ret(cursor.prev(), wantret)

    def cur_check_here(self, cursor):
        # Cannot check immediately after a remove, since the K/V in the cursor
        # does not correspond to anything
        keymsg = '/requires key be set/'
        valuemsg = '/requires value be set/'
        if self.curremoved:
            raise Exception('cur_check_here: cursor.get_key, get_value are not valid')
        elif self.nopos:
            self.traceapi_before('cursor.get_key()')
            self.assertRaisesWithMessage(wiredtiger.WiredTigerError,
                cursor.get_key, keymsg)
            self.traceapi_after('<unknown>')
            self.traceapi_before('cursor.get_value()')
            self.assertRaisesWithMessage(wiredtiger.WiredTigerError,
                cursor.get_value, valuemsg)
            self.traceapi_after('<unknown>')
        else:
            bits = self.curbits
            self.traceapi_before('cursor.get_key()')
            wtkey = cursor.get_key()
            self.traceapi_after(str(wtkey))
            if self.isrow:
                self.cur_check(cursor, wtkey, self.encode_key(bits), True)
            else:
                self.cur_check(cursor, wtkey, self.bitspos(bits) + 1, True)
            self.traceapi_before('cursor.get_value()')
            wtval = cursor.get_value()
            self.traceapi_after(str(wtval))
            self.cur_check(cursor, wtval, self.encode_value(bits), False)

    def dumpbitlist(self):
        print('bits array:')
        for bits in self.bitlist:
            print('  ' + str(self.bits_to_triple(bits)) + ' = ' + str(bits))

    def _cursor_key_to_string(self, k):
            return str(self.bits_to_triple(self.decode_key(k))) + ' = ' + str(k)

    def _cursor_value_to_string(self, v):
            return str(self.bits_to_triple(self.decode_value(v))) + ' = ' + v

    def _dumpcursor(self, cursor):
        print('cursor')
        cursor.reset()
        for k,v in cursor:
            print('  ' + self._cursor_key_to_string(k) + ' ' +
                  self._cursor_value_to_string(v))

    def cur_dump_here(self, cursor, prefix):
        try:
            k = self._cursor_key_to_string(cursor.get_key())
        except:
            k = '[invalid]'
        try:
            v = self._cursor_value_to_string(cursor.get_value())
        except:
            v = '[invalid]'
        print(prefix + k + ' ' + v)

    def cur_check(self, cursor, got, want, iskey):
        if got != want:
            if iskey:
                goti = self.decode_key(got)
                wanti = self.decode_key(want)
            else:
                goti = self.decode_value(got)
                wanti = self.decode_value(want)
            gotstr = str(self.bits_to_triple(goti))
            wantstr = str(self.bits_to_triple(wanti))
            self.dumpbitlist()
            # Note: dumpcursor() resets the cursor position,
            # but we're about to issue a fatal error, so it's okay
            self._dumpcursor(cursor)
            if iskey:
                kind = 'key'
            else:
                kind = 'value'
            self.fail('mismatched ' + kind + ', want: ' + wantstr + ', got: ' + gotstr)

if __name__ == '__main__':
    wttest.run()
