"""Static HTML -> Nuxt pages, deterministically (static_to_nuxt.py).

The module's promise is fidelity: the page's markup is never re-serialized (it is sliced out of the original), the
head becomes useHead, and the scripts keep their order. These tests pin that, the hazards a hand-written prototype
carries (which is why the markup is rendered as a string and not compiled as a Vue template), and the site-level rules.
The real 10-page RFG prototype was verified separately against a production Nuxt build in a browser."""

from __future__ import annotations

import json
import re

import pytest
from src.domain.codegen.static_to_nuxt import (
    LEGACY_SCRIPTS_COMPOSABLE_PATH,
    convert_page,
    convert_static_site,
    parse_attrs,
    render_redirect_page,
    render_vue_page,
    tokenize,
)

# Shaped like the real prototype: HTML5 optional tags (no <head>/<body>), head-ish elements first, the body
# starting at the first other element, scripts at the end.
PROTOTYPE = """<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Royal &amp; Funding</title>
<meta name="description" content="Funding &quot;fast&quot;">
<link rel="icon" href="assets/img/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- fonts -->
<link rel="stylesheet" href="assets/css/styles.css">
<script>document.documentElement.setAttribute('data-v4','');</script>
<a class="sr-only" href="#main">Skip</a>
<header class="site-header" id="siteHeader"><a href="index.html">Home</a><a href="about.html">About</a></header>
<main id="main"><img src="assets/img/x.jpg" alt="" onerror="this.remove()"></main>

<script src="assets/js/data.js"></script>
<!-- one clock -->
<script src="assets/js/app.js" defer></script>
<script>window.boot = "</scr" + "ipt>";</script>
"""


# ---- tokenizer ---------------------------------------------------------------------------------------


def test_attributes_are_parsed_and_unescaped():
    assert parse_attrs(' href="a&amp;b" data-x=\'q\' hidden crossorigin=anonymous id=unquoted') == [
        ("href", "a&b"), ("data-x", "q"), ("hidden", None), ("crossorigin", "anonymous"), ("id", "unquoted")]


def test_script_content_is_raw_text_so_a_less_than_inside_it_does_not_start_a_tag():
    tokens = tokenize('<script>if (a<b && c>d) { x = "<div>" }</script><p>after</p>')
    script = tokens[0]
    assert script.name == "script" and script.inner == 'if (a<b && c>d) { x = "<div>" }'
    assert [t.name for t in tokens if t.kind == "start"] == ["script", "p"]


def test_comments_and_a_stray_less_than_are_handled():
    kinds = [t.kind for t in tokenize("a < b <!-- c --> <p>x</p>")]
    assert "comment" in kinds and kinds.count("start") == 1


# ---- page split -------------------------------------------------------------------------------------


def test_a_page_with_no_head_or_body_tags_is_split_where_a_browser_would_split_it():
    page = convert_page(PROTOTYPE, "index")
    assert page.html_attrs == {"lang": "en"}
    assert page.head["title"] == "Royal & Funding"  # entities decoded for the head manager
    assert {"charset": "utf-8"} in page.head["meta"]
    assert {"name": "description", "content": 'Funding "fast"'} in page.head["meta"]
    assert page.body_html.startswith('<a class="sr-only" href="#main">Skip</a>')
    assert "<meta" not in page.body_html and "<link" not in page.body_html and "<title" not in page.body_html


def test_the_body_markup_is_the_original_with_only_the_script_elements_removed():
    page = convert_page(PROTOTYPE, "index")
    original_body = PROTOTYPE[PROTOTYPE.index('<a class="sr-only"'):]
    without_scripts = re.sub(r"<script\b[^>]*>.*?</script>", "", original_body, flags=re.S)
    assert page.body_html == without_scripts.strip("\r\n")  # nothing else was rewritten or normalized
    assert '<img src="assets/img/x.jpg" alt="" onerror="this.remove()">' in page.body_html
    assert 'href="index.html"' in page.body_html  # links are not rewritten


def test_scripts_are_hoisted_out_of_the_markup_in_order_with_their_attributes():
    page = convert_page(PROTOTYPE, "index")
    assert "<script" not in page.body_html
    assert [s.get("src") or "inline" for s in page.body_scripts] == [
        "assets/js/data.js", "assets/js/app.js", "inline"]
    assert page.body_scripts[1]["defer"] is True and page.body_scripts[1]["tagPosition"] == "bodyClose"
    assert page.body_scripts[2]["innerHTML"] == 'window.boot = "</scr" + "ipt>";'
    # the script in head position stays in the head, not among the body scripts
    assert page.head["script"] == [{"innerHTML": "document.documentElement.setAttribute('data-v4','');",
                                    "tagPosition": "head"}]


