from time import time
from traceback import format_exception
import sys

from pyrsistent import v
from twisted.python.reflect import fullyQualifiedName as fully_qualified_name
import attr


@attr.s
class _DelayedMessage:
    """
    A test status message that will be shown at the end of a run.
    """

    width = attr.ib()
    status = attr.ib()
    subject = attr.ib()
    body = attr.ib(default=())

    def lines(self):
        yield "=" * self.width
        yield "\n"
        yield self.status
        yield "\n"
        if self.body:
            yield from self.body
            yield "\n"


class Outputter:

    _last_test_class = None
    _last_test_module = None

    FAILED, PASSED = "FAILED", "PASSED"
    ERROR, FAIL, OK, SKIPPED = "[ERROR]", "[FAIL]", "[OK]", "[SKIPPED]"
    EXPECTED_FAILURE, UNEXPECTED_SUCCESS = "[XFAIL]", "[UNEXPECTED SUCCESS]"

    _COLORS = [
        ("_error", "RED", ERROR),
        ("_fail", "RED", FAIL),
        ("_failed", "RED", FAILED),
        ("_ok", "GREEN", OK),
        ("_passed", "GREEN", PASSED),
        ("_skipped", "BLUE", SKIPPED),
        ("_unexpected_success", "YELLOW", UNEXPECTED_SUCCESS),
        ("_expected_failure", "BLUE", EXPECTED_FAILURE),
    ]

    def __init__(self, colored=True, indent=" " * 2, line_width=120):
        self.indent = indent
        self.line_width = line_width

        if colored:
            from colorama import Fore, Style
            for attribute, color, text in self._COLORS:
                color = getattr(Fore, color)
                message = f"{Style.BRIGHT}{color}{text}{Style.RESET_ALL}"
                setattr(self, attribute, message)
        else:
            for attribute, _, text in self._COLORS:
                setattr(self, attribute, text)

        self._later = []

    def _show_later(self, **kwargs):
        self._later.append(_DelayedMessage(width=self.line_width, **kwargs))

    def run_started(self):
        pass

    def run_stopped(self, recorder, runtime):
        last = None, None
        for i, each in enumerate(self._later):
            yield "\n"
            if (each.status, each.body) != last or i == len(self._later):
                yield from each.lines()
            yield _test_name(each.subject)

            last = each.status, each.body

        yield "\n"
        yield "-" * self.line_width
        tests = "tests" if recorder.testsRun != 1 else "test"
        yield f"\nRan {recorder.testsRun} {tests} in {runtime:.3f}s\n\n"
        if recorder.wasSuccessful():
            yield self._passed
        else:
            yield self._failed

        if recorder.testsRun:
            yield " ("
            summary = []
            for attribute in (
                "successes",
                "skips",
                "failures",
                "errors",
                "expected_failures",
                "unexpected_successes",
            ):
                subcount = len(getattr(recorder, attribute))
                if subcount:
                    summary.append(f"{attribute}={subcount}")
            yield ", ".join(summary)
            yield ")"

        yield "\n"

    def test_started(self, test):
        cls = test.__class__
        module = cls.__module__

        if self._last_test_module != module:
            self._last_test_module = module
            yield module
            yield "\n"

        if self._last_test_class != cls:
            self._last_test_class = cls
            yield self.indent
            yield cls.__name__
            yield "\n"

    def test_stopped(self, test):
        pass

    def test_errored(self, test, exc_info):
        self._show_later(
            status=self.ERROR,
            body=format_exception(*exc_info),
            subject=test,
        )
        return self.format_line(test, self._error)

    def test_failed(self, test, exc_info):
        self._show_later(
            status=self.FAIL,
            body=format_exception(*exc_info),
            subject=test,
        )
        return self.format_line(test, self._fail)

    def test_skipped(self, test, reason):
        self._show_later(status=self.SKIPPED, body=reason, subject=test)
        return self.format_line(test, self._skipped)

    def test_expectedly_failed(self, test, exc_info):
        self._show_later(status=self.EXPECTED_FAILURE, subject=test)
        return self.format_line(test, self._expected_failure)

    def test_unexpectedly_succeeded(self, test):
        self._show_later(status=self.UNEXPECTED_SUCCESS, subject=test)
        return self.format_line(test, self._unexpected_success)

    def test_succeeded(self, test):
        return self.format_line(test, self._ok)

    def format_line(self, test, result):
        before = f"{self.indent}{self.indent}{test._testMethodName} ..."
        return self._pad_center(left=before, right=result) + "\n"

    def _pad_center(self, left, right):
        space = self.line_width - len(left) - len(right)
        return left + " " * space + right


