# Copyright 2018 www.privaz.io Valletech AB
# Copyright 2002-2023, OpenNebula Project, OpenNebula Systems
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from hashlib import md5
from json import load, dumps as json_dumps
from base64 import b64decode, b64encode
from pickle import dumps, loads
from os import path
from . import OneServer
from tblib import pickling_support
from sys import exc_info
from six import reraise
from collections import OrderedDict
from gzip import open
from os import environ

pickling_support.install()


def read_fixture_file(fixture_file):
    f = open(fixture_file, "rt")
    ret = load(f)
    f.close()
    return ret


def write_fixture_file(fixture_file, obj):
    f = open(fixture_file, "wb")
    f.write(json_dumps(obj).encode())
    f.close()

class OneServerTester(OneServer):
    '''
    This class extends the OneServer to facilitate unit testing
    The idea is to be able to "record" fixtures while testing with a live OpenNebula platform.
    Those recordings can later be use in "replay" mode to simulate an OpenNebula platform.
    XMLAPI method calls are recorded as test_base/unit_test/method_signature_instance
    The signature is generated as the md5 of the parameters
    if several calls with the same signature are doing during the same unit test, instance is incremented.
    The order in which calls happen within the same unit_test must be deterministic.
    '''
    def __init__(self, uri, session, fixture_file=None, fixture_unit=None, timeout=None, fixture_replay=None, **options):

        # Environment driven initialization required for capturing fixtures during ansible integration tests

        if fixture_file is None:
            fixture_file = environ.get("PYONE_TEST_FIXTURE_FILE", None)

        if fixture_replay is None:
            fixture_replay = (environ.get("PYONE_TEST_FIXTURE_REPLAY", "True").lower() in ["1", "yes", "true"])

        if fixture_unit is None:
            fixture_unit = environ.get("PYONE_TEST_FIXTURE_UNIT", "init")

        if path.isfile(fixture_file):
            self._fixtures = read_fixture_file(fixture_file)
        else:
            self._fixtures = dict()

        # all members involved in the fixtures must be predefined or
        # the magic getter method will trigger resulting in stack overflows
        self._fixture_replay = fixture_replay
        self._fixture_file = fixture_file
        self.set_fixture_unit_test(fixture_unit)

        OneServer.__init__(self, uri, session, timeout, **options)

    def set_fixture_unit_test(self,name):
        """
        Set the name of the unit test. creates an entry in the fixture tree if it does not exist.
        :param name:
        :return:
        """

        if not name in self._fixtures:
            self._fixtures[name] = dict()

        self._fixture_unit_test = self._fixtures[name]

    def _fixture_signature(self, methodname, params):
        signature_md5 = md5()
        sparms=json_dumps(params,sort_keys=True).encode()
        signature_md5.update(sparms)
        return signature_md5.hexdigest()

    def _get_fixture(self, methodname, params):
        '''
        returns the next fixture for a given call.
        :param methodname: XMlRPC method
        :param params: the paramters passed
        :return: file name were to store to or read from the fixture data
        '''

        ret = self._fixture_unit_test[methodname][self._fixture_signature(methodname,params)].pop(0)

        if not ret:
            raise Exception("Could not read fixture, if the tests changed you must re-record fixtures")

        return ret

    def _set_fixture(self,methodname,params,object):
        '''
        Will create the fixture for a given call
        :param methodname:
        :param params:
        :return:
        '''

        signature = self._fixture_signature(methodname,params)

        if not methodname in self._fixture_unit_test:
            self._fixture_unit_test[methodname]=dict()

        if not signature in self._fixture_unit_test[methodname]:
            self._fixture_unit_test[methodname][signature]=[]

        self._fixture_unit_test[methodname][signature].append(object)

    def _do_request(self,method,params):
        '''
        Intercepts requests.
        In record mode they are recorded before being returned.
        In replay mode they are read from fixtures instead
        Exceptions are also captured and reraised
        '''

        ret = None

        if self._fixture_replay:
            ret = self._get_fixture(method,params)
            if 'exception' in ret:
                reraise(*loads(b64decode(ret['exception'])))
        else:
            try:
                ret = OneServer._do_request(self, method, params)
            except Exception as exception:
                ret = {
                    "exception": b64encode(dumps(exc_info(), 2)).decode(),
                }
                raise exception
            finally:
                self._set_fixture(method,params,ret)
        return ret

    def _cast_parms(self,params):
        """
        Parameters will be used to generate the signature of the method, an md5.
        So we need signatures to be deterministic. There are two sources of randomness
        - Python version, in particular differences in dealing with encodings
        - Unsorted sets.
        This method will add casting transformations to fix those, only required for testing.

        :param params:
        :return: a list of parameters that should generate a deterministic md5 signature.
        """
        lparams = list(params)

        for i, param in enumerate(lparams):
            if isinstance(param, dict):
                lparams[i] = self._to_ordered_dict(param)

        return  OneServer._cast_parms(self, lparams)

    def _to_ordered_dict(self, param):
        """
        deep orders a dictionary
        :param param:
        :return:
        """

        if isinstance(param,dict):
            # ensure the dictionary is ordered
            param = OrderedDict(sorted(param.items()))
            # recurse
            for key, value in param.items():
                if isinstance(value, dict):
                    param[key] = self._to_ordered_dict(value)
        return param

    def server_retry_interval(self):
        return 0.01

    def server_close(self):
        """
        Unpdates the fixture data if we are recording.
        :return:
        """
        if not self._fixture_replay:
            write_fixture_file(self._fixture_file, self._fixtures)
