import json
import re
import subprocess
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urljoin


FRONTEND = Path(__file__).parent.parent / "frontend"
RENDERER = FRONTEND / "markdown.js"


def render_markdown(payload, options=None):
    options = options or {}
    script = (
        f"const render = require({json.dumps(str(RENDERER))});"
        f"process.stdout.write(render({json.dumps(payload)}, {json.dumps(options)}));"
    )
    return subprocess.run(
        ["node", "-e", script],
        check=True,
        capture_output=True,
        text=True,
    ).stdout


def toggle_task(payload, task_index, checked):
    script = (
        f"const render = require({json.dumps(str(RENDERER))});"
        f"process.stdout.write(JSON.stringify(render.toggleTask({json.dumps(payload)}, "
        f"{task_index}, {json.dumps(checked)})));"
    )
    return json.loads(
        subprocess.run(
            ["node", "-e", script],
            check=True,
            capture_output=True,
            text=True,
        ).stdout
    )


def manage_task(payload, task_index, operation):
    script = (
        f"const manage = require({json.dumps(str(FRONTEND / 'issue-sheet.js'))}).manageChecklistTask;"
        f"process.stdout.write(JSON.stringify(manage({json.dumps(payload)}, "
        f"{task_index}, {json.dumps(operation)})));"
    )
    return json.loads(
        subprocess.run(
            ["node", "-e", script],
            check=True,
            capture_output=True,
            text=True,
        ).stdout
    )


class ScriptSourceParser(HTMLParser):
    def __init__(self):
        super().__init__()
        self.sources = []

    def handle_starttag(self, tag, attrs):
        if tag == "script":
            source = dict(attrs).get("src")
            if source:
                self.sources.append(source)


def test_markdown_renderer_escapes_raw_html_before_rendering_heading():
    payload = "# <img src=x onerror=alert(document.domain)>"
    script = (
        f"const render = require({json.dumps(str(RENDERER))});"
        f"process.stdout.write(render({json.dumps(payload)}));"
    )

    result = subprocess.run(
        ["node", "-e", script],
        check=True,
        capture_output=True,
        text=True,
    )

    assert result.stdout == (
        "<h1>&lt;img src=x onerror=alert(document.domain)&gt;</h1>"
    )


def test_markdown_script_resolves_inside_dashboard_subpath():
    parser = ScriptSourceParser()
    parser.feed((FRONTEND / "index.html").read_text())
    markdown_source = next(
        source for source in parser.sources if source.endswith("markdown.js")
    )

    assert urljoin(
        "https://forge.alexanderwhitestone.com/dashboard/", markdown_source
    ) == "https://forge.alexanderwhitestone.com/dashboard/static/markdown.js"


def test_markdown_renderer_preserves_mobile_reading_structure():
    rendered = render_markdown(
        "## Plan\n\n> Ship this\n\n- [x] Tested\n- [ ] Released\n\n"
        "```python\nprint('ready')\n```"
    )

    assert rendered == (
        "<h2>Plan</h2><blockquote><p>Ship this</p></blockquote>"
        '<ul class="task-list"><li class="task-list-item">'
        '<input type="checkbox" disabled checked> Tested</li>'
        '<li class="task-list-item"><input type="checkbox" disabled> Released</li></ul>'
        '<pre><code class="language-python">print(&#39;ready&#39;)</code></pre>'
    )


def test_interactive_markdown_tasks_expose_accessible_source_indices():
    rendered = render_markdown(
        "- [ ] Verify production\n- [x] Notify support",
        {"interactiveTasks": True},
    )

    assert rendered == (
        '<ul class="task-list"><li class="task-list-item">'
        '<input type="checkbox" class="task-list-toggle" data-task-index="0" '
        'aria-label="Mark Verify production complete"> Verify production</li>'
        '<li class="task-list-item"><input type="checkbox" class="task-list-toggle" '
        'data-task-index="1" aria-label="Mark Notify support incomplete" checked> '
        'Notify support</li></ul>'
    )


def test_manageable_markdown_tasks_expose_touch_action_with_position_boundaries():
    rendered = render_markdown(
        "- [ ] Verify production\n- [x] Notify support",
        {"interactiveTasks": True, "manageTasks": True},
    )

    assert 'class="task-list-manage" data-task-index="0"' in rendered
    assert 'aria-label="Manage step: Verify production"' in rendered
    assert 'data-task-first="true"' in rendered
    assert 'class="task-list-manage" data-task-index="1"' in rendered
    assert 'data-task-last="true"' in rendered


