import re
import difflib
# noinspection PyPackageRequirements
from datadiff.tools import assert_equals
import json
# noinspection PyPackageRequirements
import pytest
from collections import OrderedDict
from warnings import warn
from tlib.base.TestHelper import sort_list, sort_dict
import jsonpath_rw
from tlib.base.ExceptionHelper import BadJsonFormatError

# noinspection PyUnresolvedReferences
class JsonHelper(object):
    """
    Functions for modifying and validating JSON data
    """

    @staticmethod
    def assert_json_equals(json1, json2, ignore_order=True, exclude_keys=list()):
        """
        Compares two JSON values\n
        If parameter validate_order is True, position of keys is taken into account during validation\n
        If json1 or json2 are strings, it must be a valid JSON string

        @param json1: String or dictionary to compare
        @type json1: str, dict
        @param json2: String or dictionary to compare
        @type json2: str, dict
        @param ignore_order: If true, order of keys is not validated
        @type ignore_order: bool
        @param exclude_keys: Keys to exclude from comparison, in the format 'key2.key3[2].key1'
        @type exclude_keys: list
        @raise AssertionError: JSON values doesn't match
        """
        if ignore_order:
            JsonHelper._validate_json_ignoring_order(json1, json2, exclude_keys)
        else:
            JsonHelper._validate_json_with_order(json1, json2, exclude_keys)

    @staticmethod
    def _validate_json_ignoring_order(json1, json2, exclude_keys):
        """
        Compares two JSON values, ignoring position of keys\n
        If json1 or json2 are strings, it must be a valid JSON string
        @param json1: String or dictionary to compare
        @type json1: str, dict
        @param json2: String or dictionary to compare
        @type json2: str, dict
        @raise AssertionError: JSON values doesn't match
        """
        items = [json1, json2]
        for i, item in enumerate(items):
            #If it's a string, convert to dictionary
            if isinstance(item, basestring):
                try:
                    item = json.loads(item)
                except ValueError:
                    pytest.fail("Value doesn't represent a valid JSON string\n%s" % item)

            #make sure any lists inside the JSON value are sorted, so diff will not fail
            if type(item) is dict:
                #Sort items inside lists
                item = sort_dict(item)
            elif type(item) is list:
                #Sort items inside lists
                item = sort_list(item)
            else:
                pytest.fail("Parameter doesn't represent a valid JSON object: %s" % item)

            #Delete unwanted keys
            JsonHelper.remove_keys(item, exclude_keys)

            items[i] = item

        assert_equals(items[0], items[1])

    @staticmethod
    def _validate_json_with_order(json1, json2, exclude_keys=list()):
        """
        Compares two JSON values, taking into account key order
        If json1 or json2 are strings, it must be a valid JSON string
        @param json1: String to compare
        @type json1: str
        @param json2: String to compare
        @type json2: str
        @param exclude_keys: Keys to exclude from comparison, in the format 'key2.key3[2].key1'
        @type exclude_keys: list
        @raise AssertionError: JSON values doesn't match
        """

        def object_pairs_hook(values):
            """
            Method that stores objects in an OrderedDict so comparison takes into account key order
            """
            return OrderedDict(values)

        items = [json1, json2]
        for i, item in enumerate(items):
            if not isinstance(item, basestring):
                pytest.fail("Only strings are allowed when validating key order")

            # By default json.loads doesn't care about key order. Let's provide
            # an object_pairs_hook function to ensure key order is kept
            try:
                item = json.loads(item, object_pairs_hook=object_pairs_hook)
            except ValueError:
                pytest.fail("Value doesn't represent a valid JSON string\n%s" % item)

            #Delete unwanted keys
            JsonHelper.remove_keys(item, exclude_keys)

            items[i] = item

        #Do validation
        if items[0] != items[1]:
            json1 = json.dumps(items[0], indent=3).splitlines(True)
            json2 = json.dumps(items[1], indent=3).splitlines(True)
            diff = difflib.unified_diff(json1, json2)
            diff_txt = "".join(diff)

            raise AssertionError(r"JSON values didn't match\nValue1:\n%s\n\nValue2:\n%s\n\nDiff:\n%s""" %
                                 ("".join(json1), "".join(json2), diff_txt))

    @staticmethod
    def remove_keys(data, keys, top_level=True):
        """
        Takes a dictionary or a list and removes keys specified by 'keys' parameter
        @param data: dictionary or OrderedDict that will be modified.
        Object is replaced in place
        @type data: dictionary, OrderedDict
        @param keys: array indicating the path to remove.
        e.g. ['businessHours.headings[2]', 'businessHours.values.name']
        @type keys: list
        """
        for key in keys:
            JsonHelper._remove_key(data, key.split('.'), top_level)

    @staticmethod
    def _remove_key(data, key, top_level=True):
        """
        Takes a dictionary or a list and removes keys specified by 'keys' parameter
        @param data: dictionary or OrderedDict that will be modified.
        Object is replaced in place
        @type data: dictionary, OrderedDict
        @param key: array indicating the path to remove. e.g. ['businessHours', 'headings[2]']
        @type key: list
        """
        orig_key = None

        #Parameter validation
        if type(key) is not list:
            pytest.fail("Invalid argument key. Was expecting a list")

        if not (type(data) in (dict, list) or isinstance(data, OrderedDict)):
            pytest.fail("Invalid argument data. Was expecting a dict or OrderedDict")

        if top_level:
            #Create a copy of the key object so we don't modify the original
            orig_key = list(key)

            #Check if first element in the keys has syntax '*' and change it to be able to match the right values
            match = re.search("^\*\[(.*)\]$", key[0])
            if match:
                try:
                    key[0] = int(match.group(1))
                except ValueError:
                    pytest.fail("Index '%s' is not a valid integer" % match.group(2))

            # split indexed items in two, only first time function is called
            # eg. ["node1", "node[2]"] => ["node1", "node", 2]
            new_key = list()

            for i, value in enumerate(key):
                match = re.search("(^.+)\[(.+)\]$", str(value))
                if match:
                    try:
                        new_key.append(match.group(1))
                        new_key.append(int(match.group(2)))
                    except ValueError:
                        pytest.fail("Index '%s' is not a valid integer" % match.group(2))

                else:
                    new_key.append(value)

            key = list(new_key)

        if type(data) is list:
            #check if next key is an index, otherwise fail
            if type(key[0]) is int:
                index = key.pop(0)

                if len(key) == 0:
                    #Found the key
                    data.pop(index)
                else:
                    #Still need to find children nodes
                    JsonHelper._remove_key(data[index], key, top_level=False)

            else:
                pytest.fail("Key '%s' is not valid for the given JSON object" % key)
        elif type(data) is dict or isinstance(data, OrderedDict):
            #Validate
            for k in data:
                if k == key[0]:
                    key.pop(0)

                    if len(key) == 0:
                        #Found the key
                        del(data[k])
                    else:
                        #Still need to find children nodes
                        JsonHelper._remove_key(data[k], key, top_level=False)

                    # Don't need to continue iterating. Node was already found
                    break
        else:
            pytest.fail("Element %s is not allowed in a JSON response" % type(data))

        if len(key) > 0:
            # Not all keys were found. Can't fail here because it would make impossible testing the scenario where
            # a JSON payload has a key and other doesn't
            warn("Key '%s' does not represent a valid element" % '.'.join(orig_key))

        if top_level:
            #Restore key variable
            # noinspection PyUnusedLocal
            key = list(orig_key)

    @staticmethod
    def get_elements(data, json_path):
        """"
        Applies a JSON path to a JSON string and returns the resulting node
        @param data:
        @type data: str, dict
        """
        #If it's a string, convert to dictionary
        if isinstance(data, basestring):
            try:
                json_data = json.loads(data)
            except ValueError:
                pytest.fail("Value doesn't represent a valid JSON string\n%s" % item)
        elif type(data) is dict:
                #Sort items inside lists
                json_data = data
        else:
            raise BadJsonFormatError("Parameter doesn't represent a valid JSON object: %s" % item)

        jsonpath_expr = jsonpath_rw.parse(json_path)
        result = []
        for i in jsonpath_expr.find(json_data):
            result.append(i.value)

        return result
