"""Ensure editors obtain Flow language knowledge from the native tooling.

The Sublime grammar is generated by ``a11 flow syntax``. Dynamic editor
features query ``a11-flow`` rather than maintaining separate language tables.
"""

import pathlib
import re

import pytest

ROOT = pathlib.Path(__file__).parents[3]

SUBLIME = ROOT / "editors" / "sublime-text" / "A11 Flow.sublime-syntax"

PYGMENTS = ROOT / "editors" / "pygments" / "a11flow_lexer.py"

PLUGIN = ROOT / "intellij-plugin"

FLOW_PLUGIN_SOURCE = (
    PLUGIN
    / "src"
    / "main"
    / "kotlin"
    / "dev"
    / "curiositystack"
    / "a11"
    / "clion"
    / "flow"
)


def test_the_sublime_syntax_is_what_the_language_generates():
    """Keep the checked-in static grammar synchronized with its generator."""
    from a11._native import flow as native_flow

    if not SUBLIME.is_file():
        pytest.skip(f"{SUBLIME.name} is not in this checkout")
    generated = native_flow.syntax("sublime")
    assert generated["path"] == "editors/sublime-text/A11 Flow.sublime-syntax"
    assert SUBLIME.read_text(encoding="utf-8") == generated["text"], (
        "the Sublime syntax is out of date -- run"
        " `a11 flow syntax --target sublime --generate`"
    )


def test_the_pygments_lexer_is_what_the_language_generates():
    """The same check for the lexer that colours the documentation.

    In prose, an unknown Flow word looks like ordinary text rather than an
    error, so the generated-file comparison detects stale highlighting rules.
    """
    from a11._native import flow as native_flow

    if not PYGMENTS.is_file():
        pytest.skip(f"{PYGMENTS.name} is not in this checkout")
    generated = native_flow.syntax("pygments")
    assert generated["path"] == "editors/pygments/a11flow_lexer.py"
    assert PYGMENTS.read_text(encoding="utf-8") == generated["text"], (
        "the Pygments lexer is out of date -- run"
        " `a11 flow syntax --target pygments --generate`"
    )


def test_the_pygments_lexer_colours_every_flow_in_the_repository():
    """It is a static grammar, so this is what says it still reads the language.

    The staleness check above compares it with what the generator writes; this
    runs it. A rule that stopped matching -- a state that no longer pops, a
    pattern the language outgrew -- leaves `Error` tokens behind, and every
    ``.flow`` file in the checkout is the corpus that finds them.
    """
    pytest.importorskip("pygments")
    if not PYGMENTS.is_file():
        pytest.skip(f"{PYGMENTS.name} is not in this checkout")

    import importlib.util

    from pygments.token import Error

    spec = importlib.util.spec_from_file_location("a11flow_lexer", PYGMENTS)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    lexer = module.A11FlowLexer()

    flows = [
        path
        for path in ROOT.glob("**/*.flow")
        if "build" not in path.parts and ".venv" not in path.parts
    ]
    assert flows, "no .flow files to read"
    for path in flows:
        source = path.read_text(encoding="utf-8")
        unknown = [
            text for kind, text in lexer.get_tokens(source) if kind is Error
        ]
        assert not unknown, f"{path.name}: the lexer chokes on {unknown[:5]}"


def test_the_generated_syntax_says_it_is_generated():
    """Generated syntax files identify themselves as generated."""
    for generated in (SUBLIME, PYGMENTS):
        if not generated.is_file():
            pytest.skip(f"{generated.name} is not in this checkout")
        assert "GENERATED FILE" in generated.read_text(encoding="utf-8")
    text = SUBLIME.read_text(encoding="utf-8")
    assert "file_extensions: [flow]" in text
    assert list((ROOT / "examples" / "003-flow-dsl").glob("*.flow"))


def test_the_plugin_keeps_no_language_knowledge_of_its_own():
    """Keep Flow language judgments out of the IntelliJ plugin."""
    if not FLOW_PLUGIN_SOURCE.is_dir():
        pytest.skip("the IntelliJ plugin is not in this checkout")
    assert not (FLOW_PLUGIN_SOURCE / "FlowVocabulary.kt").exists()
    assert not (FLOW_PLUGIN_SOURCE / "analysis").exists()

    from a11._native import flow as native_flow

    stages = set(native_flow.vocabulary()["stages"])
    for source in FLOW_PLUGIN_SOURCE.glob("*.kt"):
        text = source.read_text(encoding="utf-8")
        quoted = set(re.findall(r'"([a-z][a-z0-9_-]*)"', text))
        # A handful of stage names would be a table; the semantic *kinds* the
        # payload uses are not stage names, so this is a real signal.
        assert len(quoted & stages) < 3, (
            f"{source.name} looks like it carries a list of stages"
        )


def test_python_keeps_no_language_implementation_of_its_own():
    """Keep ``a11.flow`` as glue over ``a11._native.flow``."""
    package = ROOT / "a11" / "flow"
    for gone in ("lexer.py", "parser.py", "syntax.py", "values.py"):
        assert not (package / gone).exists(), f"a11/flow/{gone} is back"
    for gone in (
        "test_native_parity.py",
        "test_parser_parity.py",
        "test_resolve_parity.py",
    ):
        assert not (package / "tests" / gone).exists(), (
            f"{gone} is back, which means a second implementation is too"
        )
    for module in ("plan.py", "runtime.py"):
        text = (package / module).read_text(encoding="utf-8")
        assert "_native" in text, f"a11/flow/{module} stopped asking natively"


def test_the_plugin_registers_the_language_id_the_docs_tell_people_to_use():
    """`# language=A11Flow` only works if that is the id that was registered."""
    plugin_xml = (
        PLUGIN / "src" / "main" / "resources" / "META-INF" / "plugin.xml"
    )
    if not plugin_xml.is_file():
        pytest.skip("the IntelliJ plugin is not in this checkout")
    registered = plugin_xml.read_text(encoding="utf-8")
    assert 'language="A11Flow"' in registered
    assert 'extensions="flow"' in registered
    assert "multiHostInjector" in registered
    # Semantic diagnostics come from one external annotator over `a11-flow`.
    assert "externalAnnotator" in registered
    assert "localInspection" not in registered
