###################################################################################
# LAVA QA API - This class represents a LAVA job object.
# Copyright (C) 2015, 2016 Collabora Ltd.

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library 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
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  US
###################################################################################

import json
import yaml
try:
    from xmlrpc.client import Fault
except ImportError:
    from xmlrpclib import Fault
from lqa_api.test import Test
from lqa_api.exit_codes import SUCCESS, \
    EXIT_CODE_INCOMPLETE_JOB, \
    EXIT_CODE_CANCELED_JOB

class JobError(Exception):
    """This is the base exception for all Job errors."""
    def __init__(self, msg, code):
        self.msg = msg
        self.code = code

    def __str__(self):
        return "{} {}".format(self.msg, self.code)

class Job(object):

    def __init__(self, job_id, server):
        self._id = job_id
        self._server = server
        self._status = {}
        self._bundle = {}
        self._tests = []
        self._details = self._get_job_details()
        # If it is a pipeline job then it is version 2
        self._is_v2 = self._details['is_pipeline']

    def __str__(self):
        return "{} - {}".format(self._id, self.name)

    def _fetch_job_status(self):
        """Method to fetch the job status"""
        try:
            self._status = self._server.job_status(self._id)
        except Fault as e:
            raise JobError(e.faultString, e.faultCode)

    def _get_job_details(self):
        """Method to fetch the job details"""
        try:
            return self._server.job_details(self._id)
        except Fault as e:
            raise JobError(e.faultString, e.faultCode)

    def _fetch_job_bundle(self):
        """Fetch job bundle"""
        if not self._bundle:
            try:
                bundle = self._server.get_bundle(self.sha1)
                self._bundle = json.loads(bundle['content'])
            except Fault as e:
                raise JobError(e.faultString, e.faultCode)

    def _fetch_tests(self):
        if not self._tests:
            # The way tests are fetched is different for jobs V1 and V2.
            if self.is_v2():
                tests_yaml = self._server.get_testjob_results_yaml(self.id)
                for test_record in yaml.load(tests_yaml):
                    self._tests.append(Test(test_record))
            else:
                for test_run in self.test_runs:
                    self._tests.append(Test(test_run, is_v2=False))

    def _get_job_details_prop(self, prop):
        """Get property prop from job details metadata"""
        return self._details.get(prop, None)

    # External API
    @property
    def id(self):
        return self._id

    @property
    def bundle(self):
        self._fetch_job_bundle()
        return self._bundle

    @property
    def details(self):
        return self._details

    @property
    def name(self):
        return self._get_job_details_prop('description')

    @property
    def bundle_link(self):
        return self._get_job_details_prop('_results_link')

    @property
    def priority(self):
        return { None : None,
                 0    : "low",
                 50   : "medium", 
                 100  : "high" }[self._get_job_details_prop('priority')]

    @property
    def submit_time(self):
        return self._get_job_details_prop('submit_time')

    @property
    def start_time(self):
        return self._get_job_details_prop('start_time')

    @property
    def end_time(self):
        return self._get_job_details_prop('end_time')

    @property
    def device_type(self):
        return self._get_job_details_prop('requested_device_type_id')

    @property
    def job_definition(self):
        return self._get_job_details_prop('definition')

    @property
    def hostname(self):
        dc = self._get_job_details_prop('_actual_device_cache')
        if dc:
            return dc.get('hostname', None)

    @property
    def worker_host(self):
        dc = self._get_job_details_prop('_actual_device_cache')
        if dc:
            return dc.get('worker_host_id', None)

    @property
    def failure_comment(self):
        return self._get_job_details_prop('failure_comment')

    @property
    def sha1(self):
        self._fetch_job_status()
        return self._status.get('bundle_sha1', None)

    @property
    def status(self):
        self._fetch_job_status()
        return self._status.get('job_status', None)

    @property
    def test_runs(self):
        self._fetch_job_bundle()
        return self._bundle.get('test_runs', None)

    @property
    def tests(self):
        self._fetch_tests()
        return self._tests

    def output(self, offset=0):
        return self._server.job_output(self.id, offset)

    def has_bundle(self):
        return self.bundle_link is not None

    def is_v2(self):
        return self._is_v2

    # Test for the different job statuses
    def is_submitted(self):
        return self.status == 'Submitted'

    def is_running(self):
        return self.status == 'Running'

    def is_complete(self):
        return self.status == 'Complete'

    def is_incomplete(self):
        return self.status == 'Incomplete'

    def is_canceled(self):
        return self.status == 'Canceled'

    def is_canceling(self):
        return self.status == 'Canceling'

    @property
    def exit_code(self):
        """This function maps job status to a command exit code"""
        ec = SUCCESS
        if self.is_incomplete():
            ec = EXIT_CODE_INCOMPLETE_JOB
        elif self.is_canceled() or self.is_canceling():
            ec = EXIT_CODE_CANCELED_JOB
        return ec

    @property
    def results(self):
        """
        This property gets the list of all the test results for a job with
        the following format per line:

        <job_id>:<test_id>:<test_suite>:<test_case>:<result>

        It calls the correct method for jobs version 1 or 2.
        """
        return (self._is_v2 and self._results_v2()) or self._results_v1()

    def _results_v2(self):
        """This method fetches results for LAVA jobs version 2"""
        results_lines = []
        for test in self.tests:
            results_lines.append("{}:{}:{}:{}:{}".format(
                self.id, self.name, test.suite, test.name, test.result))
        return results_lines

    def _results_v1(self):
        """This method fetches results for LAVA jobs version 1"""
        results_lines = []
        for test in self.tests:
            for test_result in test.results:
                results_lines.append("{}:{}:{}:{}:{}".format(
                    self.id, self.name, test.name,
                    test_result['test_case_id'], test_result['result']))
        return results_lines
