''' WHAT: Simple test framework for checking algorithms

TASK:
*Handle output that's an object, eg. bst that gets modified
*option3: optional param - Class (accept input/output as arrays and TURN INTO object)
(option1: optional param - comparison function (instead of simple "!=")
(option2: optional param - Class (automatically deconstruct objects in arrays)
'''
import copy


def init(*args, **kwargs):
    return TestSuite(*args, **kwargs)

class TestSuite:
    def __init__(self, tests, dataStructures=None):
        self.tests = tests
        self.converter = DataStructureConverter(dataStructures) if dataStructures is not None else None

    def test(self, function):
        print('FUNCTION: {}'.format(function.__name__))
        tests = copy.deepcopy(self.tests)

        for i, test in enumerate(tests):
            params, expected = test

            try:
                actual = self.runFunction(function, params)
                if actual != expected:
                    self.printError(i+1, params, expected, actual)
                    return

            except Exception as error:
                self.printError(i+1, params, expected, 'ERROR')
                raise error

    def printError(self, iteration, params, expected, actual):
        print()
        print('ERROR: Iteration {}'.format(iteration))
        print()
        stringifiedParams = ', '.join([str(param) for param in params])
        print('input:            {}'.format(stringifiedParams))
        print('ouptut expected:  {}'.format(expected))
        print('output actual:    {}'.format(actual))
        print()

    def runFunction(self, function, params):
        if self.converter is not None:
            params = self.converter.convertInput(params)

        params = copy.deepcopy(params)
        actual = function(*params)

        if self.converter is not None:
            actual = self.converter.convertOuptut(actual)

        return actual
        
class DataStructureConverter:
    def __init__(self, config):
        self.config = config
        self.arrayToDs = {
            'SinglyLinkedList': self.createSinglyLinkedList,
            #'DoublyLinkedList': createSinglyLinkedList,
            #'BinaryTree': createBinaryTree,
            #'Queue': createQueue,
            #'Stack': createStack,
        }

        self.dsToArray = {
            'SinglyLinkedList': self.createArrayFromSinglyLinkedList,
            #'DoublyLinkedList': createSinglyLinkedList,
            #'BinaryTree': createBinaryTree,
            #'Queue': createQueue,
            #'Stack': createStack,
        }


    def convertInput(self, params):
        if isinstance(self.config, str):
            converted = []
            for param in params:
                ds = self.convertArrayToDs(param, self.config)
                converted.append(ds)
            return converted
                
        elif isinstance(self.config, dict):
            converted = []
            for param, dsName in zip(params, self.config['input']):
                if not isinstance(dsName, str):
                    converted.append(param)
                else:
                    ds = self.convertArrayToDs(param, dsName)
                    converted.append(ds)
            return converted

        else:
            raise Exception('ERROR: This is not the right format for dataStructure: {}'.format(self.config))

    def convertOuptut(self, output):
        if isinstance(self.config, str):
            return self.convertDsToArray(output, self.config)
        elif isinstance(self.config, dict):
            return self.convertDsToArray(output, self.config['output'])
        else:
            raise Exception('ERROR: This is not the right format for dataStructure: {}'.format(self.ds))

    def convertArrayToDs(self, array, dsName):
        if dsName not in self.arrayToDs:
            raise Exception('ERROR: Name of dataStructure not supported: {}'.format(dsName))

        dsConstructor = self.arrayToDs[dsName]
        ds = dsConstructor(array)
        return ds

    def convertDsToArray(self, ds, dsName):
        if dsName not in self.dsToArray:
            raise Exception('ERROR: Name of dataStructure not supported: {}'.format(dsName))

        arrayConstructor = self.dsToArray[dsName]
        array = arrayConstructor(ds)
        return array


    class Node:
        # spec determined by leetcode
        def __init__(self, val):
            self.val = val
            self.next = None

    def createSinglyLinkedList(self, array, storeInArray=False):
        if storeInArray:
            container = []

        head = None
        curr = None
        for elem in array:
            node = self.Node(elem)
            if storeInArray:
                container.append(node)
            if head is None:
                head = node
                curr = node
                continue

            curr.next = node
            curr = node
                
        if storeInArray:
            return container
        return head

    def createArrayFromSinglyLinkedList(self, head):
        array = []
        while head is not None:
            array.append(head.val)
            head = head.next
        return array


    # custom
    def createIntersectingLinkedLists(self, nA, nB, nIntersection):
        headA = self.createSinglyLinkedList(range(nA))
        headB = self.createSinglyLinkedList(range(nA, nA+nB))

        if nIntersection is None or nIntersection == 0:
            return headA, headB, None

        headI = self.createSinglyLinkedList(range(nA+nB, nA+nB+nIntersection))
        if headA is None:
            headA = headI
        else:
            self.getEndofList(headA).next = headI

        if headB is None:
            headB = headI
        else:
            self.getEndofList(headB).next = headI

        return headA, headB, headI

    def getEndofList(self, head):
        while head is not None and head.next is not None:
            head = head.next
        return head


### Example usage
def main():
    import sys
    sys.path.append('/Users/Abe/my/codingPractice/algos')
    import testSuite

    tests = [
        ([[3,2,1,5,6,4], 2], 5),
        ([[3,2,3,1,2,4,5,5,6], 4], 4)
    ]

    t = testSuite.init(tests)
    t.test(v1)
    t.test(v2)
    t.test(v3)
