"""Unit tests for `toc_composer` (#237) — the post-reframe TOC body
layout helper.

Pure stdlib tests covering layout enforcement. Data assembly logic
stays in the calling session; this module is only concerned with
canonical concatenation of caller-supplied per-section markdown.
"""
from __future__ import annotations

import pytest

from active_claude_github_mcp.toc_composer import (
    DEFAULT_POINTER_LINE,
    compose_discussions_row,
    compose_discussions_table,
    compose_milestone_narrative,
    compose_post_reframe_toc,
)


class TestComposePostReframeToc:
    def test_minimal_inputs_produces_canonical_skeleton(self):
        body = compose_post_reframe_toc(project_name="example")
        assert body.startswith("# Project Index — example\n")
        # All seven canonical section headers present even with empty content.
        for header in (
            "## Releases",
            "## Milestones",
            "## Discussions",
            "## Cross-Repo Activity",
            "## Project Board",
            "## Repos",
        ):
            assert header in body
        # Default pointer line rendered with placeholder URL.
        assert "[project board URL]" in body
        # Trailing newline is exactly one.
        assert body.endswith("\n")
        assert not body.endswith("\n\n")

    def test_section_ordering_is_canonical(self):
        body = compose_post_reframe_toc(
            project_name="x",
            releases="rel",
            milestones="mil",
            discussions="disc",
            cross_repo_activity="cra",
            project_board_url="https://board.example/1",
            repos="rep",
        )
        # Sections appear in order: Releases → Milestones → Discussions
        # → pointer line → Cross-Repo Activity → Project Board → Repos.
        positions = {
            "## Releases": body.index("## Releases"),
            "## Milestones": body.index("## Milestones"),
            "## Discussions": body.index("## Discussions"),
            "pointer": body.index("Open Issues — see"),
            "## Cross-Repo Activity": body.index("## Cross-Repo Activity"),
            "## Project Board": body.index("## Project Board"),
            "## Repos": body.index("## Repos"),
        }
        ordered = sorted(positions.items(), key=lambda kv: kv[1])
        assert [k for k, _ in ordered] == [
            "## Releases",
            "## Milestones",
            "## Discussions",
            "pointer",
            "## Cross-Repo Activity",
            "## Project Board",
            "## Repos",
        ]

    def test_preamble_inserted_between_title_and_releases(self):
        body = compose_post_reframe_toc(
            project_name="x",
            preamble="## Design Record\n\nFoo bar.",
        )
        title_idx = body.index("# Project Index — x")
        preamble_idx = body.index("## Design Record")
        releases_idx = body.index("## Releases")
        assert title_idx < preamble_idx < releases_idx

    def test_pointer_line_default_substitutes_board_url(self):
        body = compose_post_reframe_toc(
            project_name="x",
            project_board_url="https://github.com/orgs/x/projects/1",
        )
        assert "https://github.com/orgs/x/projects/1" in body
        # The default pointer line text appears verbatim except for
        # the {board_url} substitution.
        assert "[project board](https://github.com/orgs/x/projects/1)" in body

    def test_custom_pointer_line_used_verbatim(self):
        body = compose_post_reframe_toc(
            project_name="x",
            project_board_url="https://board",
            pointer_line="Custom pointer text.",
        )
        assert "Custom pointer text." in body
        # Default pointer text NOT present (custom replaces, doesn't append).
        assert "Open Issues — see" not in body

    def test_pointer_line_placement_between_discussions_and_cra(self):
        """Per #235's 'What the live TOC keeps' Issues bullet, the
        pointer line sits between ## Discussions and ## Cross-Repo
        Activity (replacing the dropped ## Issues table)."""
        body = compose_post_reframe_toc(
            project_name="x",
            project_board_url="https://board",
        )
        disc_idx = body.index("## Discussions")
        pointer_idx = body.index("Open Issues — see")
        cra_idx = body.index("## Cross-Repo Activity")
        assert disc_idx < pointer_idx < cra_idx

    def test_empty_project_name_rejected(self):
        with pytest.raises(ValueError, match="project_name"):
            compose_post_reframe_toc(project_name="")
        with pytest.raises(ValueError, match="project_name"):
            compose_post_reframe_toc(project_name="   ")

    def test_blank_section_runs_collapsed_to_single_blank(self):
        """Empty section bodies don't produce \\n\\n\\n sequences in output."""
        body = compose_post_reframe_toc(project_name="x")
        assert "\n\n\n" not in body

    def test_trailing_whitespace_trimmed_from_each_section(self):
        body = compose_post_reframe_toc(
            project_name="x",
            releases="content\n\n\n",
            milestones="more\n",
        )
        # rstrip in helper means triple-blank doesn't bleed into next
        # section. Section header for Milestones still emitted right
        # after Releases content.
        assert "\n\n\n" not in body

    def test_round_trip_through_str_format_no_ambiguity(self):
        """A composed body should be re-format-safe — no stray `{` or
        `}` from the default pointer line that would confuse later
        str.format on the body."""
        body = compose_post_reframe_toc(
            project_name="x",
            project_board_url="https://board",
        )
        # No unescaped placeholders should remain.
        assert "{board_url}" not in body
        assert "{project_name}" not in body