@attr.s
class Counter:
    """
    A counter is a recorder that does not hold references to tests it sees.
    """

    errors = attr.ib(default=0)
    failures = attr.ib(default=0)
    expected_failures = attr.ib(default=0)
    unexpected_successes = attr.ib(default=0)
    successes = attr.ib(default=0)

    shouldStop = False

    @property
    def count(self):
        return sum(attr.astuple(self))

    testsRun = count

    def startTest(self, test):
        pass

    def stopTest(self, test):
        pass

    def addError(self, test, exc_info):
        self.errors += 1

    def addFailure(self, test, exc_info):
        self.failures += 1

    def addExpectedFailure(self, *args, **kwargs):
        self.expected_failures += 1

    def addUnexpectedSuccess(self, test):
        self.unexpected_successes += 1

    def addSuccess(self, test):
        self.successes += 1


@attr.s
class Recorder:

    errors = attr.ib(default=v())
    failures = attr.ib(default=v())
    skips = attr.ib(default=v())
    successes = attr.ib(default=v())
    expected_failures = attr.ib(default=v())
    unexpected_successes = attr.ib(default=v())

    shouldStop = False

    @property
    def testsRun(self):
        return sum(len(tests) for tests in attr.astuple(self))

    def startTestRun(self):
        pass

    def stopTestRun(self):
        pass

    def startTest(self, test):
        pass

    def stopTest(self, test):
        pass

    def addError(self, test, exc_info):
        self.errors = self.errors.append((test, exc_info))

    def addFailure(self, test, exc_info):
        self.failures = self.failures.append((test, exc_info))

    def addExpectedFailure(self, test, exc_info):
        self.expected_failures = self.expected_failures.append(
            (test, exc_info),
        )

    def addSkip(self, test, reason):
        self.skips = self.skips.append(test)

    def addSuccess(self, test):
        self.successes = self.successes.append(test)

    def addUnexpectedSuccess(self, test):
        self.unexpected_successes = self.unexpected_successes.append(test)

    def wasSuccessful(self):
        return not (self.errors or self.failures or self.unexpected_successes)


@attr.s
class ComponentizedReporter:

    outputter = attr.ib(default=attr.Factory(Outputter))
    recorder = attr.ib(default=attr.Factory(Recorder), repr=False)
    stream = attr.ib(default=sys.stdout)
    _time = attr.ib(default=time, repr=False)

    shouldStop = False

    @property
    def testsRun(self):
        return self.recorder.testsRun

    def startTestRun(self):
        self._start_time = self._time()
        self.recorder.startTestRun()
        self.stream.writelines(self.outputter.run_started() or "")

    def stopTestRun(self):
        self.recorder.stopTestRun()
        runtime = self._time() - self._start_time
        self.stream.writelines(
            self.outputter.run_stopped(self.recorder, runtime) or "",
        )

    def startTest(self, test):
        self.recorder.startTest(test)
        self.stream.writelines(self.outputter.test_started(test) or "")

    def stopTest(self, test):
        self.recorder.stopTest(test)
        self.stream.writelines(self.outputter.test_stopped(test) or "")

    def addError(self, test, exc_info):
        self.recorder.addError(test, exc_info)
        self.stream.writelines(
            self.outputter.test_errored(test, exc_info) or "",
        )

    def addFailure(self, test, exc_info):
        self.recorder.addFailure(test, exc_info)
        self.stream.writelines(
            self.outputter.test_failed(test, exc_info) or "",
        )

    def addSkip(self, test, reason):
        self.recorder.addSkip(test, reason)
        self.stream.writelines(self.outputter.test_skipped(test, reason) or "")

    def addExpectedFailure(self, test, exc_info):
        self.recorder.addExpectedFailure(test, exc_info)
        self.stream.writelines(
            self.outputter.test_expectedly_failed(test, exc_info) or "",
        )

    def addUnexpectedSuccess(self, test):
        self.recorder.addUnexpectedSuccess(test)
        self.stream.writelines(
            self.outputter.test_unexpectedly_succeeded(test) or "",
        )

    def addSuccess(self, test):
        self.recorder.addSuccess(test)
        self.stream.writelines(self.outputter.test_succeeded(test) or "")

    def wasSuccessful(self):
        return self.recorder.wasSuccessful()


def _test_name(test):
    """
    Retrieve the name of the given test.

    Arguments:

        test (TestCase):

            a test case instance

    """

    return ".".join(
        (fully_qualified_name(test.__class__), test._testMethodName),
    )
