# coding=utf-8
"""
Behave formatter that supports TC
"""
import datetime
import traceback
from collections import deque

from behave.formatter.base import Formatter
from behave.model import Step, Feature, Scenario
from behave.model_core import Status

from teamcity.messages import TeamcityServiceMessages


def _step_name(step):
    assert isinstance(step, Step)
    return step.keyword + " " + step.name.strip()


def _suite_name(suite):
    return suite.name.strip()


class TeamcityFormatter(Formatter):
    """
    Stateful TC reporter.
    Since we can't fetch all steps from the very beginning (even skipped tests are reported)
    we store tests and features on each call.

    To hook into test reporting override _report_suite_started and/or _report_test_started
    """

    def __init__(self, stream_opener, config):
        super(TeamcityFormatter, self).__init__(stream_opener, config)
        self._messages = TeamcityServiceMessages()

        self.__feature = None
        self.__scenario = None
        self.__steps = deque()
        self.__scenario_opened = False
        self.__feature_opened = False

        self.__test_start_time = None

    def feature(self, feature):
        assert isinstance(feature, Feature)
        assert not self.__feature, "Prev. feature not closed"
        self.__feature = feature

    def scenario(self, scenario):
        assert isinstance(scenario, Scenario)
        self.__scenario = scenario
        self.__scenario_opened = False
        self.__steps.clear()

    def step(self, step):
        assert isinstance(step, Step)
        self.__steps.append(step)

    def match(self, match):
        if not self.__feature_opened:
            self._report_suite_started(self.__feature, _suite_name(self.__feature))
            self.__feature_opened = True

        if not self.__scenario_opened:
            self._report_suite_started(self.__scenario, _suite_name(self.__scenario))
            self.__scenario_opened = True

        assert self.__steps, "No steps left"

        step = self.__steps.popleft()
        self._report_test_started(step, _step_name(step))
        self.__test_start_time = datetime.datetime.now()

    def _report_suite_started(self, suite, suite_name):
        """
        :param suite: behave suite
        :param suite_name: suite name that must be reported, be sure to use it instead of suite.name

        """
        self._messages.testSuiteStarted(suite_name)

    def _report_test_started(self, test, test_name):
        """
        Suite name is always stripped, be sure to strip() it too
        :param test: behave test
        :param test_name: test name that must be reported, be sure to use it instead of test.name
        """
        self._messages.testStarted(test_name)

    def result(self, step):
        assert isinstance(step, Step)
        step_name = _step_name(step)
        fail_statuses = [Status.failed]
        if hasattr(Status, "error"):
            fail_statuses.append(Status.error)
        if step.status in fail_statuses:
            try:
                error = "".join(traceback.format_exception(step.exception))
                if error != step.error_message:
                    self._messages.testStdErr(step_name, error)
            except Exception:
                pass  # exception shall not prevent error message
            self._messages.testFailed(step_name, message=step.error_message)

        if step.status == Status.undefined:
            self._messages.testFailed(step_name, message="Undefined")

        if step.status == Status.skipped:
            self._messages.testIgnored(step_name)

        self._messages.testFinished(step_name, testDuration=datetime.datetime.now() - self.__test_start_time)

        if not self.__steps:
            self.__close_scenario()
        elif step.status in [Status.failed, Status.undefined]:
            # Broken background/undefined step stops whole scenario

            reason = "Undefined step" if step.status == Status.undefined else "Prev. step failed"
            self.__skip_rest_of_scenario(reason)

    def __skip_rest_of_scenario(self, reason):
        while self.__steps:
            step = self.__steps.popleft()
            self._report_test_started(step, _step_name(step))
            self._messages.testIgnored(_step_name(step),
                                       message="{0}. Rest part of scenario is skipped".format(reason))
            self._messages.testFinished(_step_name(step))
        self.__close_scenario()

    def __close_scenario(self):
        if self.__scenario:
            self._messages.testSuiteFinished(_suite_name(self.__scenario))
            self.__scenario = None

    def eof(self):
        self.__skip_rest_of_scenario("")
        self._messages.testSuiteFinished(_suite_name(self.__feature))
        self.__feature = None
        self.__feature_opened = False
