__doc__ = """
In your settings, use

  TEST_RUNNER = 'ambidjangolib.test.simple.run_tests_until_fail'

to make `manage.py test` stop after the first test suite with failures, and
only show the first failing test in the suite.
"""


from unittest import \
     TestSuite, TextTestRunner, _TextTestResult, defaultTestLoader
from django.test import _doctest as doctest
from django.conf import settings
from django.db import transaction
from django.db.models import get_app, get_apps
from django.test.utils import \
     setup_test_environment, teardown_test_environment, \
     create_test_db, destroy_test_db
from django.test.simple import build_test, get_tests, doctestOutputChecker
from django.test.testcases import DocTestRunner


class DocTestRunner(doctest.DocTestRunner):
    """
    Replacement for django.test.testcases.DocTestRunner which unfortunately
    overrides any supplied `optionflags=` kwarg with only `doctest.ELLIPSIS`.
    We need to pass on optionflags.
    """
    def __init__(self, *args, **kwargs):
        doctest.DocTestRunner.__init__(self, *args, **kwargs)
        self.optionflags |= doctest.ELLIPSIS
        # Django's original has `=` instead of `|=` here

    def report_unexpected_exception(self, out, test, example, exc_info):
        doctest.DocTestRunner.report_unexpected_exception(self, out, test,
                                                          example, exc_info)
        # Rollback, in case of database errors. Otherwise they'd have
        # side effects on other tests.
        transaction.rollback_unless_managed()


def _add_tests_for_module(suite, module):
    """
    Repeated code from inside `build_suite()` is refactored here.
    """
    # Load unit and doctests in the given module. If module has a suite()
    # method, use it. Otherwise build the test suite ourselves.
    if hasattr(module, 'suite'):
        suite.addTest(module.suite())
    else:
        suite.addTest(defaultTestLoader.loadTestsFromModule(module))
        try:
            suite.addTest(doctest.DocTestSuite(
                module,
                checker=doctestOutputChecker,
                runner=DocTestRunner,
                optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
        except ValueError:
            # No doc tests in models.py
            pass


def build_suite(app_module):
    """
    Create a complete Django test suite for the provided application module.

    This overrides Django's original `django.test.simple.build_suite()` because
    we need to pass the `REPORT_ONLY_FIRST_FAILURE` option flag to
    `DocTestSuite` instances.
    """
    suite = TestSuite()

    _add_tests_for_module(suite, app_module)

    # Check to see if a separate 'tests' module exists parallel to the
    # models module
    test_module = get_tests(app_module)
    if test_module:
        _add_tests_for_module(suite, test_module)

    return suite


class _FailStopTextTestResult(_TextTestResult):
    def addError(self, test, err):
        _TextTestResult.addError(self, test, err)
        self.shouldStop = True

    def addFailure(self, test, err):
        _TextTestResult.addFailure(self, test, err)
        self.shouldStop = True


class FailStopTextTestRunner(TextTestRunner):
    def _makeResult(self):
        return _FailStopTextTestResult(
            self.stream, self.descriptions, self.verbosity)


def run_tests_until_fail(test_labels, verbosity=1, interactive=True, extra_tests=[]):
    """
    Run the unit tests for all the test labels in the provided list.
    Labels must be of the form:
     - app.TestClass.test_method
        Run a single specific test method
     - app.TestClass
        Run all the test methods in a given class
     - app
        Search for doctests and unittests in the named application.

    When looking for tests, the test runner will look in the models and
    tests modules for the application.

    A list of 'extra' tests may also be provided; these tests
    will be added to the test suite.

    Stops the tests at the first failure and returns 1.  If all test pass,
    returns 0.

    Also displays only the first failure in the failing test suite.
    """
    setup_test_environment()

    settings.DEBUG = False
    suite = TestSuite()

    if test_labels:
        for label in test_labels:
            if '.' in label:
                suite.addTest(build_test(label))
            else:
                app = get_app(label)
                suite.addTest(build_suite(app))
    else:
        for app in get_apps():
            suite.addTest(build_suite(app))

    for test in extra_tests:
        suite.addTest(test)

    old_name = settings.DATABASE_NAME
    create_test_db(verbosity, autoclobber=not interactive)
    result = FailStopTextTestRunner(verbosity=verbosity).run(suite)
    destroy_test_db(old_name, verbosity)

    teardown_test_environment()

    return len(result.failures) + len(result.errors)