def test_a_script_in_the_middle_of_the_body_is_hoisted_and_the_markup_around_it_stays_intact():
    page = convert_page("<title>t</title><p>one</p><script src=a.js></script><p>two</p>", "x")
    assert page.body_html == "<p>one</p><p>two</p>"
    assert [s["src"] for s in page.body_scripts] == ["a.js"]


def test_a_style_element_in_the_body_is_hoisted_to_the_head():
    page = convert_page("<title>t</title><p>x</p><style>p{color:red}</style>", "x")
    assert page.body_html == "<p>x</p>" and page.head["style"] == [{"innerHTML": "p{color:red}"}]


def test_explicit_head_and_body_tags_and_their_attributes_are_honoured():
    page = convert_page(
        '<!doctype html><html lang="fr" class="a"><head><title>T</title><meta charset="utf-8"></head>'
        '<body class="home" data-x="1"><h1>Hi</h1><script src="s.js"></script></body></html>', "x")
    assert page.html_attrs == {"lang": "fr", "class": "a"}
    assert page.body_attrs == {"class": "home", "data-x": "1"}
    assert page.body_html == "<h1>Hi</h1>" and [s["src"] for s in page.body_scripts] == ["s.js"]


def test_a_page_with_no_body_content_says_so():
    page = convert_page("<title>t</title>", "x")
    assert page.body_html == "" and any("No body content" in w for w in page.warnings)


# ---- the hazards that make a Vue template the wrong container -------------------------------------------

HAZARDS = (
    '<p>{{ not_interpolation }}</p>'
    '<button :class="x" @click="go" v-if="y" #slot>a</button>'
    '<img src="assets/img/missing-not-yet-uploaded.jpg" onerror="this.remove()">'
    '<table><tr><td>no tbody</td></tr></table>'
    '<p>unclosed <div>nested wrongly</div>'
)


def test_hazardous_markup_is_carried_through_untouched():
    page = convert_page("<title>t</title>" + HAZARDS, "x")
    assert page.body_html == HAZARDS
    rendered = render_vue_page(page)
    assert "v-html=\"html\"" in rendered  # a string is parsed by the browser exactly like the original page
    assert "{{ not_interpolation }}" not in rendered  # the markup lives in legacy/, never inside a Vue template


# ---- rendering ------------------------------------------------------------------------------------------


def _use_head(vue: str) -> dict:
    match = re.search(r"useHead\((\{.*?\n\})\)", vue, re.S)
    return json.loads(match.group(1).replace("<\\/", "</")) if match else {}


def test_head_data_is_emitted_as_valid_json_a_head_manager_can_take():
    page = convert_page(PROTOTYPE, "about")
    head = _use_head(render_vue_page(page))
    assert head["title"] == "Royal & Funding" and head["htmlAttrs"] == {"lang": "en"}
    assert {"rel": "preconnect", "href": "https://fonts.gstatic.com", "crossorigin": ""} in head["link"]
    assert head["link"][-1] == {"rel": "stylesheet", "href": "assets/css/styles.css"}
    assert head["script"][0]["tagPosition"] == "head"  # only head scripts here; body scripts are not useHead


def test_body_scripts_load_after_mount_through_the_shared_composable():
    vue = render_vue_page(convert_page(PROTOTYPE, "about"))
    assert "useLegacyScripts([" in vue
    assert vue.index("useHead(") < vue.index("useLegacyScripts(")
    scripts = json.loads(re.search(r"useLegacyScripts\((\[.*?\n\])\)", vue, re.S).group(1).replace("<\\/", "</"))
    assert [s.get("src") or "inline" for s in scripts] == ["assets/js/data.js", "assets/js/app.js", "inline"]


def test_a_closing_script_tag_inside_a_script_can_never_end_the_sfc_script_block():
    vue = render_vue_page(convert_page(PROTOTYPE, "about"))
    inside = vue[vue.index("<script setup"):vue.rindex("</script>")]
    assert "</" not in inside  # no raw closing tag anywhere in the block, so it ends only at the real one
    assert r"<\/scr" in inside  # the script's own "</scr" is present, escaped as <\/ (it round-trips: see above)


def test_a_page_without_body_scripts_does_not_call_the_loader():
    assert "useLegacyScripts" not in render_vue_page(convert_page("<title>t</title><p>x</p>", "x"))


