"""Drift + behaviour guard for the Windows/no-make quickstart runner.

``tasks.py`` + ``make.bat`` exist so the README quickstart runs on
native Windows without GNU make (ticket #000055). Two thin entry
points are a known DRY cost; this test pins them:

- the supported-target set is frozen here, so adding/removing a
  quickstart target without updating this contract fails loudly;
- venv-layout + Python-launcher detection is verified for *both*
  ``os.name`` values (the host only exercises one at runtime);
- the make-style ``KEY=VALUE`` parser is verified for every shell
  quoting form (POSIX strips quotes; cmd/PowerShell do not).

No network, no subprocess, no venv — pure import + function checks.
"""

from __future__ import annotations

import importlib.util
from pathlib import Path

import pytest

REPO = Path(__file__).resolve().parents[1]


def _load():
    """Fresh import of tasks.py with cleared module-level CLI state."""
    spec = importlib.util.spec_from_file_location("arborist_tasks", REPO / "tasks.py")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    mod._CLI_VARS.clear()
    mod._POSITIONALS.clear()
    return mod


# The frozen contract. Mirrors the README Quickstart + "After the answer"
# + Setup blocks and the Makefile quickstart subset. Changing the runner's
# surface MUST change this set in the same commit.
EXPECTED_TARGETS = {
    "bootstrap",
    "fetch-cur",
    "ingest-cur-attached",
    "distill-shards-parallel",
    "distill-shards-tfidf-parallel",
    "query",
    "query-dry",
    "inspect",
    "falsify",
    "burn",
    "burn-kindergarten",
    "bootstrap-crawler",
    "crawl-ingest",
    "recrawl-check",
    "stats",
    "stats-shards",
    "verify",
    "search",
    "test",
    "clean",
    "clean-db",
    "clean-data",
}


def test_supported_target_set_is_frozen():
    mod = _load()
    assert set(mod.TARGETS) == EXPECTED_TARGETS
    # Every target is callable and self-documents (help relies on __doc__).
    for name, fn in mod.TARGETS.items():
        assert callable(fn), name
        assert (fn.__doc__ or "").strip(), f"{name} has no help docstring"


def test_help_lists_every_target(capsys):
    mod = _load()
    mod.main(["tasks.py", "help"])
    out = capsys.readouterr().out
    for name in EXPECTED_TARGETS | {"help"}:
        assert f"  {name}" in out, name


def test_unknown_target_exits_2(capsys):
    mod = _load()
    with pytest.raises(SystemExit) as exc:
        mod.main(["tasks.py", "definitely-not-a-target"])
    assert exc.value.code == 2
    captured = capsys.readouterr()
    assert "unknown target" in captured.err  # diagnostics on stderr
    assert "show this help" in captured.out  # help still printed


@pytest.mark.parametrize(
    "is_windows,bin_name,arborist_name,py_name",
    [
        (False, "bin", "arborist", "python"),
        (True, "Scripts", "arborist.exe", "python.exe"),
    ],
)
def test_venv_layout_per_platform(monkeypatch, is_windows, bin_name, arborist_name, py_name):
    mod = _load()
    monkeypatch.setattr(mod, "IS_WINDOWS", is_windows)
    assert mod.venv_bin_dir().name == bin_name
    assert mod.arborist_exe().name == arborist_name
    assert mod.venv_python().name == py_name
    assert mod.venv_exe("pytest").name == ("pytest.exe" if is_windows else "pytest")


def test_windows_prefers_py_launcher(monkeypatch):
    mod = _load()
    monkeypatch.setattr(mod, "IS_WINDOWS", True)
    monkeypatch.delenv("ARBORIST_PYTHON", raising=False)
    monkeypatch.setattr(mod.shutil, "which", lambda name: f"/x/{name}" if name == "py" else None)
    assert mod.base_python_cmd() == ["py", "-3"]


def test_posix_uses_python3(monkeypatch):
    mod = _load()
    monkeypatch.setattr(mod, "IS_WINDOWS", False)
    monkeypatch.delenv("ARBORIST_PYTHON", raising=False)
    monkeypatch.setattr(mod.shutil, "which", lambda name: f"/usr/bin/{name}")
    assert mod.base_python_cmd() == ["python3"]


def test_arborist_python_override(monkeypatch):
    mod = _load()
    monkeypatch.setenv("ARBORIST_PYTHON", "py -3.12")
    assert mod.base_python_cmd() == ["py", "-3.12"]


@pytest.mark.parametrize(
    "argv,expect_q",
    [
        # POSIX: the shell already stripped the quotes.
        (["query", "Q=What is X?"], "What is X?"),
        # cmd: KEY="value with spaces" — quotes forwarded literally.
        (["query", 'Q="What is X?"'], "What is X?"),
        # cmd-friendly whole-token form.
        (["query", '"Q=What is X?"'], "What is X?"),
    ],
)
def test_make_style_value_parsing(argv, expect_q):
    mod = _load()
    # Stop before executing the target — just parse args.
    mod.TARGETS["query"] = lambda: None
    mod.main(["tasks.py", *argv])
    assert mod._CLI_VARS["Q"] == expect_q


def test_positional_question_and_flags():
    mod = _load()
    mod.TARGETS["query"] = lambda: None
    mod.main(["tasks.py", "query", "What is X?", "JSON=1", "ANSWER_MODE=quote"])
    assert mod._POSITIONALS == ["What is X?"]
    assert mod._CLI_VARS["JSON"] == "1" and mod.truthy("JSON")
    assert mod._CLI_VARS["ANSWER_MODE"] == "quote"
    assert not mod.truthy("BURN")  # unset → falsey, like make


def test_crawl_shard_name_matches_makefile_sed():
    mod = _load()
    mod._CLI_VARS["URL"] = "https://russell.ballestrini.net/blog/x?y=1"
    # Makefile: sed 's,^https?://([^/]+).*,\1,' | tr '.' '_'
    assert mod._crawl_shard_path().name == "crawl_russell_ballestrini_net.db"


def test_shim_and_attributes_present():
    assert (REPO / "make.bat").is_file()
    text = (REPO / "make.bat").read_text()
    assert "tasks.py" in text and "py -3" in text
    ga = (REPO / ".gitattributes").read_text()
    assert "*.bat text eol=crlf" in ga
