from docutils.nodes import (
    reference,
    bullet_list,
    list_item,
    literal,
    raw,
    paragraph,
    Text,
)

from releases import Issue, construct_releases, construct_nodes

from _util import b, f, s, entry, make_app, release, releases, setup_issues


def _obj2name(obj):
    cls = obj if isinstance(obj, type) else obj.__class__
    return cls.__name__.split(".")[-1]


def _expect_type(node, cls):
    type_ = _obj2name(node)
    name = _obj2name(cls)
    msg = f"Expected {node!r} to be a {name}, but it's a {type_}"
    assert isinstance(node, cls), msg


class presentation:
    """
    Expansion/extension of docutils nodes (rendering)
    """

    def setup_method(self):
        setup_issues(self)

    def _generate(self, *entries, **kwargs):
        raw = kwargs.pop("raw", False)
        nodes = construct_nodes(releases(*entries, **kwargs))
        # By default, yield the contents of the bullet list.
        return nodes if raw else nodes[0][1][0]

    def _test_link(self, kwargs, type_, expected, entries=None):
        app = make_app(**kwargs)
        # Lazy-evaluated entries: (callable, *args)
        entries = entries or [
            (release, "1.0.2"),
            (b, 15),
            (release, "1.0.0"),
        ]
        nodes = construct_nodes(
            construct_releases(
                [entry(x[0](*x[1:], app=app)) for x in entries],
                app=app,
            )[0]
        )
        # Shorthand for "I'll do my own asserts"
        if expected is None:
            return nodes
        if type_ == "release":
            header = nodes[0][0][0].astext()
            assert expected in header
        elif type_ == "issue":
            link = nodes[0][1][0][0][2]
            assert link["refuri"] == expected
        else:
            raise Exception("Gave unknown type_ kwarg to _test_link()!")

    def issues_with_numbers_appear_as_number_links(self):
        self._test_link({}, "issue", "bar_15")

    def releases_appear_as_header_links(self):
        self._test_link({}, "release", "foo_1.0.2")

    def links_will_use_github_option_if_defined(self):
        kwargs = {
            "release_uri": None,
            "issue_uri": None,
            "github_path": "foo/bar",
        }
        for type_, expected in (
            ("issue", "https://github.com/foo/bar/issues/15"),
            ("release", "https://github.com/foo/bar/tree/1.0.2"),
        ):
            self._test_link(kwargs, type_, expected)

    def issue_links_prefer_explicit_setting_over_github_setting(self):
        kwargs = {
            "release_uri": None,
            "issue_uri": "explicit_issue_%s",
            "github_path": "foo/bar",
        }
        self._test_link(kwargs, "issue", "explicit_issue_15")

    def release_links_prefer_explicit_setting_over_github_setting(self):
        kwargs = {
            "release_uri": "explicit_release_%s",
            "issue_uri": None,
            "github_path": "foo/bar",
        }
        self._test_link(kwargs, "release", "explicit_release_1.0.2")

    def links_allow_curlybrace_number_formatting_too(self):
        kwargs = {
            "release_uri": "release_{number}",
            "issue_uri": "issue_{number}",
            "github_path": None,
        }
        self._test_link(kwargs, "release", "release_1.0.2")
        self._test_link(kwargs, "issue", "issue_15")

    def completely_blank_uri_settings_does_not_asplode(self):
        kwargs = {"release_uri": None, "issue_uri": None, "github_path": None}
        # Get nodes for direct inspection
        nodes = self._test_link(kwargs, "release", None)
        # Ensure release entry still displays release version.
        # (These are curently constructed as raw text nodes so no other great
        # way to test this. Meh.)
        text = nodes[0][0][0].astext()
        assert ">1.0.2 <span" in text
        # Ensure issues still display issue number. (Ditto.)
        text = nodes[0][1][0][0].astext()
        assert ">Bug</span>] #15:" in text

    def _assert_prefix(self, entries, expectation):
        assert expectation in self._generate(*entries)[0][0][0]

    def bugs_marked_as_bugs(self):
        self._assert_prefix(["1.0.2", self.b], "Bug")

    def features_marked_as_features(self):
        self._assert_prefix(["1.1.0", self.f], "Feature")

    def support_marked_as_support(self):
        self._assert_prefix(["1.1.0", self.s], "Support")

    def dashed_issues_appear_as_unlinked_issues(self):
        node = self._generate("1.0.2", b("-"))
        assert not isinstance(node[0][2], reference)

    def zeroed_issues_appear_as_unlinked_issues(self):
        node = self._generate("1.0.2", b(0))
        assert not isinstance(node[0][2], reference)

    def un_prefixed_list_items_appear_as_unlinked_bugs(self):
        fake = list_item(
            "",
            paragraph(
                "", "", Text("fixes an issue in "), literal("", "methodname")
            ),
        )
        node = self._generate("1.0.2", fake)
        # [<raw prefix>, <inline colon>, <inline space>, <text>, <monospace>]
        assert len(node[0]) == 5
        assert "Bug" in str(node[0][0])
        assert "fixes an issue" in str(node[0][3])
        assert "methodname" in str(node[0][4])

    def un_prefixed_list_items_get_no_prefix_under_unstable_prehistory(self):
        app = make_app(unstable_prehistory=True)
        fake = list_item("", paragraph("", "", raw("", "whatever")))
        node = self._generate("0.1.0", fake, app=app, skip_initial=True)
        # [<raw bug text>]
        assert len(node[0]) == 1
        assert "Bug" not in str(node[0][0])
        assert "whatever" in str(node[0][0])

    def issues_remain_wrapped_in_unordered_list_nodes(self):
        node = self._generate("1.0.2", self.b, raw=True)[0][1]
        _expect_type(node, bullet_list)
        _expect_type(node[0], list_item)

    def release_headers_have_local_style_tweaks(self):
        node = self._generate("1.0.2", self.b, raw=True)[0][0]
        _expect_type(node, raw)
        # Header w/ bottom margin
        assert '<h2 style="margin-bottom' in str(node)
        # Date span w/ font-size
        assert '<span style="font-size' in str(node)

    def descriptions_are_preserved(self):
        # Changelog containing an issue item w/ trailing node
        issue = list_item(
            "", paragraph("", "", self.b.deepcopy(), raw("", "x"))
        )
        # Trailing nodes should appear post-processing after the link/etc
        rest = self._generate("1.0.2", issue)[0]
        assert len(rest) == 5
        _expect_type(rest[4], raw)
        assert rest[4].astext() == "x"

    def complex_descriptions_are_preserved(self):
        # Complex 'entry' mapping to an outer list_item (list) containing two
        # paragraphs, one w/ the real issue + desc, another simply a 2nd text
        # paragraph. Using 'raw' nodes for filler as needed.
        issue = list_item(
            "",
            paragraph("", "", self.b.deepcopy(), raw("", "x")),
            paragraph("", "", raw("", "y")),
        )
        li = self._generate("1.0.2", issue)
        # Expect that the machinery parsing issue nodes/nodelists, is not
        # discarding our 2nd 'paragraph'
        assert len(li) == 2
        p1, p2 = li
        # Last item in 1st para is our 1st raw node
        _expect_type(p1[4], raw)
        assert p1[4].astext() == "x"
        # Only item in 2nd para is our 2nd raw node
        _expect_type(p2[0], raw)
        assert p2[0].astext() == "y"

    def descriptions_are_parsed_for_issue_roles(self):
        item = list_item("", paragraph("", "", self.b.deepcopy(), s(5)))
        para = self._generate("1.0.2", item)[0]
        # Sanity check - in a broken parsing scenarion, the 4th child will be a
        # raw issue object
        assert not isinstance(para[4], Issue)
        # First/primary link
        _expect_type(para[2], reference)
        assert para[2].astext() == "#15"
        assert "Bug" in para[0].astext()
        # Second/inline link
        _expect_type(para[6], reference)
        assert para[6].astext() == "#5"
        assert "Support" in para[4].astext()

    class unreleased:
        def buckets_omit_major_version_when_only_one_exists(self):
            result = self._generate(b(1), raw=True)[0][0][0]
            html = str(result)  # since repr() from test-fail hides actual text
            assert "Next bugfix release" in html

        def buckets_display_major_version_when_multiple(self):
            entries = (
                b(3),  # should appear in unreleased bugs for 2.x
                "2.0.0",
                b(2),  # should appear in unreleased bugs for 1.x
                "1.0.1",
                b(1),
            )
            # Expectation: [2.x unreleased, 1.x unreleased, 1.0.1]
            two_x, one_x, _ = self._generate(*entries, raw=True)
            html = str(two_x[0][0])
            assert "Next 2.x bugfix release" in html
            html = str(one_x[0][0])
            assert "Next 1.x bugfix release" in html

        def displays_version_when_only_some_lines_displayed(self):
            # I.e. if there's unreleased 1.x stuff but no unreleased 2.x, still
            # display the "1.x".
            entries = (
                # Note lack of any bugfixes post 2.0.0
                "2.0.0",
                b(2),
                "1.0.1",
                b(1),
            )
            # Expectation: [1.x unreleased, 1.0.1] - no 2.x.
            result = self._generate(*entries, raw=True)
            assert len(result) == 2
            html = str(result[0][0][0])
            assert "Next 1.x bugfix release" in html

        def feature_section_links_go_to_development_branch(self):
            self._test_link(
                kwargs=dict(release_uri="releasey_%s"),
                type_="release",
                expected="releasey_master",
                entries=[(f, 15), (release, "1.0.0")],
            )

        def feature_section_links_can_configure_branch_name(self):
            self._test_link(
                kwargs=dict(
                    development_branch="main", release_uri="releasey_%s"
                ),
                type_="release",
                expected="releasey_main",
                entries=[(f, 15), (release, "1.0.0")],
            )

    def unstable_prehistory_active_means_only_one_unreleased_release(self):
        app = make_app(unstable_prehistory=True)
        entries = (b(2), f(3), "0.1.0", b(1))
        result = self._generate(*entries, app=app, raw=True, skip_initial=True)
        html = str(result[0][0][0])
        assert "Next release" in html
