import copy

import pytest
import os
import io
import json
import sys
from decimal import Decimal
from unittest import mock
from deepdiff import Delta, DeepDiff
from deepdiff.helper import np, number_to_string, TEXT_VIEW, DELTA_VIEW, CannotCompare, FlatDeltaRow, FlatDataAction, SetOrdered
from deepdiff.path import GETATTR, GET
from deepdiff.delta import (
    ELEM_NOT_FOUND_TO_ADD_MSG,
    VERIFICATION_MSG, VERIFY_BIDIRECTIONAL_MSG, not_found, DeltaNumpyOperatorOverrideError,
    BINIARY_MODE_NEEDED_MSG, DELTA_AT_LEAST_ONE_ARG_NEEDED, DeltaError,
    INVALID_ACTION_WHEN_CALLING_GET_ELEM, INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM,
    INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM, INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER,
    FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG, UNABLE_TO_GET_PATH_MSG, NOT_VALID_NUMPY_TYPE)
from deepdiff.serialization import (
    DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT, DELTA_ERROR_WHEN_GROUP_BY,
    json_dumps, json_loads,
)

from tests import PicklableClass, parameterize_cases, CustomClass, CustomClass2


class TestBasicsOfDelta:

    def test_from_null_delta_json(self):
        t1 = None
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        delta = Delta(diff, serializer=json_dumps)
        dump = delta.dumps()
        delta2 = Delta(dump, deserializer=json_loads)
        assert delta2 + t1 == t2
        assert t1 + delta2 == t2
        with pytest.raises(ValueError) as exc_info:
            t2 - delta
        assert 'Please recreate the delta with bidirectional=True' == str(exc_info.value)
        delta = Delta(diff, serializer=json_dumps, bidirectional=True)
        assert t2 - delta == t1

    def test_to_null_delta1_json(self):
        t1 = 1
        t2 = None
        diff = DeepDiff(t1, t2)
        delta = Delta(diff, serializer=json_dumps)
        dump = delta.dumps()
        delta2 = Delta(dump, deserializer=json_loads)
        assert delta2 + t1 == t2
        assert t1 + delta2 == t2

    def test_to_null_delta2_json(self):
        t1 = [1, 2, 3, 5]
        t2 = None
        diff = DeepDiff(t1, t2)
        delta = Delta(diff)

        assert delta + t1 == t2
        assert t1 + delta == t2

    def test_list_difference_add_delta(self):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = {'iterable_item_added': {'root[3]': 5, 'root[2]': 3}}
        delta = Delta(diff)

        assert delta + t1 == t2
        assert t1 + delta == t2

        flat_result1 = delta.to_flat_rows()
        flat_expected1 = [
            FlatDeltaRow(path=[3], value=5, action='iterable_item_added', type=int),
            FlatDeltaRow(path=[2], value=3, action='iterable_item_added', type=int),
        ]

        assert flat_expected1 == flat_result1
        delta2 = Delta(diff=diff, bidirectional=True)
        assert t1 == t2 - delta2

    def test_list_difference_dump_delta(self):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        dump = Delta(diff).dumps()
        delta = Delta(dump)

        assert delta + t1 == t2

    def test_multiple_delta(self):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        t3 = [{1}, 3, 5]
        dump1 = Delta(DeepDiff(t1, t2)).dumps()
        dump2 = Delta(DeepDiff(t2, t3)).dumps()

        delta1 = Delta(dump1)
        delta2 = Delta(dump2)

        assert t1 + delta1 + delta2 == t3

    def test_delta_dump_and_read1(self, tmp_path):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        path = os.path.join(tmp_path, 'delta_test.delta')
        with open(path, 'wb') as the_file:
            Delta(diff).dump(the_file)
        delta = Delta(delta_path=path)
        os.remove(path)
        assert delta + t1 == t2

    def test_delta_dump_and_read2(self, tmp_path):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        delta_content = Delta(diff).dumps()
        path = os.path.join(tmp_path, 'delta_test2.delta')
        with open(path, 'wb') as the_file:
            the_file.write(delta_content)
        delta = Delta(delta_path=path)
        os.remove(path)
        assert delta + t1 == t2

    def test_delta_dump_and_read3(self, tmp_path):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        delta_content = Delta(diff).dumps()
        path = os.path.join(tmp_path, 'delta_test2.delta')
        with open(path, 'wb') as the_file:
            the_file.write(delta_content)
        with pytest.raises(ValueError) as excinfo:
            with open(path, 'r') as the_file:
                delta = Delta(delta_file=the_file)
        assert BINIARY_MODE_NEEDED_MSG[:20] == str(excinfo.value)[:20]
        with open(path, 'rb') as the_file:
            delta = Delta(delta_file=the_file)
        os.remove(path)
        assert delta + t1 == t2

    def test_delta_when_no_arg_passed(self):
        with pytest.raises(ValueError) as excinfo:
            Delta()
        assert DELTA_AT_LEAST_ONE_ARG_NEEDED == str(excinfo.value)

    def test_delta_when_group_by(self):

        t1 = [
            {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
            {'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
        ]

        t2 = [
            {'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
            {'id': 'BB', 'name': 'James', 'last_name': 'Brown'},
        ]

        diff = DeepDiff(t1, t2, group_by='id')

        with pytest.raises(ValueError) as excinfo:
            Delta(diff)
        assert DELTA_ERROR_WHEN_GROUP_BY == str(excinfo.value)

    def test_delta_repr(self):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        delta = Delta(diff)
        options = {
            "<Delta: {'iterable_item_added': {'root[2]': 3, 'root[3]': 5}}>",
            "<Delta: {'iterable_item_added': {'root[3]': 5, 'root[2]': 3}}>"}
        assert repr(delta) in options

    def test_get_elem_and_compare_to_old_value(self):
        delta = Delta({})

        with pytest.raises(DeltaError) as excinfo:
            delta._get_elem_and_compare_to_old_value(
                obj=None, path_for_err_reporting=None, expected_old_value=None, action='ketchup on steak')
        assert INVALID_ACTION_WHEN_CALLING_GET_ELEM.format('ketchup on steak') == str(excinfo.value)

    def test_simple_set_elem_value(self):
        delta = Delta({}, raise_errors=True)

        with pytest.raises(DeltaError) as excinfo:
            delta._simple_set_elem_value(
                obj=None, elem=None, value=None, action='mayo on salad', path_for_err_reporting=None)
        assert INVALID_ACTION_WHEN_CALLING_SIMPLE_SET_ELEM.format('mayo on salad') == str(excinfo.value)

        with pytest.raises(DeltaError) as excinfo:
            delta._simple_set_elem_value(
                obj={}, elem={1}, value=None, action=GET, path_for_err_reporting='mypath')
        assert str(excinfo.value) in {"Failed to set mypath due to unhashable type: 'set'",
                                      "Failed to set mypath due to 'set' objects are unhashable"}

    def test_simple_delete_elem(self):
        delta = Delta({}, raise_errors=True)

        with pytest.raises(DeltaError) as excinfo:
            delta._simple_delete_elem(
                obj=None, elem=None, action='burnt oil', path_for_err_reporting=None)
        assert INVALID_ACTION_WHEN_CALLING_SIMPLE_DELETE_ELEM.format('burnt oil') == str(excinfo.value)

        with pytest.raises(DeltaError) as excinfo:
            delta._simple_delete_elem(
                obj={}, elem=1, action=GET, path_for_err_reporting='mypath')
        assert "Failed to set mypath due to 1" == str(excinfo.value)

    def test_raise_error(self):
        t1 = [1, 2, [3, 5, 6]]
        t2 = [2, 3, [3, 6, 8]]
        diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)
        delta = Delta(diff, raise_errors=False)
        t3 = [1, 2, 3, 5]
        t4 = t3 + delta
        assert [3, 2, 3, 5] == t4

        delta2 = Delta(diff, raise_errors=True)

        with pytest.raises(DeltaError) as excinfo:
            t3 + delta2
        assert "Unable to get the item at root[2][1]" == str(excinfo.value)

    def test_identical_delta(self):
        delta = Delta({})

        t1 = [1, 3]
        assert t1 + delta == t1

        flat_result1 = delta.to_flat_rows()
        flat_expected1 = []

        assert flat_expected1 == flat_result1

    def test_delta_mutate(self):
        t1 = [1, 2]
        t2 = [1, 2, 3, 5]
        diff = DeepDiff(t1, t2)
        delta = Delta(diff, mutate=True)
        t1 + delta
        assert t1 == t2

    @mock.patch('deepdiff.delta.logger.error')
    def test_list_difference_add_delta_when_index_not_valid(self, mock_logger):
        t1 = [1, 2]
        diff = {'iterable_item_added': {'root[20]': 3, 'root[3]': 5}}
        delta = Delta(diff, log_errors=False)
        assert delta + t1 == t1

        # since we sort the keys by the path elements, root[3] is gonna be processed before root[20]
        expected_msg = ELEM_NOT_FOUND_TO_ADD_MSG.format(3, 'root[3]')

        delta2 = Delta(diff, bidirectional=True, raise_errors=True, log_errors=False)
        with pytest.raises(ValueError) as excinfo:
            delta2 + t1
        assert expected_msg == str(excinfo.value)
        assert not mock_logger.called

        delta3 = Delta(diff, bidirectional=True, raise_errors=True, log_errors=True)
        with pytest.raises(ValueError) as excinfo:
            delta3 + t1
        assert expected_msg == str(excinfo.value)
        mock_logger.assert_called_once_with(expected_msg)

    def test_list_difference3_delta(self):
        t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 5]}}
        t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 5]}}
        diff = {
            'values_changed': {
                "root[4]['b'][2]": {
                    'new_value': 2,
                    'old_value': 5
                },
                "root[4]['b'][1]": {
                    'new_value': 3,
                    'old_value': 2
                }
            },
            'iterable_item_added': {
                "root[4]['b'][3]": 5
            }
        }
        delta = Delta(diff)

        assert delta + t1 == t2
        assert t1 + delta == t2

        flat_result1 = delta.to_flat_rows()
        flat_expected1 = [
            FlatDeltaRow(path=[4, 'b', 2], action='values_changed', value=2, old_value=5, type=int, old_type=int),
            FlatDeltaRow(path=[4, 'b', 1], action='values_changed', value=3, old_value=2, type=int, old_type=int),
            FlatDeltaRow(path=[4, 'b', 3], value=5, action='iterable_item_added', type=int),
        ]

        assert flat_expected1 == flat_result1

        delta2 = Delta(diff=diff, bidirectional=True)
        assert t1 == t2 - delta2

    def test_list_difference_delta_raises_error_if_prev_value_does_not_match(self):
        t1 = [1, 2, 6]
        t2 = [1, 3, 2, 5]
        diff = {
            'values_changed': {
                "root[2]": {
                    'new_value': 2,
                    'old_value': 5
                },
                "root[1]": {
                    'new_value': 3,
                    'old_value': 2
                }
            },
            'iterable_item_added': {
                "root[3]": 5
            }
        }

        expected_msg = VERIFICATION_MSG.format('root[2]', 5, 6, VERIFY_BIDIRECTIONAL_MSG)

        delta = Delta(diff, bidirectional=True, raise_errors=True)
        with pytest.raises(ValueError) as excinfo:
            delta + t1
        assert expected_msg == str(excinfo.value)

        delta2 = Delta(diff, bidirectional=False)
        assert delta2 + t1 == t2

        flat_result2 = delta2.to_flat_rows()
        flat_expected2 = [
            FlatDeltaRow(path=[2], action='values_changed', value=2, old_value=5, type=int, old_type=int),
            FlatDeltaRow(path=[1], action='values_changed', value=3, old_value=2, type=int, old_type=int),
            FlatDeltaRow(path=[3], value=5, action='iterable_item_added', type=int),
        ]

        assert flat_expected2 == flat_result2

    def test_list_difference_delta1(self):
        t1 = {
            1: 1,
            2: 2,
            3: 3,
            4: {
                "a": "hello",
                "b": [1, 2, 'to_be_removed', 'to_be_removed2']
            }
        }
        t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2]}}

        diff = {
            'iterable_item_removed': {
                "root[4]['b'][2]": "to_be_removed",
                "root[4]['b'][3]": 'to_be_removed2'
            }
        }
        delta = Delta(diff)

        assert delta + t1 == t2

        flat_result = delta.to_flat_rows()
        flat_expected = [
            FlatDeltaRow(path=[4, 'b', 2], value='to_be_removed', action='iterable_item_removed', type=str),
            FlatDeltaRow(path=[4, 'b', 3], value='to_be_removed2', action='iterable_item_removed', type=str),
        ]

        assert flat_expected == flat_result

        delta2 = Delta(diff=diff, bidirectional=True)
        assert t1 == t2 - delta2

    @mock.patch('deepdiff.delta.logger.error')
    def test_list_difference_delta_if_item_is_already_removed(self, mock_logger):
        t1 = [1, 2, 'to_be_removed']
        t2 = [1, 2]

        diff = {
            'iterable_item_removed': {
                "root[2]": "to_be_removed",
                "root[3]": 'to_be_removed2'
            }
        }
        delta = Delta(diff, bidirectional=True, raise_errors=True)
        assert delta + t1 == t2, (
            "We used to throw errors when the item to be removed was not found. "
            "Instead, we try to look for the item to be removed even when the "
            "index of it in delta is different than the index of it in the object."
        )

        delta2 = Delta(diff, bidirectional=False, raise_errors=False)
        assert t1 + delta2 == t2
        expected_msg = UNABLE_TO_GET_PATH_MSG.format('root[3]')
        assert 0 == mock_logger.call_count

    def test_list_difference_delta_does_not_raise_error_if_prev_value_changed(self):
        t1 = {
            1: 1,
            2: 2,
            3: 3,
            4: {
                "a": "hello",
                "b": [1, 2, 'wrong', 'to_be_removed2']
            }
        }
        t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2]}}

        diff = {
            'iterable_item_removed': {
                "root[4]['b'][2]": "to_be_removed",
                "root[4]['b'][3]": 'to_be_removed2'
            }
        }

        # The previous behavior was to throw an error here because the original value for "root[4]['b'][2]" was not 'wrong' anymore.
        # However, I decided to change that behavior to what makes more sense and is consistent with the bidirectional flag.
        # No more verify_symmetry flag.
        delta = Delta(diff, bidirectional=True, raise_errors=True)
        assert delta + t1 != t2
        expected = {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': [1, 2, 'wrong']}}
        assert expected == delta + t1

        delta2 = Delta(diff, bidirectional=False, raise_errors=True)
        assert expected == t1 + delta2

    def test_delta_dict_items_added_retain_order(self):
        t1 = {
            6: 6
        }

        t2 = {
            6: 6,
            7: 7,
            3: 3,
            5: 5,
            2: 2,
            4: 4
        }

        expected_delta_dict = {
            'dictionary_item_added': {
                'root[7]': 7,
                'root[3]': 3,
                'root[5]': 5,
                'root[2]': 2,
                'root[4]': 4
            }
        }

        diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0)
        delta_dict = diff._to_delta_dict()
        assert expected_delta_dict == delta_dict
        delta = Delta(diff, bidirectional=False, raise_errors=True)

        result = t1 + delta
        assert result == t2

        assert set(result.keys()) == {6, 7, 3, 5, 2, 4}
        assert set(result.keys()) == set(t2.keys())

        delta2 = Delta(diff=diff, bidirectional=True)
        assert t1 == t2 - delta2

        delta3 = Delta(diff, always_include_values=True, bidirectional=True, raise_errors=True)
        flat_rows_list = delta3.to_flat_rows()
        delta4 = Delta(flat_rows_list=flat_rows_list,
                      always_include_values=True, bidirectional=True, raise_errors=True)
        assert t1 == t2 - delta4
        assert t1 + delta4 == t2


    def test_delta_constr_flat_dict_list_param_preserve(self):
        """
        Issue: https://github.com/seperman/deepdiff/issues/457

        Scenario:
        We found that when a flat_rows_list was provided as a constructor
        parameter for instantiating a new delta, the provided flat_rows_list
        is unexpectedly being mutated/changed, which can be troublesome for the
        caller if they were expecting the flat_rows_list to be used BY COPY
        rather than BY REFERENCE.

        Intent:
        Preserve the original value of the flat_rows_list variable within the
        calling module/function after instantiating the new delta.
        """

        t1 = {
            "individualNames": [
                {
                    "firstName": "Johnathan",
                    "lastName": "Doe",
                    "prefix": "COLONEL",
                    "middleName": "A",
                    "primaryIndicator": True,
                    "professionalDesignation": "PHD",
                    "suffix": "SR",
                    "nameIdentifier": "00001"
                },
                {
                    "firstName": "John",
                    "lastName": "Doe",
                    "prefix": "",
                    "middleName": "",
                    "primaryIndicator": False,
                    "professionalDesignation": "",
                    "suffix": "SR",
                    "nameIdentifier": "00002"
                }
            ]
        }

        t2 = {
            "individualNames": [
                {
                    "firstName": "Johnathan",
                    "lastName": "Doe",
                    "prefix": "COLONEL",
                    "middleName": "A",
                    "primaryIndicator": True,
                    "professionalDesignation": "PHD",
                    "suffix": "SR",
                    "nameIdentifier": "00001"
                },
                {
                    "firstName": "Johnny",
                    "lastName": "Doe",
                    "prefix": "",
                    "middleName": "A",
                    "primaryIndicator": False,
                    "professionalDesignation": "",
                    "suffix": "SR",
                    "nameIdentifier": "00003"
                }
            ]
        }

        def compare_func(item1, item2, level=None):
            print("*** inside compare ***")
            it1_keys = item1.keys()

            try:

                # --- individualNames ---
                if 'nameIdentifier' in it1_keys and 'lastName' in it1_keys:
                    match_result = item1['nameIdentifier'] == item2['nameIdentifier']
                    print("individualNames - matching result:", match_result)
                    return match_result
                else:
                    print("Unknown list item...", "matching result:", item1 == item2)
                    return item1 == item2
            except Exception:
                raise CannotCompare() from None
        # ---------------------------- End of nested function

        # This diff should show:
        # 1 - list item (with an index on the path) being added
        # 1 - list item (with an index on the path) being removed
        diff = DeepDiff(t1, t2, report_repetition=True,
                             ignore_order=True, iterable_compare_func=compare_func, cutoff_intersection_for_pairs=1)

        # Now create a flat_rows_list from a delta instantiated from the diff...
        temp_delta = Delta(diff, always_include_values=True, bidirectional=True, raise_errors=True)
        flat_rows_list = temp_delta.to_flat_rows()

        # Note: the list index is provided on the path value...
        assert flat_rows_list == [FlatDeltaRow(path=['individualNames', 1],
                                   value={'firstName': 'Johnny',
                                             'lastName': 'Doe',
                                             'prefix': '',
                                             'middleName': 'A',
                                             'primaryIndicator': False,
                                             'professionalDesignation': '',
                                             'suffix': 'SR',
                                             'nameIdentifier': '00003'},
                                   action='unordered_iterable_item_added',
                                   type=dict),
                                  FlatDeltaRow(path=['individualNames', 1],
                                   value={'firstName': 'John',
                                             'lastName': 'Doe',
                                             'prefix': '',
                                             'middleName': '',
                                             'primaryIndicator': False,
                                             'professionalDesignation': '',
                                             'suffix': 'SR',
                                             'nameIdentifier': '00002'},
                                   action='unordered_iterable_item_removed',
                                   type=dict),
                                  ]

        preserved_flat_dict_list = copy.deepcopy(flat_rows_list)  # Use this later for assert comparison

        # Now use the flat_rows_list to instantiate a new delta...
        delta = Delta(flat_rows_list=flat_rows_list,
                      always_include_values=True, bidirectional=True, raise_errors=True)

        flat_rows_list_again = delta.to_flat_rows()
        # if the flat_rows_list is (unexpectedly) mutated, it will be missing the list index number on the path value.
        old_mutated_list_missing_indexes_on_path = [FlatDeltaRow(path=['individualNames'],
                                         value={'firstName': 'Johnny',
                                                   'lastName': 'Doe',
                                                   'prefix': '',
                                                   'middleName': 'A',
                                                   'primaryIndicator': False,
                                                   'professionalDesignation': '',
                                                   'suffix': 'SR',
                                                   'nameIdentifier': '00003'},
                                         action='unordered_iterable_item_added'),
                                        FlatDeltaRow(path=['individualNames'],
                                         value={'firstName': 'John',
                                                   'lastName': 'Doe',
                                                   'prefix': '',
                                                   'middleName': '',
                                                   'primaryIndicator': False,
                                                   'professionalDesignation': '',
                                                   'suffix': 'SR',
                                                   'nameIdentifier': '00002'},
                                         action='unordered_iterable_item_removed')]

        # Verify that our fix in the delta constructor worked...
        assert flat_rows_list != old_mutated_list_missing_indexes_on_path
        assert flat_rows_list == preserved_flat_dict_list
        assert flat_rows_list == flat_rows_list_again


