"""
PYTEST_DONT_REWRITE
"""
import enum
import logging
from datetime import datetime
from pathlib import Path
from test.manifest import RDFT
from typing import TYPE_CHECKING, Generator, Optional, Tuple, cast

import pytest

from pytest import Item

from rdflib import RDF, BNode, Graph, Literal, Namespace, URIRef
from rdflib.namespace import DC, DOAP, FOAF, DefinedNamespace
from rdflib.term import Node

if TYPE_CHECKING:
    from _pytest.main import Session
    from _pytest.python import CallSpec2
    from _pytest.reports import TestReport
    from _pytest.runner import CallInfo
    from pluggy._result import _Result


class EARL(DefinedNamespace):
    _fail = True
    _NS = Namespace("http://www.w3.org/ns/earl#")

    assertedBy: URIRef  # assertor of an assertion
    Assertion: URIRef  # a statement that embodies the results of a test
    Assertor: URIRef  # an entity such as a person, a software tool, an organization, or any other grouping that carries out a test collectively
    automatic: URIRef  # where the test was carried out automatically by the software tool and without any human intervention
    CannotTell: URIRef  # the class of outcomes to denote an undetermined outcome
    cantTell: URIRef  # it is unclear if the subject passed or failed the test
    failed: URIRef  # the subject failed the test
    Fail: URIRef  # the class of outcomes to denote failing a test
    inapplicable: URIRef  # the test is not applicable to the subject
    info: URIRef  # additional warnings or error messages in a human-readable form
    mainAssertor: URIRef  # assertor that is primarily responsible for performing the test
    manual: URIRef  # where the test was carried out by human evaluators
    mode: URIRef  # mode in which the test was performed
    NotApplicable: URIRef  # the class of outcomes to denote the test is not applicable
    NotTested: URIRef  # the class of outcomes to denote the test has not been carried out
    outcome: URIRef  # outcome of performing the test
    OutcomeValue: URIRef  # a discrete value that describes a resulting condition from carrying out the test
    passed: URIRef  # the subject passed the test
    Pass: URIRef  # the class of outcomes to denote passing a test
    pointer: URIRef  # location within a test subject that are most relevant to a test result
    result: URIRef  # result of an assertion
    semiAuto: URIRef  # where the test was partially carried out by software tools, but where human input or judgment was still required to decide or help decide the outcome of the test
    Software: URIRef  # any piece of software such as an authoring tool, browser, or evaluation tool
    subject: URIRef  # test subject of an assertion
    TestCase: URIRef  # an atomic test, usually one that is a partial test for a requirement
    TestCriterion: URIRef  # a testable statement, usually one that can be passed or failed
    TestMode: URIRef  # describes how a test was carried out
    TestRequirement: URIRef  # a higher-level requirement that is tested by executing one or more sub-tests
    TestResult: URIRef  # the actual result of performing the test
    TestSubject: URIRef  # the class of things that have been tested against some test criterion
    test: URIRef  # test criterion of an assertion
    undisclosed: URIRef  # where the exact testing process is undisclosed
    unknownMode: URIRef  # where the testing process is unknown or undetermined
    untested: URIRef  # the test has not been carried out


class EarlReport:
    """
    This is a helper class for building an EARL report graph.
    """

    def __init__(
        self,
        asserter_uri: Optional[str] = None,
        asserter_homepage: Optional[str] = None,
        asserter_name: Optional[str] = None,
    ) -> None:
        self.graph = graph = Graph()
        graph.bind("foaf", FOAF)
        graph.bind("earl", EARL)
        graph.bind("doap", DOAP)
        graph.bind("dc", DC)

        self.asserter: Node
        asserter: Node
        if asserter_uri is not None or asserter_homepage is not None:
            self.asserter = asserter = URIRef(
                asserter_homepage if asserter_uri is None else asserter_uri
            )
            graph.add((asserter, RDF.type, FOAF.Person))
        else:
            self.asserter = asserter = BNode()
            graph.add((asserter, RDF.type, FOAF.Person))
        if asserter_name:
            graph.add((asserter, FOAF.name, Literal(asserter_name)))
        if asserter_homepage:
            graph.add((asserter, FOAF.homepage, URIRef(asserter_homepage)))

        self.project = project = URIRef("https://github.com/RDFLib/rdflib")

        graph.add((project, DOAP.homepage, project))
        graph.add((project, DOAP.name, Literal("RDFLib")))
        graph.add((project, RDF.type, DOAP.Project))
        graph.add((project, DOAP["programming-language"], Literal("Python")))
        graph.add(
            (
                project,
                DOAP.description,
                Literal(
                    (
                        "RDFLib is a Python library for working with RDF, "
                        "a simple yet powerful language for representing information."
                    ),
                    lang="en",
                ),
            )
        )

        self.now = Literal(datetime.now())

    def add_test_outcome(
        self, test_id: URIRef, outcome: URIRef, info: Optional[Literal] = None
    ) -> Tuple[Node, Node]:
        graph = self.graph
        assertion = BNode()
        graph.add((assertion, RDF.type, EARL.Assertion))
        graph.add((assertion, EARL.test, test_id))
        graph.add((assertion, EARL.subject, self.project))
        graph.add((assertion, EARL.mode, EARL.automatic))
        if self.asserter:
            graph.add((assertion, EARL.assertedBy, self.asserter))

        result = BNode()
        graph.add((assertion, EARL.result, result))
        graph.add((result, RDF.type, EARL.TestResult))
        graph.add((result, DC.date, self.now))
        graph.add((result, EARL.outcome, outcome))
        if info:
            graph.add((result, EARL.info, info))

        return graph, result


