#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import (
    absolute_import,
    print_function,
    unicode_literals,
)

import collections
import sys

import yaml

from ansible import constants as C
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.plugins.callback import CallbackBase
from ansible.utils.color import stringc

__metaclass__ = type


def indent(text, indent=2, char=' '):
    return '\n'.join('{indent}{line}'.format(indent=indent*char, line=line)
                     for line in text.splitlines())


def dump_yaml(data, **kwargs):
    return '\n'.join([
        '---',
        yaml.dump(data, Dumper=AnsibleDumper, allow_unicode=True,
                  default_flow_style=False, **kwargs).strip(),
        '...',
    ])


class TestResult(object):
    PASSED = ('passed',)
    FAILED = ('failed',)
    EXPECTED = ('expected',)
    UNEXPECTED = ('unexpected',)
    SKIPPED = ('skipped',)


class CallbackModule(CallbackBase):
    """
    TAP output for Ansible.
    """

    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'tap'

    if sys.stdout.isatty():
        OK = stringc('ok', C.COLOR_OK)
        NOT_OK = stringc('not ok', C.COLOR_ERROR)
    else:
        OK = 'ok'
        NOT_OK = 'not ok'

    def __init__(self):
        super(CallbackModule, self).__init__()
        # Play stats will include all tasks. We want to exclude setup/teardown
        # tasks (tagged with 'diagnostic') from the final test count.
        self.counter = collections.Counter()

    def description(self, result):
        """
        Construct a test line description based on the name of the Ansible
        module and task name.
        """
        description = '{0}'.format(result._task.action)
        if result._task.name:
            description = '{0}: {1}'.format(description, result._task.name)
        return description

    def ok(self, result):
        """
        Print a passed test.
        """
        if 'TODO' in result._task.tags:
            directive = '# TODO'
        else:
            directive = None
        description = self.description(result)
        output = self._tap(self.OK, description, directive=directive)
        self._display.display(output)

    def skip(self, result):
        """
        Print a skipped test.
        """
        description = self.description(result)
        directive = '# SKIP {0}'.format(result._result['skip_reason'])
        output = self._tap(self.OK, description, directive=directive)
        self._display.display(output)

    def not_ok(self, result):
        """
        Print a failed test.
        """
        if 'TODO' in result._task.tags:
            directive = '# TODO'
        else:
            directive = None
        description = self.description(result)
        output = self._tap(self.NOT_OK, description, directive=directive)
        self._display.display(output)

    def _tap(self, status, description, directive=None):
        """
        Print a TAP test line.
        """
        test_line = '{0} - {1}'.format(status, description)
        if directive:
            test_line += ' {0}'.format(directive)
        lines = [test_line]
        return '\n'.join(lines)

    def v2_playbook_on_start(self, playbook):
        self._display.display('TAP version 13')

    def v2_runner_on_failed(self, result, ignore_errors=False):
        self.not_ok(result)
        # Print reason for failure if this was not an expected failure.
        if 'TODO' not in result._task.tags:
            self._display.display(indent(dump_yaml(result._result)))
            self.counter.update(TestResult.EXPECTED)
            return
        self.counter.update(TestResult.FAILED)

    def v2_runner_on_ok(self, result):
        if 'diagnostic' in result._task.tags:
            self._display.display('# {0}'.format(self.description(result)))
            return
        if 'TODO' in result._task.tags:
            self.counter.update(TestResult.UNEXPECTED)
        else:
            self.counter.update(TestResult.PASSED)
        self.ok(result)

    def v2_runner_on_skipped(self, result):
        self.skip(result)
        self.counter.update(TestResult.SKIPPED)

    def v2_playbook_on_stats(self, stats):
        self._display.display('1..{}'.format(sum(self.counter.values())))
        # Because tests set `ignore_errors`, we need to call exit() ourselves.
        if self.counter['failed']:
            sys.exit(TaskQueueManager.RUN_FAILED_HOSTS)

@@ -996,24 +996,93 @@
 kipped',)%0A%0A%0A
+class Tag(object):%0A    TODO = 'todo'%0A    DIAGNOSTIC = 'diagnostic'%0A%0A%0A
 class Callba
@@ -1101,24 +1101,24 @@
 lbackBase):%0A
-
     %22%22%22%0A    
@@ -2116,34 +2116,90 @@
     
-if 'TODO' in result._task.
+cleaned_tags = self._clean_tags(result._task.tags)%0A        if Tag.TODO in cleaned_
 tags
@@ -2836,34 +2836,90 @@
     
-if 'TODO' in result._task.
+cleaned_tags = self._clean_tags(result._task.tags)%0A        if Tag.TODO in cleaned_
 tags
@@ -3461,16 +3461,107 @@
 lines)%0A%0A
+    @staticmethod%0A    def _clean_tags(tags):%0A        return %5Btag.lower() for tag in tags%5D%0A%0A
     def 
@@ -3731,32 +3731,91 @@
 .not_ok(result)%0A
+        cleaned_tags = self._clean_tags(result._task.tags)%0A
         # Print 
@@ -3885,22 +3885,24 @@
  if 
-'
+Tag.
 TODO
-'
  not in 
 resu
@@ -3893,37 +3893,32 @@
 TODO not in 
-result._task.
+cleaned_
 tags:%0A      
@@ -4151,40 +4151,96 @@
     
-if 'diagnostic' in result._task.
+cleaned_tags = self._clean_tags(result._task.tags)%0A        if Tag.DIAGNOSTIC in cleaned_
 tags
@@ -4351,31 +4351,28 @@
  if 
-'
+Tag.
 TODO
-'
  in 
-result._task.
+cleaned_
 tags

Clean user input for tags