picklalbe_obj_without_item = PicklableClass(11)
del picklalbe_obj_without_item.item


DELTA_CASES = {
    'delta_case0': {
        't1': frozenset([1, 2, 'B']),
        't2': frozenset([1, 2, 'B']),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {},
    },
    'delta_case1': {
        't1': frozenset([1, 2, 'B']),
        't2': frozenset([1, 2, 3, 5]),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'set_item_removed': {'root': {'B'}}, 'set_item_added': {'root': {3, 5}}},
    },
    'delta_case2': {
        't1': [1, 2, 'B'],
        't2': [1, 2, 3, 5],
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'type_changes': {
                'root[2]': {
                    'old_type': str,
                    'new_type': int,
                    'new_value': 3
                }
            },
            'iterable_item_added': {
                'root[3]': 5
            }
        },
    },
    'delta_case3': {
        't1': [1, 2, '3'],
        't2': [1, 2, 3],
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'type_changes': {
                'root[2]': {
                    'old_type': str,
                    'new_type': int,
                }
            }
        },
    },
    'delta_case4': {
        't1': 3,
        't2': '3',
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'type_changes': {
                'root': {
                    'old_type': int,
                    'new_type': str,
                }
            }
        },
    },
    'delta_case5': {
        't1': 3.2,
        't2': Decimal('3.2'),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'type_changes': {
                'root': {
                    'old_type': float,
                    'new_type': Decimal,
                    'new_value': Decimal('3.2')
                }
            }
        },
    },
    'delta_case6': {
        't1': (1, 2),
        't2': (1, 3),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[1]': {
                    'new_value': 3
                }
            }
        },
    },
    'delta_case7': {
        't1': (1, 2, 5),
        't2': (1, ),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_item_removed': {
                'root[1]': 2,
                'root[2]': 5
            }
        },
    },
    'delta_case8': {
        't1': (1, 2, 5),
        't2': (1, 3),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_item_removed': {
                'root[2]': 5
            },
            'values_changed': {
                'root[1]': {
                    'new_value': 3
                }
            }
        },
    },
    'delta_case9': {
        't1': (1, ),
        't2': (1, 3),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_item_added': {
                'root[1]': 3
            },
        },
    },
    'delta_case10': {
        't1': {
            2: 2
        },
        't2': {
            2: 2,
            3: 3
        },
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'dictionary_item_added': {
                'root[3]': 3
            },
        },
    },
    'delta_case11': {
        't1': {
            1: 1,
            2: 2
        },
        't2': {
            2: 2,
            3: 3
        },
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'dictionary_item_added': {
                'root[3]': 3
            },
            'dictionary_item_removed': {
                'root[1]': 1
            },
        },
    },
    'delta_case12': {
        't1': PicklableClass(10),
        't2': PicklableClass(11),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root.item': {
                    'new_value': 11
                }
            }
        }
    },
    'delta_case13': {
        't1': PicklableClass(10),
        't2': picklalbe_obj_without_item,
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'attribute_removed': {
                'root.item': 10
            }
        }
    },
    'delta_case14': {
        't1': picklalbe_obj_without_item,
        't2': PicklableClass(10),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'attribute_added': {
                'root.item': 10
            }
        }
    },
    'delta_case14b_threshold_to_diff_deeper': {
        't1': picklalbe_obj_without_item,
        't2': PicklableClass(11),
        'deepdiff_kwargs': {'threshold_to_diff_deeper': 0.5},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'attribute_added': {'root.item': 11}}
    },
    'delta_case15_diffing_simple_numbers': {
        't1': 1,
        't2': 2,
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'values_changed': {'root': {'new_value': 2}}}
    },
    'delta_case16_diffmultiline_text': {
        't1': {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': 'world\n1\n2\nEnd'}},
        't2': {1: 1, 2: 2, 3: 3, 4: {'a': 'hello', 'b': 'world!\nGoodbye!\n1\n2\nEnd'}},
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'values_changed': {"root[4]['b']": {'new_value': 'world!\nGoodbye!\n1\n2\nEnd'}}}
    },
    'delta_case17_numbers_and_letters': {
        't1': [0, 1, 2, 3, 4, 5, 6, 7, 8],
        't2': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'],
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'iterable_item_added': {'root[9]': 'a', 'root[10]': 'b', 'root[11]': 'c'}}
    },
    'delta_case18_numbers_and_letters': {
        't1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'],
        't2': [0, 1, 2, 3, 4, 5, 6, 7, 8],
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'iterable_item_removed': {'root[9]': 'a', 'root[10]': 'b', 'root[11]': 'c'}}
    },
    'delta_case19_value_removed_from_the_middle_of_list': {
        't1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'],
        't2': [0, 1, 2, 3, 5, 6, 7, 8, 'a', 'b', 'c'],
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {'directed': True},
        'expected_delta_dict': {'iterable_item_removed': {'root[4]': 4}}
    },
    'delta_case20_quotes_in_path': {
        't1': {"a']['b']['c": 1},
        't2': {"a']['b']['c": 2},
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {'directed': True},
        'expected_delta_dict': {'values_changed': {'root["a\'][\'b\'][\'c"]': {'new_value': 2}}}
    },
    'delta_case21_empty_list_add': {
        't1': {'car_model': [], 'car_model_version_id': 0},
        't2': {'car_model': ['Super Duty F-250'], 'car_model_version_id': 1},
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {'directed': True},
        'expected_delta_dict': {'iterable_item_added': {"root['car_model'][0]": 'Super Duty F-250'}, 'values_changed': {"root['car_model_version_id']": {'new_value': 1}}},
    },
}