def pytest_addoption(parser):
    group = parser.getgroup("terminal reporting")
    group.addoption(
        "--earl-report",
        action="store",
        dest="earl_path",
        metavar="path",
        default=None,
        help="create EARL report file at given path.",
    )

    group.addoption(
        "--earl-asserter-uri",
        action="store",
        dest="earl_asserter_uri",
        metavar="uri",
        default=None,
        help="Set the EARL asserter URI, defaults to the asserter homepage if not set.",
    )

    group.addoption(
        "--earl-asserter-homepage",
        action="store",
        dest="earl_asserter_homepage",
        metavar="URL",
        default=None,
        help="Set the EARL asserter homepage.",
    )

    group.addoption(
        "--earl-asserter-name",
        action="store",
        dest="earl_asserter_name",
        metavar="name",
        default=None,
        help="Set the EARL asserter name.",
    )


def pytest_configure(config):
    earl_path = config.option.earl_path
    if earl_path:
        config._earl = EarlReporter(
            Path(earl_path),
            EarlReport(
                asserter_uri=config.option.earl_asserter_uri,
                asserter_name=config.option.earl_asserter_name,
                asserter_homepage=config.option.earl_asserter_homepage,
            ),
        )
        config.pluginmanager.register(config._earl)


def pytest_unconfigure(config):
    earl = getattr(config, "_excel", None)
    if earl:
        del config._earl
        config.pluginmanager.unregister(earl)


# https://docs.pytest.org/en/latest/reference.html#pytest.hookspec.pytest_runtest_protocol


class TestResult(enum.Enum):
    PASS = enum.auto()
    FAIL = enum.auto()
    ERROR = enum.auto()
    SKIP = enum.auto()


class TestReportHelper:
    @classmethod
    def get_rdf_test_uri(cls, report: "TestReport") -> Optional[URIRef]:
        return next(
            (
                cast(URIRef, item[1])
                for item in report.user_properties
                if item[0] == RDFT.Test
            ),
            None,
        )


class EarlReporter:
    """
    This class is a pytest plugin that will write a EARL report with results for
    every pytest which has a rdf_test_uri parameter that is a string or an
    URIRef.
    """

    def __init__(self, output_path: Path, report: Optional[EarlReport] = None) -> None:
        self.report = report if report is not None else EarlReport()
        self.output_path = output_path

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_makereport(
        self, item: Item, call: "CallInfo[None]"
    ) -> Generator[None, "_Result", None]:
        result = yield

        report: "TestReport" = result.get_result()

        if not hasattr(item, "callspec"):
            return
        callspec: "CallSpec2" = getattr(item, "callspec")
        rdf_test_uri = callspec.params.get("rdf_test_uri")
        if rdf_test_uri is None:
            return
        if not isinstance(rdf_test_uri, URIRef) and not isinstance(rdf_test_uri, str):
            logging.warning("rdf_test_uri parameter is not a URIRef or a str")
            return
        if not isinstance(rdf_test_uri, URIRef):
            rdf_test_uri = URIRef(rdf_test_uri)

        report.user_properties.append((RDFT.Test, rdf_test_uri))

    def append_result(self, report: "TestReport", test_result: TestResult) -> None:
        rdf_test_uri = TestReportHelper.get_rdf_test_uri(report)
        if rdf_test_uri is None:
            # No RDF test
            return
        if test_result is TestResult.PASS:
            self.report.add_test_outcome(rdf_test_uri, EARL.passed)
        elif test_result is TestResult.FAIL:
            self.report.add_test_outcome(rdf_test_uri, EARL.failed)
        elif (test_result) is TestResult.SKIP:
            self.report.add_test_outcome(rdf_test_uri, EARL.untested)
        else:
            self.report.add_test_outcome(rdf_test_uri, EARL.cantTell)

    def pytest_runtest_logreport(self, report: "TestReport") -> None:
        if report.passed:
            if report.when == "call":  # ignore setup/teardown
                self.append_result(report, TestResult.PASS)
        elif report.failed:
            if report.when == "call":  # ignore setup/teardown
                self.append_result(report, TestResult.FAIL)
            else:
                self.append_result(report, TestResult.ERROR)
        elif report.skipped:
            self.append_result(report, TestResult.SKIP)

    def pytest_sessionfinish(self, session: "Session"):
        self.report.graph.serialize(format="turtle", destination=self.output_path)