class TestDefaultPointerLine:
    def test_format_string_has_board_url_placeholder(self):
        # Sanity check: `{board_url}` is the only template token.
        assert "{board_url}" in DEFAULT_POINTER_LINE

    def test_default_pointer_includes_canonical_phrasing_per_235(self):
        # This is the spec text from #235 §"What the live TOC keeps"
        # — drift from this means either the spec changed (update
        # both) or the composer drifted (fix the composer).
        assert "Open Issues" in DEFAULT_POINTER_LINE
        assert "[project board]" in DEFAULT_POINTER_LINE
        assert "Closed Issues" in DEFAULT_POINTER_LINE
        assert "milestone page" in DEFAULT_POINTER_LINE
        assert "planning Discussion" in DEFAULT_POINTER_LINE


class TestComposeMilestoneNarrative:
    def test_format_matches_spec(self):
        line = compose_milestone_narrative(
            version="2.3.0",
            url="https://github.com/o/r/milestone/11",
            status="planned 2026-04-28",
            summary="9 firm Issues across three themes.",
        )
        expected = (
            "- **[2.3.0](https://github.com/o/r/milestone/11)** "
            "(planned 2026-04-28) — 9 firm Issues across three themes."
        )
        assert line == expected

    def test_summary_whitespace_stripped(self):
        line = compose_milestone_narrative(
            version="x", url="https://x", status="s",
            summary="  trim me  ",
        )
        assert line.endswith("trim me")
        assert "  trim me" not in line

    def test_empty_version_rejected(self):
        with pytest.raises(ValueError, match="version"):
            compose_milestone_narrative(
                version="", url="https://x", status="s", summary="t",
            )

    def test_empty_url_rejected(self):
        with pytest.raises(ValueError, match="url"):
            compose_milestone_narrative(
                version="x", url="", status="s", summary="t",
            )


class TestComposeDiscussionsRow:
    def test_format_matches_table_shape(self):
        row = compose_discussions_row(
            title="My topic",
            url="https://github.com/o/r/discussions/42",
            category="Architecture",
            status="Active",
            linked_issues="#1, #2",
        )
        assert row == (
            "| [My topic](https://github.com/o/r/discussions/42) | "
            "Architecture | Active | #1, #2 |"
        )

    def test_default_linked_issues_is_em_dash(self):
        row = compose_discussions_row(
            title="X", url="https://x",
            category="Ideas", status="Active",
        )
        assert row.endswith("— |")

    def test_empty_title_rejected(self):
        with pytest.raises(ValueError, match="title"):
            compose_discussions_row(
                title="", url="https://x",
                category="X", status="Active",
            )

    def test_empty_url_rejected(self):
        with pytest.raises(ValueError, match="url"):
            compose_discussions_row(
                title="x", url="",
                category="X", status="Active",
            )


class TestComposeDiscussionsTable:
    def test_empty_rows_yields_header_only(self):
        out = compose_discussions_table([])
        assert "| Title |" in out
        assert "|-------|" in out
        # No data rows present.
        assert out.count("\n") == 1

    def test_rows_assembled_with_canonical_header(self):
        rows = [
            compose_discussions_row(
                title="A", url="https://a",
                category="Ideas", status="Active",
            ),
            compose_discussions_row(
                title="B", url="https://b",
                category="Architecture", status="Decided ✓",
            ),
        ]
        out = compose_discussions_table(rows)
        lines = out.split("\n")
        assert lines[0].startswith("| Title")
        assert lines[1].startswith("|-------")
        assert lines[2].startswith("| [A]")
        assert lines[3].startswith("| [B]")


if __name__ == "__main__":
    import sys
    sys.exit(pytest.main([__file__, "-v"]))
