# Copyright (C) 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
#
# This file is part of Kitty.
#
# Kitty is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Kitty is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Kitty.  If not, see <http://www.gnu.org/licenses/>.
'''
This module defines the :class:`~kitty.data.report.Report` class
'''
from base64 import b64decode
from binascii import unhexlify
import codecs
import six
from kitty.model.low_level.encoder import StrEncodeEncoder


class Report(object):
    '''
    This class represent a report for a single test.
    This report may contain subreports from nested entities.

    :example:

        In this example, the report, generated by the controller, indicates
        failure.

        ::

            report = Report('Controller')
            report.add('generation time', 0)
            report.failed('target does not respond')
    '''

    PASSED = 'passed'
    FAILED = 'failed'
    ERROR = 'error'

    allowed_statuses = [PASSED, FAILED, ERROR]

    reserved_keys = {
        'status': 'set_status(status), success(), failed(reason) or error(reason)',
        'failed': 'failed(reason)'
    }

    def __init__(self, name, default_failed=False):
        '''
        :param name: name of the report (or the issuer)
        :param default_failed: is the default status of the report failed (default: False)
        '''
        self._data_fields = {}
        self._sub_reports = {}
        self._name = name
        self._default_failed = default_failed
        self.clear()

    def clear(self):
        '''
        Set the report to its defaults.
        This will clear the report, keeping only the name and setting the
        failure status to the default.
        '''
        self._data_fields = {}
        self._sub_reports = {}
        self.set_status(Report.FAILED if self._default_failed else Report.PASSED)
        self.add('name', self._name)
        self.add('sub_reports', [])

    def get_name(self):
        '''
        :return: the name of the report
        '''
        return self.get('name')

    def passed(self):
        '''
        Set the report status to PASSED
        '''
        self.set_status(Report.PASSED)
        if 'reason' in self._data_fields:
            del self._data_fields['reason']

    def success(self):
        '''
        Set the report status to PASSED.

        .. deprecated:: 0.6.7
            use :func:`~kitty.data.report.Report.passed`
        '''
        self.passed()

    def failed(self, reason=None):
        '''
        Set the test status to Report.FAILED, and set the failure reason

        :param reason: failure reason (default: None)
        '''
        self.set_status(Report.FAILED)
        if reason:
            self.add('reason', reason)

    def error(self, reason=None):
        '''
        Set the test status to Report.ERROR, and set the error reason

        :param reason: error reason (default: None)
        '''
        self.set_status(Report.ERROR)
        if reason:
            self.add('reason', reason)

    def set_status(self, new_status):
        '''
        Set the status of the report.

        :param new_status: the new status of the report (either PASSED, FAILED or ERROR)
        '''
        if new_status not in Report.allowed_statuses:
            raise Exception('status must be one of: %s' % (', '.join(Report.allowed_statuses)))
        self._data_fields['status'] = new_status.lower()

    def add(self, key, value):
        '''
        Add an entry to the report

        :param key: entry's key
        :param value: the actual value

        :example:

            ::

                my_report.add('retry count', 3)
        '''
        if key in self.reserved_keys:
            raise Exception('You cannot add the key %s directly, use %s' % (key, self.reserved_keys[key]))
        if isinstance(value, Report):
            self._sub_reports[key] = value
            self._data_fields['sub_reports'].append(key)
        else:
            self._data_fields[key] = value

    def get(self, key):
        '''
        Get a value for a given key

        :param key: entry's key
        :return: corresponding value
        '''
        if key in self._data_fields:
            return self._data_fields[key]
        if key in self._sub_reports:
            return self._sub_reports[key]
        return None

    def to_dict(self, encoding='base64'):
        '''
        Return a dictionary version of the report

        :param encoding: required encoding for the string values (default: 'base64')
        :rtype: dictionary
        :return: dictionary representation of the report
        '''
        res = {}
        for k, v in self._data_fields.items():
            if isinstance(v, (bytes, bytearray, six.string_types)):
                v = StrEncodeEncoder(encoding).encode(v).tobytes().decode()
            res[k] = v
        for k, v in self._sub_reports.items():
            res[k] = v.to_dict(encoding)
        return res

    @classmethod
    def _decode(cls, val, encoding):
        if isinstance(val, six.string_types):
            if encoding == 'hex':
                val = unhexlify(val).decode()
            elif encoding == 'base64':
                val = b64decode(val)
                val = val.decode() if val.isalnum() else val
            else:
                val = codecs.decode(val, encoding).decode()
        return val

    @classmethod
    def from_dict(cls, d, encoding='base64'):
        '''
        Construct a ``Report`` object from dictionary.

        :type d: dictionary
        :param d: dictionary representing the report
        :param encoding: encoding of strings in the dictionary (default: 'base64')
        :return: Report object
        '''
        report = Report(Report._decode(d['name'], encoding))
        report.set_status(Report._decode(d['status'], encoding))
        sub_reports = Report._decode(d['sub_reports'], encoding)
        del d['sub_reports']
        for k, v in d.items():
            if k in sub_reports:
                report.add(k, Report.from_dict(v))
            else:
                if k.lower() == 'status':
                    report.set_status(Report._decode(v, encoding))
                else:
                    report.add(k, Report._decode(v, encoding))

        return report

    def get_status(self):
        '''
        Get the status of the report and its sub-reports.

        :rtype: str
        :return: report status ('passed', 'failed' or 'error')
        '''
        status = self.get('status')
        if status == Report.PASSED:
            for sr_name in self._sub_reports:
                sr = self._sub_reports[sr_name]
                sr_status = sr.get_status()
                reason = sr.get('reason')
                if sr_status == Report.ERROR:
                    self.error(reason)
                    break
                if sr_status == Report.FAILED:
                    self.failed(reason)
                    break
            status = self.get('status')
        return status

    def is_failed(self):
        '''
        .. deprecated:: 0.6.7
            use :func:`~kitty.data.report.Report.get_status`
        '''
        raise NotImplementedError('API was changed, use get_status instead')
