class LinkedList_Test_Operations_On_List_with_1000_Random_Elements(unittest.TestCase):
    def setUp(self):
        self._linked_list = LinkedList()
        self._shadow_list = []
        for i in range(10):
            random_element = randint(0, 1000)
            self._linked_list.append(random_element)
            self._shadow_list.append(random_element)#will keep track of count and index 
    
    def test_count_index_remove_index_count_on_each_removal(self):
        """
        Check count, index before and after each predictable removal operation 
        """
        for i in self._shadow_list[:]:
            self.assertEqual(self._linked_list.length, len(self._shadow_list))
            
            self.assertEqual(self._linked_list.index(i), self._shadow_list.index(i))
            
            self._shadow_list.remove(i)
            self._linked_list.remove(i)
            
            try:
                self._shadow_list.index(i)
                self.assertEqual(self._linked_list.index(i), self._shadow_list.index(i))
            except ValueError:
                self.assertEqual(self._linked_list.index(i), -1)
            
            self.assertEqual(self._linked_list.length, len(self._shadow_list))
class LinkedList_Test_Remove_Non_Existing_Element_From_0_Element_List(unittest.TestCase):
    
    def setUp(self):
        self._linkedList = LinkedList()
    
    def test_count_of_list(self):
        self._linkedList.remove(-1)
        self.assertEqual(self._linkedList.length, 0, 'Length must be 0 for empty list')
    
    def tearDown(self):
        self._linkedList = None
class LinkedList_Test_Check_Head_On_Remove_In_4_Element_List(unittest.TestCase):

    def setUp(self):
        self._linkedList = LinkedList()
        self._linkedList.append(1)
        self._linkedList.append(2)
        self._linkedList.append(3)
        self._linkedList.append(4)
    
    def test_tail_node_on_remove(self):
        #list is [1, 2, 3, 4]
        self._linkedList.remove(3)#removed head. Head must be same node 
        self.assertEqual(self._linkedList.head, 1)
        #Now list is [1, 2, 4]
        self._linkedList.remove(1)#head should next be 2
        self.assertEqual(self._linkedList.head, 2, 'head must be adjusted on deletion of first node')
        #Now list is [2, 4]
        self._linkedList.remove(2)#head should next be 4
        self.assertEqual(self._linkedList.head, 4, 'head must be adjusted on deletion of first node')
        #Now list is [4]
        self._linkedList.remove(4)#head should next be None
        self.assertEqual(self._linkedList.head, None, 'Head must be None on empty list')
        
    def tearDown(self):
        self._linkedList = None    
class LinkedListHashBucket(collections.MutableMapping):
    '''
    A hash bucket is used to hold objects that 'hash to the same value' (collision) in a hash table. 
    This is hash a bucket using a LIST. This masquerades as a python dict in code where it is used.
    
    Note: HASHBUCKET ITERATION YIELDS KEYS. not the key value pairs in the bucket. 
    '''
    def __init__(self):
        self._list_of_kv_pairs = LinkedList() #we use our linked list!
    
    def __len__(self):
        '''
        The number of entries in the bucket!
        '''
        return self._list_of_kv_pairs.length
    
    def get(self, key, default = None):
        '''
        Get object associated with a key and on key miss return specified default. This is there in 
        Python dict and this class masquerades as dict, so we implement it.
        '''
        try:
            value = self[key]
            return value
        except KeyError:
            return default            
    
    def __getitem__(self, key):
        for kv_pair in self._list_of_kv_pairs:
            if kv_pair.key == key:
                return kv_pair.value
        raise KeyError('Key Error: %s ' % repr(key))
    
    def __delitem__(self, key):
        for kv_pair in self._list_of_kv_pairs:
            if kv_pair.key == key:
                self._list_of_kv_pairs.remove(kv_pair)
                return    
        raise KeyError('Key Error: %s ' % repr(key))
    
    def __setitem__(self, key, obj):
        for kv_pair in self._list_of_kv_pairs:
            if kv_pair.key == key:
                kv_pair.value = obj
                return    
        self._list_of_kv_pairs.append(KeyValuePair(key = key, value = obj))
  
    def __iter__(self):
        for kvpair in self._list_of_kv_pairs:
            yield kvpair.key
class LinkedList_Test_Remove_Middle_Element_From_4_Element_List(unittest.TestCase):
    
    def setUp(self):
        self._linkedList = LinkedList()
        self._linkedList.append(1)
        self._linkedList.append(2)
        self._linkedList.append(3)
        self._linkedList.append(4)
    
    def test_count_of_list(self):
        self._linkedList.remove(3)
        self.assertEqual(self._linkedList.length, 3, 'Length not valid after trying to remove non-existin element')

    def tearDown(self):
        self._linkedList = None
class LinkedList_Test_Check_Tail_On_Remove_In_4_Element_List(unittest.TestCase):
    
    def setUp(self):
        self._linkedList = LinkedList()
        self._linkedList.append(1)
        self._linkedList.append(2)
        self._linkedList.append(3)
        self._linkedList.append(4)
    
    def test_tail_node_on_remove(self):
        self._linkedList.remove(1)#removed head. Tail must be same node 
        self.assertEqual(self._linkedList.tail, 4)
        #Now list is [2, 3, 4]
        self._linkedList.remove(4)#tail should next be 3
        self.assertEqual(self._linkedList.tail, 3, 'Tail must be adjusted on deletion of last node')
        #Now list is [2, 3]
        self._linkedList.remove(2)#tail should next be 3
        self.assertEqual(self._linkedList.tail, 3, 'Tail must be adjusted on deletion of last node')
        #Now list is [3]
        self._linkedList.remove(3)#tail should next be None
        self.assertEqual(self._linkedList.tail, None, 'Tail must be None on empty list')
    def tearDown(self):
        self._linkedList = None    
    print('Tail of list is: %s' % str(link_list.tail))
    print('Print list by iterating')

    #Find an element's index
    element = 4
    element_index = link_list.index(element)
    if element_index == -1:
        print('Element %s was not found on list' % str(element))
    else:
        print('Element %s found at index %s' %
              (str(element), str(element_index)))

    #Lets delete 7
    print('Deleting 7')
    element_to_remove = 7
    link_list.remove(element_to_remove)

    #Try to find it.
    print('Trying to find %s' % str(element_to_remove))
    index_of_deleted = link_list.index(element_to_remove)
    print('Index of deleted element is %s' % str(index_of_deleted))
    #print list again
    print_list(link_list)

    #
    #Lets insert 7 at index 5
    print('Insert 7 into the list at index 4')
    element_to_insert = 7
    index_at = 5
    link_list.insert_at(index=index_at, element=element_to_insert)
    print_list(link_list)