DELTA_CASES_PARAMS = parameterize_cases('test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict', DELTA_CASES)


class TestDelta:

    @pytest.mark.parametrize(**DELTA_CASES_PARAMS)
    def test_delta_cases(self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict):
        diff = DeepDiff(t1, t2, **deepdiff_kwargs)
        delta_dict = diff._to_delta_dict(**to_delta_kwargs)
        assert expected_delta_dict == delta_dict, f"test_delta_cases {test_name} failed."
        delta = Delta(diff, bidirectional=False, raise_errors=True)
        assert t1 + delta == t2, f"test_delta_cases {test_name} failed."

        delta2 = Delta(diff, bidirectional=True, raise_errors=True)
        assert t2 - delta2 == t1, f"test_delta_cases {test_name} failed."


DELTA_IGNORE_ORDER_CASES = {
    'delta_ignore_order_case1': {
        't1': [1, 2, 'B', 3],
        't2': [1, 2, 3, 5],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_items_added_at_indexes': {
                'root': {
                    3: 5
                }
            },
            'iterable_items_removed_at_indexes': {
                'root': {
                    2: 'B'
                }
            }
        },
        'expected_t1_plus_delta': 't2',
    },
    'delta_ignore_order_case2': {
        't1': [1, 2, 'B', 3, 'B', 'B', 4],
        't2': [1, 2, 3, 5],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[6]': {
                    'new_value': 5,
                    'new_path': 'root[3]',
                },
            },
            'iterable_items_removed_at_indexes': {
                'root': {
                    2: 'B',
                    4: 'B',
                    5: 'B'
                }
            }
        },
        'expected_t1_plus_delta': 't2',
    },
    'delta_ignore_order_case_reverse2': {
        't1': [1, 2, 3, 5],
        't2': [1, 2, 'B', 3, 'B', 'B', 4],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[3]': {
                    'new_value': 4,
                    'new_path': 'root[6]',
                },
            },
            'iterable_items_added_at_indexes': {
                'root': {
                    2: 'B',
                    4: 'B',
                    5: 'B'
                }
            }
        },
        'expected_t1_plus_delta': 't2',
    },
    'delta_ignore_order_case3': {
        't1': [5, 1, 1, 1, 6],
        't2': [7, 1, 1, 1, 8],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[4]': {
                    'new_value': 7,
                    'new_path': 'root[0]'
                },
                'root[0]': {
                    'new_value': 8,
                    'new_path': 'root[4]'
                }
            }
        },
        'expected_t1_plus_delta': [8, 1, 1, 1, 7],
    },
    'delta_ignore_order_case4': {
        't1': [5, 1, 3, 1, 4, 4, 6],
        't2': [7, 4, 4, 1, 3, 4, 8],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[6]': {
                    'new_value': 7,
                    'new_path': 'root[0]'
                },
                'root[0]': {
                    'new_value': 8,
                    'new_path': 'root[6]'
                }
            },
            'iterable_items_added_at_indexes': {
                'root': {
                    1: 4,
                    2: 4,
                    5: 4,
                    3: 1,
                }
            }
        },
        'expected_t1_plus_delta': [8, 4, 4, 1, 3, 4, 7],
    },
    'delta_ignore_order_case5': {
        't1': (5, 1, 3, 1, 4, 4, 6),
        't2': (7, 4, 4, 1, 3, 4, 8, 1),
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_items_added_at_indexes': {
                'root': {
                    1: 4,
                    2: 4,
                    5: 4
                }
            },
            'values_changed': {
                'root[6]': {
                    'new_value': 7,
                    'new_path': 'root[0]',
                },
                'root[0]': {
                    'new_value': 8,
                    'new_path': 'root[6]',
                }
            }
        },
        'expected_t1_plus_delta': (8, 4, 4, 1, 3, 4, 1, 7),
    },
    'delta_ignore_order_case6': {
        't1': [{1, 2, 3}, {4, 5}],
        't2': [{4, 5, 6}, {1, 2, 3}],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {'set_item_added': {'root[1]': {6}}},
        'expected_t1_plus_delta': [{1, 2, 3}, {4, 5, 6}],
    },
    'delta_ignore_order_case7': {
        't1': [{1, 2, 3}, {4, 5, 'hello', 'right!'}, {4, 5, (2, 4, 7)}],
        't2': [{4, 5, 6, (2, )}, {1, 2, 3}, {5, 'hello', 'right!'}],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'set_item_removed': {
                'root[1]': {4}
            },
            'iterable_items_added_at_indexes': {
                'root': {
                    0: {(2, ), 4, 5, 6}
                }
            },
            'iterable_items_removed_at_indexes': {
                'root': {
                    2: {4, 5, (2, 4, 7)}
                }
            }
        },
        'expected_t1_plus_delta': 't2',
    },
    'delta_ignore_order_case8_multi_dimensional_list': {
        't1': [[1, 2, 3, 4], [4, 2, 2, 1]],
        't2': [[4, 1, 1, 1], [1, 3, 2, 4]],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_items_added_at_indexes': {
                'root[1]': {
                    1: 1,
                    2: 1,
                    3: 1
                }
            },
            'iterable_items_removed_at_indexes': {
                'root[1]': {
                    1: 2,
                    2: 2
                }
            }
        },
        'expected_t1_plus_delta': [[1, 2, 3, 4], [4, 1, 1, 1]],
    },
    'delta_ignore_order_case9': {
        't1': [{
            "path": ["interface1", "ipv1"]
        }, {
            "path": ["interface2", "ipv2"]
        }, {
            "path": ["interface3", "ipv3"]
        }, {
            "path": [{
                "test0": "interface4.0",
                "test0.0": "ipv4.0"
            }, {
                "test1": "interface4.1",
                "test1.1": "ipv4.1"
            }]
        }, {
            "path": ["interface5", "ipv5"]
        }],
        't2': [{
            "path": ["interface1", "ipv1"]
        }, {
            "path": ["interface3", "ipv3"]
        }, {
            "path": [{
                "test0": "interface4.0",
                "test0.0": "ipv4.0"
            }, {
                "test2": "interface4.2",
                "test2.2": "ipv4.0"
            }, {
                "test1": "interface4.1",
                "test1.1": "ipv4.1"
            }]
        }, {
            "path": ["interface6", "ipv6"]
        }, {
            "path": ["interface5", "ipv5"]
        }],
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_items_added_at_indexes': {
                "root[3]['path']": {
                    1: {
                        'test2': 'interface4.2',
                        'test2.2': 'ipv4.0'
                    }
                },
                'root': {
                    3: {
                        'path': [
                            'interface6', 'ipv6'
                        ]
                    }
                }
            },
            'iterable_items_removed_at_indexes': {
                'root': {
                    1: {
                        'path': ['interface2', 'ipv2']
                    }
                }
            }
        },
        'expected_t1_plus_delta':
        't2',
    },
}