def test_interactive_task_toggle_changes_exact_source_marker_only():
    body = (
        "```md\r\n- [ ] example only\r\n```\r\n"
        "- [ ] Duplicate\r\n  - [X] Nested duplicate\r\n- [ ] Duplicate\r\n"
    )

    assert toggle_task(body, 1, True) == (
        "```md\r\n- [ ] example only\r\n```\r\n"
        "- [ ] Duplicate\r\n  - [X] Nested duplicate\r\n- [x] Duplicate\r\n"
    )


def test_manage_task_renames_exact_visible_task_and_preserves_nested_step_line_endings_and_fences():
    body = (
        "```md\r\n- [ ] example only\r\n```\r\n"
        "- [ ] Build\r\n  - [X] Verify rollback\r\n- [ ] Release\r\n"
    )

    assert manage_task(body, 1, {"action": "rename", "label": "Ship"}) == (
        "```md\r\n- [ ] example only\r\n```\r\n"
        "- [ ] Build\r\n  - [X] Verify rollback\r\n- [ ] Ship\r\n"
    )


def test_manage_task_removes_only_the_selected_task_line():
    body = "Plan\n- [ ] Build\n- [x] Test\n\nNotes"

    assert manage_task(body, 0, {"action": "remove"}) == "Plan\n- [x] Test\n\nNotes"


def test_manage_task_moves_a_step_within_its_list_without_changing_step_content():
    body = "Plan\n- [ ] Build\n- [x] Test\n- [ ] Release\n\nNotes"

    assert manage_task(body, 2, {"action": "move-earlier"}) == (
        "Plan\n- [ ] Build\n- [ ] Release\n- [x] Test\n\nNotes"
    )
    assert manage_task(body, 0, {"action": "move-later"}) == (
        "Plan\n- [x] Test\n- [ ] Build\n- [ ] Release\n\nNotes"
    )


def test_manage_task_rejects_an_empty_or_duplicate_renamed_label():
    script = f"""
const manage = require({json.dumps(str(FRONTEND / 'issue-sheet.js'))}).manageChecklistTask;
const body = '- [ ] Build\\n- [x] Test';
const errors = ['', '  test  '].map(label => {{
  try {{ manage(body, 0, {{action:'rename',label}}); return null; }}
  catch (error) {{ return error.message; }}
}});
process.stdout.write(JSON.stringify(errors));
"""

    assert json.loads(subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout) == ["Enter a checklist step.", "That checklist step already exists."]


def test_markdown_renderer_allows_only_safe_links_and_keeps_html_inert():
    rendered = render_markdown(
        "[Forge](https://forge.example/work?q=1&safe=yes) "
        "[relative](/git/org/repo/issues/1) "
        "[attack](javascript:alert(1)) <img src=x onerror=alert(2)>"
    )

    assert (
        '<a href="https://forge.example/work?q=1&amp;safe=yes" target="_blank" '
        'rel="noopener noreferrer">Forge</a>' in rendered
    )
    assert (
        '<a href="/git/org/repo/issues/1" target="_blank" '
        'rel="noopener noreferrer">relative</a>' in rendered
    )
    assert "javascript:" not in rendered
    assert "<img" not in rendered
    assert "&lt;img src=x onerror=alert(2)&gt;" in rendered


def test_all_read_only_work_bodies_use_the_shared_markdown_renderer():
    dashboard = (FRONTEND / "dashboard.js").read_text()

    expected_paths = (
        "renderMarkdown(comment.body || 'No comment body provided.')",
        "renderMarkdown(detail.subject_body || 'No subject context was provided.')",
        "renderMarkdown(detail.body || 'No description provided.')",
        "renderMarkdown(item.body || 'No description provided.')",
        "renderMarkdown(review.body)",
        "issueController.renderTasks(qs('#issue-sheet-body'), detail,",
    )
    for path in expected_paths:
        assert path in dashboard

    assert dashboard.count("renderMarkdown(detail.body || 'No description provided.')") == 3


def test_markdown_work_bodies_are_mobile_safe_block_containers():
    html = (FRONTEND / "index.html").read_text()
    css = (FRONTEND / "dashboard.css").read_text()
    worker = (FRONTEND / "service-worker.js").read_text()

    for body_id in (
        "issue-sheet-body",
        "pull-sheet-body",
        "review-sheet-body",
        "update-subject-body",
        "search-preview-body",
    ):
        body_tag = re.search(rf'<div\b[^>]*\bid="{body_id}"[^>]*>', html)
        assert body_tag
        assert "markdown-content" in body_tag.group(0)

    assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
    assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
    assert ".markdown-content a { min-height:44px;" in css
    assert "stackchain-dashboard-shell-v127" in worker
