import argparse
import sys

import pytest
from tap.formatter import format_as_diagnostics
from tap.tracker import Tracker

SHOW_CAPTURE_LOG = ("log", "all")
SHOW_CAPTURE_OUT = ("stdout", "all")
SHOW_CAPTUER_ERR = ("stderr", "all")


class TAPPlugin:
    def __init__(self, config: pytest.Config) -> None:
        self._tracker = Tracker(
            outdir=config.option.tap_outdir,
            combined=config.option.tap_combined,
            streaming=config.option.tap_stream,
            stream=sys.stdout,
        )

        if self._tracker.streaming:
            reporter = config.pluginmanager.getplugin("terminalreporter")
            if reporter:
                config.pluginmanager.unregister(reporter)
            # A common pytest pattern is to use test functions without classes.
            # The header looks really dumb for that pattern because it puts
            # out a lot of line noise since every function gets its own header.
            # Disable it automatically for streaming.
            self._tracker.header = False

        self.show_capture = config.option.showcapture
        self.log_passing_tests = config.option.tap_log_passing_tests

    @pytest.hookimpl()
    def pytest_runtestloop(self, session):
        """Output the plan line first."""
        option = session.config.option
        if option.tap_stream or option.tap_combined:
            self._tracker.set_plan(session.testscollected)

    @pytest.hookimpl(optionalhook=True)
    def pytest_xdist_node_collection_finished(self, node, ids):
        """Output the plan line first when using xdist."""
        if self._tracker.streaming or self._tracker.combined:
            self._tracker.set_plan(len(ids))

    @pytest.hookimpl()
    def pytest_runtest_logreport(self, report: pytest.TestReport):
        """Add a test result to the tracker."""
        is_trackable_result = (
            (report.when == "setup" and report.outcome == "skipped")
            or (report.when == "setup" and report.outcome == "failed")
            or report.when == "call"
        )
        if not is_trackable_result:
            return

        description = str(report.location[0]) + "::" + str(report.location[2])
        testcase = report.location[0]

        # Handle xfails first because they report in unusual ways.
        # Non-strict xfails will include `wasxfail` while strict xfails won't.
        if hasattr(report, "wasxfail"):
            reason = ""
            # pytest adds an ugly "reason: " for expectedFailure
            # even though the standard library doesn't accept a reason
            # for that decorator.
            # Ignore the "reason: " from pytest.
            if report.wasxfail and report.wasxfail != "reason: ":
                reason = f": {report.wasxfail}"

            if report.skipped:
                directive = f"TODO expected failure{reason}"
                self._tracker.add_not_ok(testcase, description, directive=directive)
            elif report.passed:
                directive = f"TODO unexpected success{reason}"
                self._tracker.add_ok(testcase, description, directive=directive)
        elif report.passed:
            diagnostics = None
            if self.log_passing_tests:
                diagnostics = _make_as_diagnostics(report, self.show_capture)
            self._tracker.add_ok(testcase, description, diagnostics=diagnostics)
        elif report.failed:
            diagnostics = _make_as_diagnostics(report, self.show_capture)

            # pytest treats an unexpected success from unitest.expectedFailure
            # as a failure.
            # To match up with TAPTestResult and the TAP spec, treat the pass
            # as an ok with a todo directive instead.
            if "Unexpected success" in str(report.longrepr):
                self._tracker.add_ok(
                    testcase, description, directive="TODO unexpected success"
                )
                return

            # A strict xfail that passes (i.e., XPASS) should be marked as a failure.
            # The only indicator that strict xfail occurred
            # for XPASS is to check longrepr.
            if (
                isinstance(report.longrepr, str)
                and "[XPASS(strict)]" in report.longrepr
            ):
                self._tracker.add_not_ok(
                    testcase,
                    description,
                    directive=f"unexpected success: {report.longrepr}",
                )
                return

            self._tracker.add_not_ok(testcase, description, diagnostics=diagnostics)
        elif report.skipped:
            reason = report.longrepr[2].split(":", 1)[1].strip()  # type: ignore
            self._tracker.add_skip(testcase, description, reason)

    @pytest.hookimpl()
    def pytest_unconfigure(self, config: pytest.Config):
        """Dump the results."""
        self._tracker.generate_tap_reports()


def pytest_addoption(parser):
    """Include all the command line options."""
    group = parser.getgroup("terminal reporting", "reporting", after="general")
    group.addoption(
        "--tap",
        default=False,
        dest="tap_stream",
        action="store_true",
        help="Stream TAP output instead of the default test runner output.",
    )
    # Deprecated, but keeping for backwards compatibility.
    group.addoption(
        "--tap-stream", default=False, action="store_true", help=argparse.SUPPRESS
    )
    group.addoption(
        "--tap-files",
        default=False,
        action="store_true",
        help="Store all TAP test results into individual files per test case.",
    )
    group.addoption(
        "--tap-combined",
        default=False,
        action="store_true",
        help="Store all TAP test results into a combined output file.",
    )
    group.addoption(
        "--tap-outdir",
        metavar="path",
        help=(
            "An optional output directory to write TAP files to. "
            "If the directory does not exist, it will be created."
        ),
    )
    group.addoption(
        "--tap-log-passing-tests",
        default=False,
        action="store_true",
        help="Capture log information for passing tests to TAP report",
    )


@pytest.hookimpl(trylast=True)
def pytest_configure(config: pytest.Config) -> None:
    """Enable the plugin if the TAP flags are used."""
    # The help printing uses the terminalreporter,
    # which is unregistered by the streaming mode.
    if config.option.help:
        return

    if (
        config.option.tap_stream
        or config.option.tap_combined
        or config.option.tap_files
    ):
        config.pluginmanager.register(TAPPlugin(config), "tapplugin")


def _make_as_diagnostics(report, show_capture):
    """Format a report as TAP diagnostic output."""
    lines = report.longreprtext.splitlines(keepends=True)

    if show_capture in SHOW_CAPTURE_LOG:
        if lines:
            lines[-1] += "\n"
        lines += ["--- Captured Log ---\n"] + (
            report.caplog.splitlines(keepends=True) or [""]
        )
    if show_capture in SHOW_CAPTURE_OUT:
        if lines:
            lines[-1] += "\n"
        lines += ["--- Captured Out ---\n"] + (
            report.capstdout.splitlines(keepends=True) or [""]
        )
    if show_capture in SHOW_CAPTUER_ERR:
        if lines:
            lines[-1] += "\n"
        lines += ["--- Captured Err ---\n"] + (
            report.capstderr.splitlines(keepends=True) or [""]
        )

    return format_as_diagnostics(lines)