DELTA_IGNORE_ORDER_CASES_PARAMS = parameterize_cases(
    'test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta', DELTA_IGNORE_ORDER_CASES)


class TestIgnoreOrderDelta:

    @pytest.mark.parametrize(**DELTA_IGNORE_ORDER_CASES_PARAMS)
    def test_ignore_order_delta_cases(
            self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta, request):
        # test_name = request.node.callspec.id
        diff = DeepDiff(t1, t2, **deepdiff_kwargs)
        delta_dict = diff._to_delta_dict(**to_delta_kwargs)
        assert expected_delta_dict == delta_dict, f"test_ignore_order_delta_cases {test_name} failed"
        delta = Delta(diff, bidirectional=False, raise_errors=True)
        expected_t1_plus_delta = t2 if expected_t1_plus_delta == 't2' else expected_t1_plus_delta
        t1_plus_delta = t1 + delta
        assert t1 + delta == t1_plus_delta, f"test_ignore_order_delta_cases {test_name} 'asserting that delta is not mutated once it is applied' failed"
        # assert not DeepDiff(t1_plus_delta, expected_t1_plus_delta, ignore_order=True), f"test_ignore_order_delta_cases {test_name} failed: diff = {DeepDiff(t1_plus_delta, expected_t1_plus_delta, ignore_order=True)}"


DELTA_NUMPY_TEST_CASES = {
    'delta_case15_similar_to_delta_numpy': {
        't1': [1, 2, 3],
        't2': [1, 2, 5],
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'values_changed': {'root[2]': {'new_value': 5}}},
        'expected_result': 't2'
    },
    'delta_numpy1_operator_override': {
        't1': np.array([1, 2, 3], np.int8),
        't2': np.array([1, 2, 5], np.int8),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'values_changed': {'root[2]': {'new_value': 5}}, '_numpy_paths': {'root': 'int8'}},
        'expected_result': DeltaNumpyOperatorOverrideError
    },
    'delta_numpy2': {
        't1': np.array([1, 2, 3], np.int8),
        't2': np.array([1, 2, 5], np.int8),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'values_changed': {'root[2]': {'new_value': 5}}, '_numpy_paths': {'root': 'int8'}},
        'expected_result': 't2'
    },
    'delta_numpy3_type_change_but_no_value_change': {
        't1': np.array([1, 2, 3], np.int8),
        't2': np.array([1, 2, 3], np.int16),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {'type_changes': {'root': {'old_type': np.int8, 'new_type': np.int16}}},
        'expected_result': 't2'
    },
    'delta_numpy4_type_change_plus_value_change': {
        't1': np.array([1, 2, 3], np.int8),
        't2': np.array([1, 2, 5], np.int16),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': None,  # Not easy to compare since it throws:
        # ValueError: The truth value of an array with more than one element is ambiguous.
        # And we don't want to use DeepDiff for testing the equality inside deepdiff tests themselves!
        'expected_result': 't2'
    },
    'delta_numpy4_type_change_ignore_numeric_type_changes': {
        't1': np.array([1, 2, 3], np.int8),
        't2': np.array([1, 2, 5], np.int16),
        'deepdiff_kwargs': {
            'ignore_numeric_type_changes': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[2]': {
                    'new_value': 5
                }
            },
            '_numpy_paths': {
                'root': 'int16'
            }
        },
        'expected_result': 't2'
    },
    'delta_numpy5_multi_dimensional': {
        't1': np.array([[1, 2, 3], [4, 2, 2]], np.int8),
        't2': np.array([[1, 2, 5], [4, 1, 2]], np.int8),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[0][2]': {
                    'new_value': 5
                },
                'root[1][1]': {
                    'new_value': 1
                }
            },
            '_numpy_paths': {
                'root': 'int8'
            }
        },
        'expected_result': 't2'
    },
    'delta_numpy6_multi_dimensional_ignore_order': {
        't1': np.array([[1, 2, 3, 4], [4, 2, 2, 1]], np.int8),
        't2': np.array([[4, 1, 1, 1], [1, 3, 2, 4]], np.int8),
        'deepdiff_kwargs': {
            'ignore_order': True,
            'report_repetition': True
        },
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'iterable_items_added_at_indexes': {
                'root[1]': {
                    1: 1,
                    2: 1,
                    3: 1
                }
            },
            'iterable_items_removed_at_indexes': {
                'root[1]': {
                    1: 2,
                    2: 2
                }
            },
            '_numpy_paths': {
                'root': 'int8'
            }
        },
        'expected_result': 't2_via_deepdiff'
    },
    'delta_numpy7_arrays_of_different_sizes': {
        't1': np.array([1, 2, 3, 4]),
        't2': np.array([5, 6, 7, 8, 9, 10]),
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
            'values_changed': {
                'root[0]': {
                    'new_value': 5
                },
                'root[1]': {
                    'new_value': 6
                },
                'root[2]': {
                    'new_value': 7
                },
                'root[3]': {
                    'new_value': 8
                }
            },
            'iterable_item_added': {
                'root[4]': 9,
                'root[5]': 10
            },
            '_numpy_paths': {
                'root': np.where((sys.maxsize > 2**32), 'int64', 'int32')
            }
        },
        'expected_result': 't2'
    },
    'delta_with_null_as_key': {
        't1': { None: [1, 2], 'foo': [1, 2] },
        't2': { None: [1], 'foo': [1] },
        'deepdiff_kwargs': {},
        'to_delta_kwargs': {},
        'expected_delta_dict': {
        },
        'expected_result': 't2'
    },
}


DELTA_NUMPY_TEST_PARAMS = parameterize_cases(
    'test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result', DELTA_NUMPY_TEST_CASES)


class TestNumpyDelta:

    @pytest.mark.parametrize(**DELTA_NUMPY_TEST_PARAMS)
    def test_numpy_delta_cases(self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result):
        diff = DeepDiff(t1, t2, **deepdiff_kwargs)
        delta_dict = diff._to_delta_dict(**to_delta_kwargs)
        if expected_delta_dict:
            assert expected_delta_dict == delta_dict, f"test_numpy_delta_cases {test_name} failed."
        delta = Delta(diff, bidirectional=False, raise_errors=True)
        if expected_result == 't2':
            result = delta + t1
            assert np.array_equal(result, t2), f"test_numpy_delta_cases {test_name} failed."
        elif expected_result == 't2_via_deepdiff':
            result = delta + t1
            diff = DeepDiff(result, t2, ignore_order=True, report_repetition=True)
            assert not diff, f"test_numpy_delta_cases {test_name} failed."
        elif expected_result is DeltaNumpyOperatorOverrideError:
            with pytest.raises(DeltaNumpyOperatorOverrideError):
                t1 + delta
        else:
            result = delta + t1
            assert np.array_equal(result, expected_result), f"test_numpy_delta_cases {test_name} failed."

    def test_invalid_numpy_type(self):

        t1 = np.array([1, 2, 3], np.int8)
        delta_dict = {'iterable_item_added': {'root[2]': 5}, '_numpy_paths': {'root': 'int11'}}

        with pytest.raises(DeltaError) as excinfo:
            Delta(delta_dict, raise_errors=True) + t1

        expected_msg = NOT_VALID_NUMPY_TYPE.format("'int11'")
        assert expected_msg == str(excinfo.value)


