from __future__ import annotations

import pytest
from pygitweb.static_tests.js_runtime import JsError, JsPage

_FORM = (
    '<input id="search-patterns">'
    '<input id="search-paths">'
    '<input id="search-globs">'
    '<select id="search-sort"><option value=""></option><option value="path">path</option></select>'
    '<input id="search-max-count">'
    '<input id="search-heading" type="checkbox">'
    '<input id="search-multiline" type="checkbox">'
    '<div id="search-status"></div>'
    '<div id="search-results"></div>'
    '<details id="search-advanced"></details>'
)


def _ready(js: JsPage, href: str = "http://127.0.0.1/search") -> None:
    js.run("window.SEARCH_API_URL = '/api/search';")
    js.set_location(href)
    js.set_html(_FORM)


def test_load_throws_when_a_control_is_missing(js: JsPage) -> None:
    js.run("window.SEARCH_API_URL = '/api/search';")
    js.set_html("<div></div>")
    with pytest.raises(JsError):
        js.load("search-page.js")


def test_read_url_fills_the_form_and_opens_advanced_filters(js: JsPage) -> None:
    js.route("/api/search", body={"results": []})
    _ready(
        js,
        "http://127.0.0.1/search?patterns=foo&paths=src&globs=*.py&sort=path&max_count=5&heading=true&multiline=true",
    )
    js.load("search-page.js")
    assert js.prop("#search-patterns", "value") == "foo"
    assert js.prop("#search-paths", "value") == "src"
    assert js.prop("#search-globs", "value") == "*.py"
    assert js.prop("#search-sort", "value") == "path"
    assert js.prop("#search-max-count", "value") == "5"
    assert js.prop("#search-heading", "checked") is True
    assert js.prop("#search-multiline", "checked") is True
    assert js.prop("#search-advanced", "open") is True


def test_empty_pattern_clears_results_and_the_query_string(js: JsPage) -> None:
    _ready(js, "http://127.0.0.1/search?patterns=old")
    js.load("search-page.js")
    js.set_prop("#search-patterns", "value", "  ")
    js.dispatch("#search-patterns", "keydown", key="Enter")
    assert js.text("#search-status") == ""
    assert js.prop("#search-results", "innerHTML") == ""
    assert "patterns" not in js.value("location.search")


def test_results_are_escaped_and_pluralized(js: JsPage) -> None:
    js.route("/api/search", body={"results": ["<script>", "ok"]})
    _ready(js, "http://127.0.0.1/search?patterns=foo")
    js.load("search-page.js")
    assert js.text("#search-status") == "2 matches"
    assert "&lt;script&gt;" in js.prop("#search-results", "innerHTML")
    assert "<script>" not in js.prop("#search-results", "innerHTML")
    assert "is-loading" not in js.classes("#search-results")


def test_a_single_match_uses_the_singular_label(js: JsPage) -> None:
    js.route("/api/search", body={"results": ["one"]})
    _ready(js, "http://127.0.0.1/search?patterns=foo&heading=false")
    js.set_html(_FORM)
    js.load("search-page.js")
    assert js.text("#search-status") == "1 match"
    url = next(str(item["url"]) for item in js.requests() if "/api/search" in str(item["url"]))
    assert "heading=false" in url
    assert "patterns=foo" in url


def test_missing_results_count_as_zero(js: JsPage) -> None:
    js.route("/api/search", body={})
    _ready(js, "http://127.0.0.1/search?patterns=foo")
    js.load("search-page.js")
    assert js.text("#search-status") == "0 matches"
    assert js.prop("#search-results", "innerHTML") == ""


def test_error_detail_is_shown(js: JsPage) -> None:
    js.route("/api/search", status=400, body={"detail": "bad pattern"}, status_text="Bad")
    _ready(js, "http://127.0.0.1/search?patterns=foo")
    js.load("search-page.js")
    assert js.text("#search-status") == "Error 400: bad pattern"


def test_error_without_json_uses_the_status_text(js: JsPage) -> None:
    js.route("/api/search", status=502, json_error=True, status_text="Bad Gateway")
    _ready(js, "http://127.0.0.1/search?patterns=foo")
    js.load("search-page.js")
    assert js.text("#search-status") == "Error 502: Bad Gateway"


def test_network_error_is_shown(js: JsPage) -> None:
    js.route("/api/search", error="offline")
    _ready(js, "http://127.0.0.1/search?patterns=foo")
    js.load("search-page.js")
    assert js.text("#search-status") == "Error: offline"
    assert "is-loading" not in js.classes("#search-results")


def test_a_newer_search_aborts_the_previous_one(js: JsPage) -> None:
    js.route("/api/search", hang=True)
    _ready(js, "http://127.0.0.1/search?patterns=foo")
    js.load("search-page.js")
    assert js.text("#search-status") == "Searching…"
    js.set_prop("#search-patterns", "value", "bar")
    js.dispatch("#search-patterns", "keydown", key="Enter")
    assert js.text("#search-status") == "Searching…"
    js.fulfill("/api/search", body={"results": ["bar"]})
    assert js.text("#search-status") == "1 match"
    assert "Error" not in js.text("#search-status")


def test_input_debounces_and_other_controls_search_too(js: JsPage) -> None:
    js.route("/api/search", body={"results": ["x"]})
    _ready(js)
    js.load("search-page.js")
    js.set_prop("#search-patterns", "value", "foo")
    js.set_prop("#search-paths", "value", "src")
    js.set_prop("#search-globs", "value", "*.py")
    js.set_prop("#search-max-count", "value", "")
    js.set_prop("#search-sort", "value", "path")
    js.set_prop("#search-heading", "checked", True)
    js.set_prop("#search-multiline", "checked", True)
    js.dispatch("#search-patterns", "input")
    assert js.text("#search-status") == ""
    js.tick()
    url = next(str(item["url"]) for item in js.requests() if "patterns=foo" in str(item["url"]))
    assert "paths=src" in url
    assert "globs=*.py" in url
    assert "sort=path" in url
    assert "max_count" not in url
    assert "heading=true" in url
    assert "multiline=true" in url
    assert js.value("location.search").find("patterns=foo") >= 0


def test_other_keys_do_not_run_a_search_immediately(js: JsPage) -> None:
    _ready(js)
    js.load("search-page.js")
    js.set_prop("#search-patterns", "value", "foo")
    js.dispatch("#search-patterns", "keydown", key="a")
    assert js.text("#search-status") == ""