def test_layout_is_off_and_only_the_home_page_gets_the_index_html_alias():
    assert "definePageMeta({ layout: false })" in render_vue_page(convert_page("<title>t</title><p>x</p>", "about"))
    assert 'definePageMeta({ layout: false, alias: ["/index.html"] })' in render_vue_page(
        convert_page("<title>t</title><p>x</p>", "index"))


def test_the_markup_is_imported_raw_from_legacy_pages():
    vue = render_vue_page(convert_page("<title>t</title><p>x</p>", "about"))
    assert "import html from '~~/legacy/pages/about.html?raw'" in vue


def test_the_clean_url_redirects_to_the_html_url_the_scripts_expect():
    vue = render_redirect_page("about")
    assert "redirect: '/about.html'" in vue and "layout: false" in vue


# ---- site level -------------------------------------------------------------------------------------------


def site(**extra):
    files = {
        "index.html": PROTOTYPE, "about.html": PROTOTYPE, "thank-you.html": "<title>t</title><p>x</p>",
        "assets/css/styles.css": "a{}", "assets/js/data.js": "var D=1", "assets/js/app.js": "var A=1",
        "assets/img/notes.md": "not text asset", "README.md": "# readme", "src/pages/index.html": "<p>source</p>",
    }
    files.update(extra)
    return convert_static_site(files)


def test_each_page_becomes_a_page_file_a_markup_file_and_a_redirect_except_the_home_page():
    out = site()
    assert {"pages/index.vue", "pages/about.html.vue", "pages/about.vue", "pages/thank-you.html.vue",
            "pages/thank-you.vue", "legacy/pages/index.html", "legacy/pages/about.html",
            "legacy/pages/thank-you.html"} <= set(out.files)
    assert "pages/index.html.vue" not in out.files  # the home page lives at / (and /index.html)
    assert [(p["route"], p["redirect_from"]) for p in out.pages] == [
        ("/about.html", "/about"), ("/", None), ("/thank-you.html", "/thank-you")]


def test_text_assets_move_to_public_and_everything_else_is_left_alone():
    files = site().files
    assert files["public/assets/css/styles.css"] == "a{}" and files["public/assets/js/data.js"] == "var D=1"
    assert not any("README" in f or "notes.md" in f or f.startswith("public/src") for f in files)


def test_the_loader_composable_is_emitted_once_and_only_when_a_page_needs_it():
    assert LEGACY_SCRIPTS_COMPOSABLE_PATH in site().files
    plain = convert_static_site({"index.html": "<title>t</title><p>no scripts</p>"})
    assert LEGACY_SCRIPTS_COMPOSABLE_PATH not in plain.files


def test_nested_unsafe_and_already_taken_pages_are_reported_not_converted():
    out = convert_static_site(
        {"blog/post.html": "<p>x</p>", "About Us.html": "<p>x</p>", "Team.html": "<p>x</p>",
         "admin.html": "<p>x</p>", "faq.html": "<p>x</p>"},
        existing_routes={"/admin"},
    )
    reasons = {s["path"]: s["reason"] for s in out.skipped}
    assert "nested" in reasons["blog/post.html"]
    assert "safe route name" in reasons["About Us.html"] and "safe route name" in reasons["Team.html"]
    assert "/admin route" in reasons["admin.html"]
    assert [p["source"] for p in out.pages] == ["faq.html"]


def test_an_existing_html_route_also_blocks_the_page():
    out = convert_static_site({"about.html": "<p>x</p>"}, existing_routes={"/about.html"})
    assert out.pages == [] and "about" in out.skipped[0]["reason"]


def test_a_site_without_pages_converts_nothing():
    out = convert_static_site({"assets/css/a.css": "a{}"})
    assert not out.converted and out.pages == []


def test_conversion_is_deterministic():
    assert site().files == site().files


@pytest.mark.parametrize("name", ["index", "about", "thank-you", "funding-solutions"])
def test_every_generated_page_is_a_complete_sfc(name):
    vue = render_vue_page(convert_page(PROTOTYPE, name))
    assert vue.count("<script setup") == 1 and vue.count("</script>") == 1
    assert vue.count("<template>") == 1 and vue.rstrip().endswith("</template>")


def test_whitespace_at_the_edges_of_the_markup_is_kept_only_line_breaks_are_trimmed():
    # trailing spaces can matter (inline content, pre-formatted text): only the line breaks around the body go
    page = convert_page("<title>t</title>\n  <p>x</p>  \n", "x")
    assert page.body_html == "<p>x</p>  "