class TestDeltaOther:

    def test_list_ignore_order_various_deltas1(self):
        t1 = [5, 1, 3, 1, 4, 4, 6]
        t2 = [7, 4, 4, 1, 3, 4, 8]

        delta_dict1 = {'iterable_items_added_at_indexes': {'root': {0: 7, 6: 8, 1: 4, 2: 4, 5: 4, 3: 1}}, 'iterable_items_removed_at_indexes': {'root': {0: 5, 6: 6}}}
        delta_dict2 = {'iterable_items_added_at_indexes': {'root': {1: 4, 2: 4, 5: 4, 3: 1}}, 'values_changed': {'root[6]': {'new_value': 7}, 'root[0]': {'new_value': 8}}}
        delta1 = Delta(delta_dict1)
        t1_plus_delta1 = t1 + delta1
        assert t1_plus_delta1 == t2
        delta2 = Delta(delta_dict2)
        t1_plus_delta2 = t1 + delta2
        assert t1_plus_delta2 == [8, 4, 4, 1, 3, 4, 7]

    def test_list_ignore_order_various_deltas2(self):
        t1 = (5, 1, 3, 1, 4, 4, 6)
        t2 = (7, 4, 4, 1, 3, 4, 8, 1)

        delta_dict1 = {'iterable_items_added_at_indexes': {'root': {0: 7, 6: 8, 1: 4, 2: 4, 5: 4}}, 'iterable_items_removed_at_indexes': {'root': {6: 6, 0: 5}}}
        delta_dict2 = {'iterable_items_added_at_indexes': {'root': {1: 4, 2: 4, 5: 4}}, 'values_changed': {'root[6]': {'new_value': 7}, 'root[0]': {'new_value': 8}}}
        delta1 = Delta(delta_dict1)
        t1_plus_delta1 = t1 + delta1
        assert t1_plus_delta1 == t2
        delta2 = Delta(delta_dict2)
        t1_plus_delta2 = t1 + delta2
        assert t1_plus_delta2 == (8, 4, 4, 1, 3, 4, 1, 7)

        flat_result1 = delta1.to_flat_rows()
        flat_expected1 = [
            {'path': [0], 'value': 7, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [6], 'value': 8, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [1], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [2], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [5], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [6], 'value': 6, 'action': 'unordered_iterable_item_removed', 'type': int},
            {'path': [0], 'value': 5, 'action': 'unordered_iterable_item_removed', 'type': int},
        ]
        flat_expected1 = [FlatDeltaRow(**i) for i in flat_expected1]
        assert flat_expected1 == flat_result1

        delta1_again = Delta(flat_rows_list=flat_expected1)
        assert t1_plus_delta1 == t1 + delta1_again
        assert delta1.diff == delta1_again.diff

        flat_result2 = delta2.to_flat_rows()
        flat_expected2 = [
            {'path': [1], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [2], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [5], 'value': 4, 'action': 'unordered_iterable_item_added', 'type': int},
            {'path': [6], 'action': 'values_changed', 'value': 7, 'type': int},
            {'path': [0], 'action': 'values_changed', 'value': 8, 'type': int},
        ]
        flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2]
        assert flat_expected2 == flat_result2

        delta2_again = Delta(flat_rows_list=flat_expected2)
        assert delta2.diff == delta2_again.diff

    def test_delta_view_and_to_delta_dict_are_equal_when_parameteres_passed(self):
        """
        This is a test that passes parameters in a dictionary instead of kwargs.
        Note that when parameters are passed as a dictionary, all of them even the ones that
        have default values need to be passed.
        """
        t1 = [4, 2, 2, 1]
        t2 = [4, 1, 1, 1]
        _parameters = {
            'ignore_order': True,
            'ignore_numeric_type_changes': False,
            'ignore_string_type_changes': False,
            'ignore_type_in_groups': [],
            'report_repetition': True,
            'use_enum_value': False,
            'exclude_paths': None,
            'include_paths': None,
            'exclude_regex_paths': None,
            'exclude_types': None,
            'exclude_types_tuple': None,
            'ignore_type_subclasses': False,
            'ignore_string_case': False,
            'include_obj_callback': None,
            'include_obj_callback_strict': None,
            'exclude_obj_callback': None,
            'exclude_obj_callback_strict': None,
            'ignore_private_variables': True,
            'ignore_nan_inequality': False,
            'hasher': None,
            'significant_digits': None,
            'number_format_notation': 'f',
            'verbose_level': 1,
            'view': DELTA_VIEW,
            'max_passes': 10000000,
            'max_diffs': None,
            'number_to_string': number_to_string,
            'cache_tuning_sample_size': 500,
            'cache_size': 500,
            'cutoff_intersection_for_pairs': 0.6,
            'group_by': None,
            'ignore_order_func': lambda *args, **kwargs: True,
            'custom_operators': [],
            'encodings': None,
            'ignore_encoding_errors': False,
            'iterable_compare_func': None,
        }

        expected = {'iterable_items_added_at_indexes': {'root': {1: 1, 2: 1, 3: 1}}, 'iterable_items_removed_at_indexes': {'root': {1: 2, 2: 2}}}
        diff1 = DeepDiff(t1, t2, _parameters=_parameters)
        assert expected == diff1

        _parameters['view'] = TEXT_VIEW
        diff2 = DeepDiff(t1, t2, _parameters=_parameters)
        assert expected == diff2._to_delta_dict()

    def test_verify_symmetry_and_get_elem_and_compare_to_old_value(self):
        """
        Test a specific case where path was a list of elements (in the form of tuples)
        and the item could not be found.
        """
        delta = Delta({}, bidirectional=True, raise_errors=True, log_errors=False)
        with pytest.raises(DeltaError) as excinfo:
            delta._get_elem_and_compare_to_old_value(
                obj={},
                path_for_err_reporting=(('root', GETATTR),),
                expected_old_value='Expected Value',
                action=GET,
                elem='key')
        assert VERIFICATION_MSG.format('root', 'Expected Value', 'not found', "'key'") == str(excinfo.value)

    def test_apply_delta_to_incompatible_object1(self):
        t1 = {1: {2: [4, 5]}}
        t2 = {1: {2: [4]}, 0: 'new'}

        diff = DeepDiff(t1, t2)
        delta = Delta(diff, raise_errors=True)

        t3 = []

        with pytest.raises(DeltaError) as excinfo:
            delta + t3
        assert "Unable to get the item at root[1][2][1]: list index out of range" == str(excinfo.value)
        assert [] == t3

    def test_apply_delta_to_incompatible_object3_errors_can_be_muted(self):
        t1 = {1: {2: [4]}}
        t2 = {1: {2: [4, 6]}}
        t3 = []

        diff = DeepDiff(t1, t2)

        delta2 = Delta(diff, raise_errors=False)
        t4 = delta2 + t3
        assert [] == t4

    def test_apply_delta_to_incompatible_object4_errors_can_be_muted(self):
        t1 = {1: {2: [4, 5]}}
        t2 = {1: {2: [4]}, 0: 'new'}
        t3 = []

        diff = DeepDiff(t1, t2)

        # The original delta was based on a diff between 2 dictionaries.
        # if we turn raise_errors=False, we can try to see what portions of the delta
        delta2 = Delta(diff, raise_errors=False)
        t4 = delta2 + t3
        assert ['new'] == t4

    def test_apply_delta_to_incompatible_object5_no_errors_detected(self):
        t1 = {3: {2: [4]}}
        t2 = {3: {2: [4]}, 0: 'new', 1: 'newer'}
        diff = DeepDiff(t1, t2)

        t3 = []
        # The original delta was based on a diff between 2 dictionaries.
        # if we turn raise_errors=True, and there are no errors, a delta can be applied fully to another object!
        delta2 = Delta(diff, raise_errors=True)
        t4 = delta2 + t3
        assert ['new', 'newer'] == t4

    def test_apply_delta_to_incompatible_object6_value_change(self):
        t1 = {1: {2: [4]}}
        t2 = {1: {2: [5]}}
        t3 = []

        diff = DeepDiff(t1, t2)

        delta2 = Delta(diff, raise_errors=False)
        t4 = delta2 + t3
        assert [] == t4

        flat_result2 = delta2.to_flat_rows()
        flat_expected2 = [{'path': [1, 2, 0], 'action': 'values_changed', 'value': 5, 'type': int}]
        flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2]

        assert flat_expected2 == flat_result2

        delta2_again = Delta(flat_rows_list=flat_expected2)
        assert delta2.diff == delta2_again.diff

        delta3 = Delta(diff, raise_errors=False, bidirectional=True)
        flat_result3 = delta3.to_flat_rows()
        flat_expected3 = [{'path': [1, 2, 0], 'action': 'values_changed', 'value': 5, 'old_value': 4, 'type': int, 'old_type': int}]
        flat_expected3 = [FlatDeltaRow(**i) for i in flat_expected3]

        assert flat_expected3 == flat_result3

        delta3_again = Delta(flat_rows_list=flat_expected3)
        assert delta3.diff == delta3_again.diff

    def test_apply_delta_to_incompatible_object7_type_change(self):
        t1 = ['1']
        t2 = [1]
        t3 = ['a']

        diff = DeepDiff(t1, t2)

        delta2 = Delta(diff, raise_errors=False)
        t4 = delta2 + t3
        assert ['a'] == t4

    @mock.patch('deepdiff.delta.logger.error')
    def test_apply_delta_to_incompatible_object7_verify_symmetry(self, mock_logger):
        t1 = [1]
        t2 = [2]
        t3 = [3]

        diff = DeepDiff(t1, t2)

        delta2 = Delta(diff, raise_errors=False, bidirectional=True)
        t4 = delta2 + t3

        assert [2] == t4
        expected_msg = VERIFICATION_MSG.format('root[0]', 1, 3, VERIFY_BIDIRECTIONAL_MSG)
        mock_logger.assert_called_once_with(expected_msg)

    @mock.patch('deepdiff.delta.logger.error')
    def test_apply_delta_to_incompatible_object8_verify_symmetry_ignore_order(self, mock_logger):
        t1 = [1, 2, 'B', 3]
        t2 = [1, 2, 3, 5]
        t3 = []

        diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)

        delta2 = Delta(diff, raise_errors=False, bidirectional=True)
        t4 = delta2 + t3

        assert [5] == t4
        expected_msg = INDEXES_NOT_FOUND_WHEN_IGNORE_ORDER.format({3: 5})
        mock_logger.assert_called_once_with(expected_msg)

    @mock.patch('deepdiff.delta.logger.error')
    def test_apply_delta_to_incompatible_object9_ignore_order_and_verify_symmetry(self, mock_logger):
        t1 = [1, 2, 'B']
        t2 = [1, 2]
        t3 = [1, 2, 'C']

        diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)

        delta = Delta(diff, raise_errors=False, bidirectional=True)
        t4 = delta + t3

        assert [1, 2, 'C'] == t4
        expected_msg = FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format(2, 'root', 'B', 'C')
        mock_logger.assert_called_once_with(expected_msg)

    @mock.patch('deepdiff.delta.logger.error')
    def test_apply_delta_to_incompatible_object10_ignore_order(self, mock_logger):
        t1 = [1, 2, 'B']
        t2 = [1, 2]
        t3 = [1, 2, 'C']

        diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)

        # when bidirectional=False, we still won't remove the item that is different
        # than what we expect specifically when ignore_order=True when generating the diff.
        # The reason is that when ignore_order=True, we can' rely too much on the index
        # of the item alone to delete it. We need to make sure we are deleting the correct value.
        # The expected behavior is exactly the same as when bidirectional=True
        # specifically for when ignore_order=True AND an item is removed.
        delta = Delta(diff, raise_errors=False, bidirectional=False)
        t4 = delta + t3

        assert [1, 2, 'C'] == t4
        expected_msg = FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format(2, 'root', 'B', 'C')
        mock_logger.assert_called_once_with(expected_msg)

    @mock.patch('deepdiff.delta.logger.error')
    def test_apply_delta_to_incompatible_object11_ignore_order(self, mock_logger):
        t1 = [[1, 2, 'B']]
        t2 = [[1, 2]]
        t3 = {}

        diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)
        delta = Delta(diff, raise_errors=False, bidirectional=False)
        t4 = delta + t3

        assert {} == t4
        expected_msg = UNABLE_TO_GET_PATH_MSG.format('root[0][0]')
        mock_logger.assert_called_once_with(expected_msg)

    def test_delta_to_dict(self):
        t1 = [1, 2, 'B']
        t2 = [1, 2]
        diff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)
        delta = Delta(diff, raise_errors=False, bidirectional=False)

        result = delta.to_dict()
        expected = {'iterable_items_removed_at_indexes': {'root': {2: 'B'}}}
        assert expected == result

        flat_result = delta.to_flat_rows()
        flat_expected = [{'action': 'unordered_iterable_item_removed', 'path': [2], 'value': 'B', 'type': str}]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]

        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected)
        assert delta.diff == delta_again.diff

    def test_class_type_change(self):
        t1 = CustomClass
        t2 = CustomClass2
        diff = DeepDiff(t1, t2, view=DELTA_VIEW)
        expected = {'type_changes': {'root': {'new_type': CustomClass2,
                    'old_type': CustomClass}}}

        assert expected == diff

    def test_numpy_type_invalid(self):
        t1 = np.array([[1, 2, 3], [4, 2, 2]], np.int8)
        diff = {
            'iterable_item_added': {'root[2]': [7, 8, 9]},
            'values_changed': {
                'root[0][2]': {
                    'new_value': 5
                },
                'root[1][1]': {
                    'new_value': 1
                }
            },
            '_numpy_paths': {
                'root': 'int88'
            }
        }

        delta = Delta(diff, raise_errors=True)
        with pytest.raises(DeltaError) as excinfo:
            delta + t1
        assert "'int88' is not a valid numpy type." == str(excinfo.value)

    def test_ignore_order_but_not_report_repetition(self):
        t1 = [1, 2, 'B', 3]
        t2 = [1, 2, 3, 5]

        with pytest.raises(ValueError) as excinfo:
            Delta(DeepDiff(t1, t2, ignore_order=True))

        assert DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT == str(excinfo.value)

    def test_none_in_delta_object(self):
        t1 = {"a": None}
        t2 = {"a": 1}

        dump = Delta(DeepDiff(t1, t2)).dumps()
        delta = Delta(dump)
        assert t2 == delta + t1

        flat_result = delta.to_flat_rows()
        flat_expected = [{'path': ['a'], 'action': 'type_changes', 'value': 1, 'type': int, 'old_type': type(None)}]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]

        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected)
        assert delta.diff == delta_again.diff

        with pytest.raises(ValueError) as exc_info:
            delta.to_flat_rows(report_type_changes=False)
        assert str(exc_info.value).startswith("When converting to flat dictionaries, if report_type_changes=False and there are type")
        delta2 = Delta(dump, always_include_values=True)
        flat_result2 = delta2.to_flat_rows(report_type_changes=False)
        flat_expected2 = [{'path': ['a'], 'action': 'values_changed', 'value': 1}]
        flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2]

        assert flat_expected2 == flat_result2

    def test_delta_set_in_objects(self):
        t1 = [[1, SetOrdered(['A', 'B'])], {1}]
        t2 = [[2, SetOrdered([10, 'C', 'B'])], {1}]
        delta = Delta(DeepDiff(t1, t2))
        flat_result = delta.to_flat_rows()
        flat_expected = [
            {'path': [0, 1], 'value': 10, 'action': 'set_item_added', 'type': int},
            {'path': [0, 0], 'action': 'values_changed', 'value': 2, 'type': int},
            {'path': [0, 1], 'value': 'A', 'action': 'set_item_removed', 'type': str},
            {'path': [0, 1], 'value': 'C', 'action': 'set_item_added', 'type': str},
        ]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]

        # Sorting because otherwise the order is not deterministic for sets,
        # even though we are using SetOrdered here. It still is converted to set at some point and loses its order.
        flat_result.sort(key=lambda x: str(x.value))
        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected)
        assert delta.diff == delta_again.diff

    def test_delta_with_json_serializer(self):
        t1 = {"a": 1}
        t2 = {"a": 2}

        diff = DeepDiff(t1, t2)
        delta = Delta(diff, serializer=json.dumps)
        dump = delta.dumps()
        delta_reloaded = Delta(dump, deserializer=json.loads)
        assert t2 == delta_reloaded + t1

        the_file = io.StringIO()
        delta.dump(the_file)
        the_file.seek(0)

        delta_reloaded_again = Delta(delta_file=the_file, deserializer=json.loads)
        assert t2 == delta_reloaded_again + t1

    def test_brackets_in_keys(self):
        """
        Delta calculation not correct when bracket in Json key
        https://github.com/seperman/deepdiff/issues/265
        """
        t1 = "{ \
            \"test\": \"test1\" \
        }"

        t2 = "{ \
            \"test\": \"test1\", \
            \"test2 [uuu]\": \"test2\" \
        }"

        json1 = json.loads(t1)
        json2 = json.loads(t2)

        ddiff = DeepDiff(json1, json2)
        delta = Delta(ddiff)

        original_json2 = delta + json1
        assert json2 == original_json2


class TestDeltaCompareFunc:

    @staticmethod
    def compare_func(x, y, level):
        if (not isinstance(x, dict) or not isinstance(y, dict)):
            raise CannotCompare
        if(level.path() == "root['path2']"):
            if (x["ID"] == y["ID"]):
                return True
            return False

        if("id" in x and "id" in y):
            if (x["id"] == y["id"]):
                return True
            return False

        raise CannotCompare

    def test_compare_func1(self, compare_func_t1, compare_func_t2, compare_func_result1):

        ddiff = DeepDiff(
            compare_func_t1, compare_func_t2,
            iterable_compare_func=self.compare_func, verbose_level=1)
        assert compare_func_result1 == ddiff
        delta = Delta(ddiff)
        recreated_t2 = compare_func_t1 + delta
        assert compare_func_t2 == recreated_t2

    def test_compare_func_with_duplicates_removed(self):
        t1 = [{'id': 1, 'val': 1}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}, {'id': 3, 'val': 3}]
        t2 = [{'id': 3, 'val': 3}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}]
        ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
        expected = {
            "iterable_item_removed": {
                "root[2]": {
                    "id": 1,
                    "val": 3
                }
            },
            "iterable_item_moved": {
                "root[0]": {
                    "new_path": "root[2]",
                    "value": {
                        "id": 1,
                        "val": 3
                    }
                },
                "root[3]": {
                    "new_path": "root[0]",
                    "value": {
                        "id": 3,
                        "val": 3
                    }
                }
            },
            'values_changed': {
                "root[2]['val']": {
                    'new_value': 3,
                    'old_value': 1,
                    'new_path': "root[0]['val']"
                }
            },
        }
        assert expected == ddiff
        delta = Delta(ddiff)
        recreated_t2 = t1 + delta
        assert t2 == recreated_t2

        flat_result = delta.to_flat_rows()
        flat_expected = [
            {'path': [2, 'val'], 'value': 3, 'action': 'values_changed', 'type': int, 'new_path': [0, 'val']},
            {'path': [2], 'value': {'id': 1, 'val': 3}, 'action': 'iterable_item_removed', 'type': dict},
            {'path': [0], 'value': {'id': 1, 'val': 3}, 'action': 'iterable_item_removed', 'type': dict},
            {'path': [3], 'value': {'id': 3, 'val': 3}, 'action': 'iterable_item_removed', 'type': dict},
            {'path': [0], 'action': 'iterable_item_moved', 'value': {'id': 1, 'val': 3}, 'new_path': [2], 'type': dict},
            {'path': [3], 'action': 'iterable_item_moved', 'value': {'id': 3, 'val': 3}, 'new_path': [0], 'type': dict},
        ]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]

        assert flat_expected == flat_result

        # Delta.DEBUG = True
        delta_again = Delta(flat_rows_list=flat_expected, iterable_compare_func_was_used=True)
        expected_delta_dict = {
            'iterable_item_removed': {
                'root[2]': {
                    'id': 1,
                    'val': 3
                },
                'root[0]': {
                    'id': 1,
                    'val': 3
                },
                'root[3]': {
                    'id': 3,
                    'val': 3
                }
            },
            'iterable_item_moved': {
                'root[0]': {
                    'new_path': 'root[2]',
                    'value': {
                        'id': 1,
                        'val': 3
                    }
                },
                'root[3]': {
                    'new_path': 'root[0]',
                    'value': {
                        'id': 3,
                        'val': 3
                    }
                }
            },
            'values_changed': {
                "root[2]['val']": {
                    'new_value': 3,
                    'new_path': "root[0]['val']"
                }
            }
        }
        assert expected_delta_dict == delta_again.diff
        assert t2 == t1 + delta_again

    def test_compare_func_with_duplicates_added(self):
        t1 = [{'id': 3, 'val': 3}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}]
        t2 = [{'id': 1, 'val': 1}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}, {'id': 3, 'val': 3}]
        ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
        expected = {
            'iterable_item_added': {
                'root[2]': {
                    'id': 1,
                    'val': 3
                }
            },
            'iterable_item_moved': {
                'root[0]': {
                    'new_path': 'root[3]',
                    'value': {
                        'id': 3,
                        'val': 3
                    }
                },
                'root[2]': {
                    'new_path': 'root[0]',
                    'value': {
                        'id': 1,
                        'val': 1
                    }
                }
            },
            'values_changed': {
                "root[0]['val']": {
                    'new_value': 1,
                    'old_value': 3,
                    'new_path': "root[2]['val']"
                }
            },
        }
        assert expected == ddiff
        delta = Delta(ddiff)
        recreated_t2 = t1 + delta
        assert t2 == recreated_t2

    def test_compare_func_swap(self):
        t1 = [{'id': 1, 'val': 1}, {'id': 1, 'val': 3}]
        t2 = [{'id': 1, 'val': 3}, {'id': 1, 'val': 1}]
        ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
        expected = {'values_changed': {"root[0]['val']": {'new_value': 3, 'old_value': 1}, "root[1]['val']": {'new_value': 1, 'old_value': 3}}}
        assert expected == ddiff
        delta = Delta(ddiff)
        recreated_t2 = t1 + delta
        assert t2 == recreated_t2

    def test_compare_func_path_specific(self):
        t1 = {"path1": [{'id': 1, 'val': 1}, {'id': 2, 'val': 3}], "path2": [{'ID': 4, 'val': 3}, {'ID': 3, 'val': 1}, ], "path3": [{'no_id': 5, 'val': 1}, {'no_id': 6, 'val': 3}]}
        t2 = {"path1": [{'id': 1, 'val': 1}, {'id': 2, 'val': 3}], "path2": [{'ID': 3, 'val': 1}, {'ID': 4, 'val': 3}], "path3": [{'no_id': 5, 'val': 1}, {'no_id': 6, 'val': 3}]}
        ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
        expected = {'iterable_item_moved': {"root['path2'][0]": {'new_path': "root['path2'][1]", 'value': {'ID': 4, 'val': 3}},"root['path2'][1]": {'new_path': "root['path2'][0]", 'value': {'ID': 3, 'val': 1}}}}
        assert expected == ddiff
        delta = Delta(ddiff)
        recreated_t2 = t1 + delta
        assert t2 == recreated_t2

    def test_compare_func_nested_changes(self):
        t1 = {
            "TestTable": [
                {
                    "id": "022fb580-800e-11ea-a361-39b3dada34b5",
                    "name": "Max",
                    "NestedTable": [
                        {
                            "id": "022fb580-800e-11ea-a361-39b3dada34a6",
                            "NestedField": "Test Field"
                        }
                    ]
                },
                {
                    "id": "022fb580-800e-11ea-a361-12354656532",
                    "name": "Bob",
                    "NestedTable": [
                        {
                            "id": "022fb580-800e-11ea-a361-39b3dada34c7",
                            "NestedField": "Test Field 2"
                        },
                    ]
                },
            ]
        }
        t2 = {"TestTable": [
            {
                "id": "022fb580-800e-11ea-a361-12354656532",
                "name": "Bob (Changed Name)",
                "NestedTable": [
                    {
                        "id": "022fb580-800e-11ea-a361-39b3dada34c7",
                        "NestedField": "Test Field 2 (Changed Nested Field)"
                    },
                    {
                        "id": "new id",
                        "NestedField": "Test Field 3"
                    },
                    {
                        "id": "newer id",
                        "NestedField": "Test Field 4"
                    },
                ]
            },
            {
                "id": "adding_some_random_id",
                "name": "New Name",
                "NestedTable": [
                    {
                        "id": "random_nested_id_added",
                        "NestedField": "New Nested Field"
                    },
                    {
                        "id": "random_nested_id_added2",
                        "NestedField": "New Nested Field2"
                    },
                    {
                        "id": "random_nested_id_added3",
                        "NestedField": "New Nested Field43"
                    },
                ]
            }
        ]}

        ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
        delta = Delta(ddiff)
        recreated_t2 = t1 + delta
        assert t2 == recreated_t2

    def test_delta_force1(self):
        t1 = {
            'x': {
                'y': [1, 2, 3]
            },
            'q': {
                'r': 'abc',
            }
        }

        t2 = {
            'x': {
                'y': [1, 2, 3, 4]
            },
            'q': {
                'r': 'abc',
                't': 0.5,
            }
        }

        diff = DeepDiff(t1, t2)

        delta = Delta(diff=diff, force=True)
        result = {} + delta
        expected = {'x': {'y': {3: 4}}, 'q': {'t': 0.5}}
        assert expected == result

    def test_flatten_dict_with_one_key_added(self):
        t1 = {"field1": {"joe": "Joe"}}
        t2 = {"field1": {"joe": "Joe Nobody"}, "field2": {"jimmy": "Jimmy"}}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        flat_result = delta.to_flat_rows(report_type_changes=False)
        flat_expected = [
            {'path': ['field2', 'jimmy'], 'value': 'Jimmy', 'action': 'dictionary_item_added'},
            {'path': ['field1', 'joe'], 'action': 'values_changed', 'value': 'Joe Nobody'},
        ]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]
        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected, force=True)  # We need to enable force so it creates the dictionary when added to t1
        expected_data_again_diff = {'dictionary_item_added': {"root['field2']['jimmy']": 'Jimmy'}, 'values_changed': {"root['field1']['joe']": {'new_value': 'Joe Nobody'}}}

        assert delta.diff != delta_again.diff, "Since a dictionary containing a single field was created, the flat dict acted like one key was added."
        assert expected_data_again_diff == delta_again.diff, "Since a dictionary containing a single field was created, the flat dict acted like one key was added."

        assert t2 == t1 + delta_again

    def test_flatten_dict_with_multiple_keys_added(self):
        t1 = {"field1": {"joe": "Joe"}}
        t2 = {"field1": {"joe": "Joe Nobody"}, "field2": {"jimmy": "Jimmy", "sar": "Sarah"}}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        flat_result = delta.to_flat_rows(report_type_changes=False)
        flat_expected = [
            {'path': ['field2'], 'value': {'jimmy': 'Jimmy', 'sar': 'Sarah'}, 'action': 'dictionary_item_added'},
            {'path': ['field1', 'joe'], 'action': 'values_changed', 'value': 'Joe Nobody'},
        ]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]
        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected)
        assert delta.diff == delta_again.diff

    def test_flatten_list_with_one_item_added(self):
        t1 = {"field1": {"joe": "Joe"}}
        t2 = {"field1": {"joe": "Joe"}, "field2": ["James"]}
        t3 = {"field1": {"joe": "Joe"}, "field2": ["James", "Jack"]}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        flat_result = delta.to_flat_rows(report_type_changes=False)
        flat_expected = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]
        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected, force=True)
        assert {'iterable_item_added': {"root['field2'][0]": 'James'}} == delta_again.diff
        # delta_again.DEBUG = True
        assert t2 == t1 + delta_again

        diff2 = DeepDiff(t2, t3)
        delta2 = Delta(diff=diff2, always_include_values=True)
        flat_result2 = delta2.to_flat_rows(report_type_changes=False)
        flat_expected2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}]
        flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2]

        assert flat_expected2 == flat_result2

        delta_again2 = Delta(flat_rows_list=flat_expected2, force=True)

        assert {'iterable_item_added': {"root['field2'][1]": 'Jack'}} == delta_again2.diff
        assert t3 == t2 + delta_again2

    def test_flatten_set_with_one_item_added(self):
        t1 = {"field1": {"joe": "Joe"}}
        t2 = {"field1": {"joe": "Joe"}, "field2": {"James"}}
        t3 = {"field1": {"joe": "Joe"}, "field2": {"James", "Jack"}}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        assert t2 == t1 + delta
        flat_result = delta.to_flat_rows(report_type_changes=False)
        flat_expected = [{'path': ['field2'], 'value': 'James', 'action': 'set_item_added'}]
        flat_expected = [FlatDeltaRow(**i) for i in flat_expected]
        assert flat_expected == flat_result

        delta_again = Delta(flat_rows_list=flat_expected, force=True)
        assert {'set_item_added': {"root['field2']": {'James'}}} == delta_again.diff
        assert t2 == t1 + delta_again

        diff = DeepDiff(t2, t3)
        delta2 = Delta(diff=diff, always_include_values=True)
        flat_result2 = delta2.to_flat_rows(report_type_changes=False)
        flat_expected2 = [{'path': ['field2'], 'value': 'Jack', 'action': 'set_item_added'}]
        flat_expected2 = [FlatDeltaRow(**i) for i in flat_expected2]

        assert flat_expected2 == flat_result2

        delta_again2 = Delta(flat_rows_list=flat_expected2, force=True)
        assert {'set_item_added': {"root['field2']": {'Jack'}}} == delta_again2.diff
        assert t3 == t2 + delta_again2

    def test_flatten_tuple_with_one_item_added(self):
        t1 = {"field1": {"joe": "Joe"}}
        t2 = {"field1": {"joe": "Joe"}, "field2": ("James", )}
        t3 = {"field1": {"joe": "Joe"}, "field2": ("James", "Jack")}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        assert t2 == t1 + delta
        flat_expected = delta.to_flat_rows(report_type_changes=False)
        expected_result = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}]
        expected_result = [FlatDeltaRow(**i) for i in expected_result]

        assert expected_result == flat_expected

        delta_again = Delta(flat_rows_list=flat_expected, force=True)
        assert {'iterable_item_added': {"root['field2'][0]": 'James'}} == delta_again.diff
        assert {'field1': {'joe': 'Joe'}, 'field2': ['James']} == t1 + delta_again, "We lost the information about tuple when we convert to flat dict."

        diff = DeepDiff(t2, t3)
        delta2 = Delta(diff=diff, always_include_values=True, force=True)
        flat_result2 = delta2.to_flat_rows(report_type_changes=False)
        expected_result2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}]
        expected_result2 = [FlatDeltaRow(**i) for i in expected_result2]

        assert expected_result2 == flat_result2
        assert t3 == t2 + delta2

        delta_again2 = Delta(flat_rows_list=flat_result2)
        assert {'iterable_item_added': {"root['field2'][1]": 'Jack'}} == delta_again2.diff
        assert t3 == t2 + delta_again2

    def test_flatten_list_with_multiple_item_added(self):
        t1 = {"field1": {"joe": "Joe"}}
        t2 = {"field1": {"joe": "Joe"}, "field2": ["James", "Jack"]}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        flat_result = delta.to_flat_rows(report_type_changes=False)
        expected_result = [{'path': ['field2'], 'value': ['James', 'Jack'], 'action': 'dictionary_item_added'}]
        expected_result = [FlatDeltaRow(**i) for i in expected_result]

        assert expected_result == flat_result

        delta2 = Delta(diff=diff, bidirectional=True, always_include_values=True)
        flat_result2 = delta2.to_flat_rows(report_type_changes=False)
        assert expected_result == flat_result2

        delta_again = Delta(flat_rows_list=flat_result)
        assert delta.diff == delta_again.diff

    def test_flatten_attribute_added(self):
        t1 = picklalbe_obj_without_item
        t2 = PicklableClass(10)
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, always_include_values=True)
        flat_result = delta.to_flat_rows(report_type_changes=False)
        expected_result = [{'path': ['item'], 'value': 10, 'action': 'attribute_added'}]
        expected_result = [FlatDeltaRow(**i) for i in expected_result]

        assert expected_result == flat_result

        delta_again = Delta(flat_rows_list=flat_result)
        assert delta.diff == delta_again.diff

    def test_flatten_when_simple_type_change(self):
        t1 = [1, 2, '3']
        t2 = [1, 2, 3]

        diff = DeepDiff(t1, t2)
        expected_diff = {
            'type_changes': {'root[2]': {'old_type': str, 'new_type': int, 'old_value': '3', 'new_value': 3}}
        }

        assert expected_diff == diff
        delta = Delta(diff=diff)
        with pytest.raises(ValueError) as exc_info:
            delta.to_flat_rows(report_type_changes=False)
        assert str(exc_info.value).startswith("When converting to flat dictionaries")

        delta2 = Delta(diff=diff, always_include_values=True)
        flat_result2 = delta2.to_flat_rows(report_type_changes=False)
        expected_result2 = [{'path': [2], 'action': 'values_changed', 'value': 3}]
        expected_result2 = [FlatDeltaRow(**i) for i in expected_result2]

        assert expected_result2 == flat_result2

        delta3 = Delta(diff=diff, always_include_values=True, bidirectional=True)
        flat_result3 = delta3.to_flat_rows(report_type_changes=False)

        expected_result3 = [{'path': [2], 'action': 'values_changed', 'value': 3, 'old_value': '3'}]
        expected_result3 = [FlatDeltaRow(**i) for i in expected_result3]
        assert expected_result3 == flat_result3

        delta_again = Delta(flat_rows_list=flat_result3)
        assert {'values_changed': {'root[2]': {'new_value': 3, 'old_value': '3'}}} == delta_again.diff

    def test_subtract_delta1(self):
        t1 = {'field_name1': ['yyy']}
        t2 = {'field_name1': ['xxx', 'yyy']}
        delta_diff = {'iterable_items_removed_at_indexes': {"root['field_name1']": {(0, 'GET'): 'xxx'}}}
        expected_reverse_diff = {'iterable_items_added_at_indexes': {"root['field_name1']": {(0, 'GET'): 'xxx'}}}

        delta = Delta(delta_diff=delta_diff, bidirectional=True)
        reversed_diff = delta._get_reverse_diff()
        assert expected_reverse_diff == reversed_diff
        assert t2 != {'field_name1': ['yyy', 'xxx']} == t1 - delta, "Since iterable_items_added_at_indexes is used when ignore_order=True, the order is not necessarily the original order."

    def test_subtract_delta_made_from_flat_dicts1(self):
        t1 = {'field_name1': ['xxx', 'yyy']}
        t2 = {'field_name1': []}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, bidirectional=True)
        flat_rows_list = delta.to_flat_rows(include_action_in_path=False, report_type_changes=True)
        expected_flat_dicts = [{
            'path': ['field_name1', 0],
            'value': 'xxx',
            'action': 'iterable_item_removed',
            'type': str,
        }, {
            'path': ['field_name1', 1],
            'value': 'yyy',
            'action': 'iterable_item_removed',
            'type': str,
        }]
        expected_flat_dicts = [FlatDeltaRow(**i) for i in expected_flat_dicts]

        assert expected_flat_dicts == flat_rows_list

        delta1 = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True)
        assert t1 == t2 - delta1

        delta2 = Delta(flat_rows_list=[flat_rows_list[0]], bidirectional=True, force=True)
        middle_t = t2 - delta2
        assert {'field_name1': ['xxx']} == middle_t

        delta3 = Delta(flat_rows_list=[flat_rows_list[1]], bidirectional=True, force=True)
        assert t1 == middle_t - delta3

    def test_subtract_delta_made_from_flat_dicts2(self):
        t1 = {'field_name1': []}
        t2 = {'field_name1': ['xxx', 'yyy']}
        diff = DeepDiff(t1, t2)
        delta = Delta(diff=diff, bidirectional=True)
        flat_rows_list = delta.to_flat_rows(include_action_in_path=False, report_type_changes=True)
        expected_flat_dicts = [{
            'path': ['field_name1', 0],
            'value': 'xxx',
            'action': 'iterable_item_added',
            'type': str,
        }, {
            'path': ['field_name1', 1],
            'value': 'yyy',
            'action': 'iterable_item_added',
            'type': str,
        }]
        expected_flat_dicts = [FlatDeltaRow(**i) for i in expected_flat_dicts]

        assert expected_flat_dicts == flat_rows_list

        delta1 = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True)
        assert t1 == t2 - delta1

        # We need to subtract the changes in the reverse order if we want to feed the flat dict rows individually to Delta
        delta2 = Delta(flat_rows_list=[flat_rows_list[0]], bidirectional=True, force=True)
        middle_t = t2 - delta2
        assert {'field_name1': ['yyy']} == middle_t

        delta3 = Delta(flat_rows_list=[flat_rows_list[1]], bidirectional=True, force=True)
        delta3.DEBUG = True
        assert t1 == middle_t - delta3

    def test_list_of_alphabet_and_its_delta(self):
        l1 = "A B C D E F G D H".split()
        l2 = "B C X D H Y Z".split()
        diff = DeepDiff(l1, l2)

        # Problem: The index of values_changed should be either all for AFTER removals or BEFORE removals.
        # What we have here is that F & G transformation to Y and Z is not compatible with A and E removal
        # it is really meant for the removals to happen first, and then have indexes in L2 for values changing
        # rather than indexes in L1. Here what we need to have is:
        # A B C D E F G D H
        # A B C-X-E 
        # B C D F G D H  # removal

        # What we really need is to report is as it is in difflib for delta specifically:
        # A B C D E F G D H
        # B C D E F G D H     delete    t1[0:1] --> t2[0:0]    ['A'] --> []
        # B C D E F G D H     equal     t1[1:3] --> t2[0:2] ['B', 'C'] --> ['B', 'C']
        # B C X D H           replace   t1[3:7] --> t2[2:3] ['D', 'E', 'F', 'G'] --> ['X']
        # B C X D H           equal     t1[7:9] --> t2[3:5] ['D', 'H'] --> ['D', 'H']
        # B C X D H Y Z       insert    t1[9:9] --> t2[5:7]       [] --> ['Y', 'Z']

        # So in this case, it needs to also include information about what stays equal in the delta
        # NOTE: the problem is that these operations need to be performed in a specific order.
        # DeepDiff removes that order and just buckets all insertions vs. replace vs. delete in their own buckets.
        # For times that we use Difflib, we may want to keep the information for the array_change key
        # just for the sake of delta, but not for reporting in deepdiff itself.
        # that way we can re-apply the changes as they were reported in delta.

        delta = Delta(diff)
        assert l2 == l1 + delta
        with pytest.raises(ValueError) as exc_info:
            l1 == l2 - delta
        assert "Please recreate the delta with bidirectional=True" == str(exc_info.value)

        delta2 = Delta(diff, bidirectional=True)
        assert l2 == l1 + delta2
        assert l1 == l2 - delta2

        dump = Delta(diff, bidirectional=True).dumps()
        delta3 = Delta(dump, bidirectional=True)

        assert l2 == l1 + delta3
        assert l1 == l2 - delta3

        dump4 = Delta(diff, bidirectional=True, serializer=json_dumps).dumps()
        delta4 = Delta(dump4, bidirectional=True, deserializer=json_loads)

        assert l2 == l1 + delta4
        assert l1 == l2 - delta4

        flat_rows = delta2.to_flat_rows()

        expected_flat_rows = [
            FlatDeltaRow(path=[3], action='values_changed', value='X', old_value='D', type=str, old_type=str, new_path=[2]),
            FlatDeltaRow(path=[6], action='values_changed', value='Z', old_value='G', type=str, old_type=str),
            FlatDeltaRow(path=[5], action='values_changed', value='Y', old_value='F', type=str, old_type=str),
            FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_deleted, value=[], old_value=['A'], type=list, old_type=list, t1_from_index=0, t1_to_index=1, t2_from_index=0, t2_to_index=0),
            FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_equal, value=None, old_value=None, type=type(None), old_type=type(None), t1_from_index=1, t1_to_index=3, t2_from_index=0, t2_to_index=2),
            FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_replaced, value=['X'], old_value=['D', 'E', 'F', 'G'], type=list, old_type=list, t1_from_index=3, t1_to_index=7, t2_from_index=2, t2_to_index=3),
            FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_equal, value=None, old_value=None, type=type(None), old_type=type(None), t1_from_index=7, t1_to_index=9, t2_from_index=3, t2_to_index=5),
            FlatDeltaRow(path=[], action=FlatDataAction.iterable_items_inserted, value=['Y', 'Z'], old_value=[], type=list, old_type=list, t1_from_index=9, t1_to_index=9, t2_from_index=5, t2_to_index=7)
        ]

        # The order of the first 3 items is not deterministic
        assert not DeepDiff(expected_flat_rows[:3], flat_rows[:3], ignore_order=True)
        assert expected_flat_rows[3:] == flat_rows[3:]

        delta5 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True)


        assert l2 == l1 + delta5
        assert l1 == l2 - delta5

    def test_delta_flat_rows(self):
        t1 = {"key1": "value1"}
        t2 = {"field2": {"key2": "value2"}}
        diff = DeepDiff(t1, t2, verbose_level=2)
        delta = Delta(diff, bidirectional=True)
        assert t1 + delta == t2
        flat_rows = delta.to_flat_rows()
        # we need to set force=True because when we create flat rows, if a nested
        # dictionary with a single key is created, the path in the flat row will be
        # the path to the leaf node.
        delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True)
        assert t1 + delta2 == t2

    def test_delta_bool(self):
        flat_rows_list = [FlatDeltaRow(path=['dollar_to_cent'], action='values_changed', value=False, old_value=True, type=bool, old_type=bool)]
        value = {'dollar_to_cent': False}
        delta = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True)
        assert {'dollar_to_cent': True} == value - delta

    def test_detla_add_to_empty_iterable_and_flatten(self):
        t1 = {'models': [], 'version_id': 0}
        t2 = {'models': ['Super Duty F-250'], 'version_id': 1}
        t3 = {'models': ['Super Duty F-250', 'Focus'], 'version_id': 1}
        diff = DeepDiff(t1, t2, verbose_level=2)
        delta = Delta(diff, bidirectional=True)
        assert t1 + delta == t2
        flat_rows = delta.to_flat_rows()
        delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True)  # , force=True
        assert t1 + delta2 == t2
        assert t2 - delta2 == t1

        diff3 = DeepDiff(t2, t3)
        delta3 = Delta(diff3, bidirectional=True)
        flat_dicts3 = delta3.to_flat_dicts()

        delta3_again = Delta(flat_dict_list=flat_dicts3, bidirectional=True)
        assert t2 + delta3_again == t3
        assert t3 - delta3_again == t2


    def test_flat_dict_and_deeply_nested_dict(self):
        beforeImage = [
            {
                "usage": "Mailing",
                "standardization": "YES",
                "primaryIndicator": True,
                "addressIdentifier": "Z8PDWBG42YC",
                "addressLines": ["871 PHILLIPS FERRY RD"],
            },
            {
                "usage": "Residence",
                "standardization": "YES",
                "primaryIndicator": False,
                "addressIdentifier": "Z8PDWBG42YC",
                "addressLines": ["871 PHILLIPS FERRY RD"],
            },
            {
                "usage": "Mailing",
                "standardization": None,
                "primaryIndicator": False,
                "addressIdentifier": "MHPP3BY0BYC",
                "addressLines": ["871 PHILLIPS FERRY RD", "APT RV92"],
            },
        ]
        allAfterImage = [
            {
                "usage": "Residence",
                "standardization": "NO",
                "primaryIndicator": False,
                "addressIdentifier": "Z8PDWBG42YC",
                "addressLines": ["871 PHILLIPS FERRY RD"],
            },
            {
                "usage": "Mailing",
                "standardization": None,
                "primaryIndicator": False,
                "addressIdentifier": "MHPP3BY0BYC",
                "addressLines": ["871 PHILLIPS FERRY RD", "APT RV92"],
            },
            {
                "usage": "Mailing",
                "standardization": "NO",
                "primaryIndicator": True,
                "addressIdentifier": "Z8PDWBG42YC",
                "addressLines": ["871 PHILLIPS FERRY RD"],
            },
        ]

        diff = DeepDiff(
            beforeImage,
            allAfterImage,
            ignore_order=True,
            report_repetition=True,
        )
        # reverse_diff = DeepDiff(
        #     allAfterImage,
        #     beforeImage,
        #     ignore_order=True,
        #     report_repetition=True,
        # )
        delta = Delta(
            diff, always_include_values=True, bidirectional=True
        )
        # reverse_delta = Delta(
        #     reverse_diff, always_include_values=True, bidirectional=True
        # )
        allAfterImageAgain = beforeImage + delta
        diff2 = DeepDiff(allAfterImage, allAfterImageAgain, ignore_order=True)
        assert not diff2

        # print("\ndelta.diff")
        # pprint(delta.diff)
        # print("\ndelta._get_reverse_diff()")
        # pprint(delta._get_reverse_diff())
        # print("\nreverse_delta.diff")
        # pprint(reverse_delta.diff)
        beforeImageAgain = allAfterImage - delta
        diff3 = DeepDiff(beforeImage, beforeImageAgain, ignore_order=True)
        assert not diff3

        # ------ now let's recreate the delta from flat dicts -------

        flat_dict_list = delta.to_flat_dicts()

        delta2 = Delta(
            flat_dict_list=flat_dict_list,
            always_include_values=True,
            bidirectional=True,
            raise_errors=False,
            force=True,
        )
        # print("\ndelta from flat dicts")
        # pprint(delta2.diff)
        allAfterImageAgain2 = beforeImage + delta2
        diff4 = DeepDiff(allAfterImage, allAfterImageAgain2, ignore_order=True)
        assert not diff4

        beforeImageAgain2 = allAfterImage - delta2
        diff4 = DeepDiff(beforeImage, beforeImageAgain2, ignore_order=True)
        assert not diff4
