from collections.abc import Iterator
from pathlib import Path

import pytest

import strix.skills as skills_mod
from strix.agents.prompt import _resolve_skills, render_system_prompt
from strix.skills import (
    get_all_skill_names,
    get_available_skills,
    load_skills,
    register_skill_dir,
    registered_skill_dirs,
    skill_search_dirs,
    validate_requested_skills,
)
from strix.utils.resource_paths import get_strix_resource_path


@pytest.fixture(autouse=True)
def _clear_extra_dirs() -> Iterator[None]:
    original = list(skills_mod._EXTRA_SKILL_DIRS)
    skills_mod._EXTRA_SKILL_DIRS.clear()
    try:
        yield
    finally:
        skills_mod._EXTRA_SKILL_DIRS[:] = original


def _write_skill(root: Path, category: str, name: str, body: str) -> None:
    category_dir = root / category
    category_dir.mkdir(parents=True, exist_ok=True)
    (category_dir / f"{name}.md").write_text(body, encoding="utf-8")


def _write_root_skill(root: Path, name: str, body: str) -> None:
    root.mkdir(parents=True, exist_ok=True)
    (root / f"{name}.md").write_text(body, encoding="utf-8")


def test_no_registration_leaves_builtin_only() -> None:
    assert registered_skill_dirs() == ()
    builtin = get_strix_resource_path("skills")
    assert skill_search_dirs() == (builtin,)
    assert {"nmap", "subfinder"}.issubset(
        {skill["name"] for skill in get_available_skills()["tooling"]}
    )


def test_register_is_idempotent_and_ordered(tmp_path: Path) -> None:
    a = tmp_path / "a"
    b = tmp_path / "b"
    a.mkdir()
    b.mkdir()

    register_skill_dir(a)
    register_skill_dir(b)
    register_skill_dir(a)

    # Most recently registered wins → highest precedence first.
    assert registered_skill_dirs() == (b, a)


def test_registered_dir_adds_new_skill(tmp_path: Path) -> None:
    _write_skill(tmp_path, "extra", "widget", "widget body")
    register_skill_dir(tmp_path)

    assert "widget" in get_all_skill_names()
    assert get_available_skills()["extra"] == [{"name": "widget", "description": ""}]
    assert load_skills(["widget"]) == {"widget": "widget body"}


def test_available_skill_includes_frontmatter_description(tmp_path: Path) -> None:
    _write_skill(
        tmp_path,
        "extra",
        "widget",
        "---\nname: widget\ndescription: Useful widget guidance\n---\nwidget body",
    )
    register_skill_dir(tmp_path)

    assert get_available_skills()["extra"] == [
        {"name": "widget", "description": "Useful widget guidance"}
    ]


def test_available_skill_supports_colon_in_description(tmp_path: Path) -> None:
    _write_skill(
        tmp_path,
        "extra",
        "widget",
        '---\nname: widget\ndescription: "Useful widget: handles YAML"\n---\nwidget body',
    )
    register_skill_dir(tmp_path)

    assert get_available_skills()["extra"] == [
        {"name": "widget", "description": "Useful widget: handles YAML"}
    ]


def test_available_skill_normalizes_quoted_description(tmp_path: Path) -> None:
    _write_skill(
        tmp_path,
        "extra",
        "widget",
        '---\nname: widget\ndescription: "Useful: widget guidance"\n---\nwidget body',
    )
    register_skill_dir(tmp_path)

    assert get_available_skills()["extra"] == [
        {"name": "widget", "description": "Useful: widget guidance"}
    ]


def test_available_skill_normalizes_multiline_descriptions(tmp_path: Path) -> None:
    _write_skill(
        tmp_path,
        "extra",
        "block",
        "---\nname: block\n\ndescription: |\n"
        "  First paragraph\n\n  Second paragraph\n\n---\nblock body",
    )
    _write_skill(
        tmp_path,
        "extra",
        "plain",
        "---\nname: plain\n\ndescription: First line\n  Second line\n\n---\nplain body",
    )
    register_skill_dir(tmp_path)

    available = {skill["name"]: skill["description"] for skill in get_available_skills()["extra"]}
    assert available == {
        "block": "First paragraph Second paragraph",
        "plain": "First line Second line",
    }


def test_available_skill_supports_block_scalar_trailing_comment(tmp_path: Path) -> None:
    _write_skill(
        tmp_path,
        "extra",
        "commented",
        "---\nname: commented\ndescription: | # paragraph\n"
        "  First line\n  Second line\n---\ncommented body",
    )
    register_skill_dir(tmp_path)

    assert get_available_skills()["extra"] == [
        {"name": "commented", "description": "First line Second line"}
    ]


def test_malformed_frontmatter_keeps_skill_body(tmp_path: Path) -> None:
    _write_skill(
        tmp_path,
        "extra",
        "broken",
        "---\nname: [broken\ndescription: should be empty\n---\nbroken body",
    )
    register_skill_dir(tmp_path)

    assert get_available_skills()["extra"] == [{"name": "broken", "description": ""}]
    assert load_skills(["extra/broken"]) == {"broken": "broken body"}


def test_system_prompt_renders_skill_descriptions() -> None:
    prompt = render_system_prompt(scan_mode="quick", is_root=True)

    assert "- technologies/firebase: Firebase security testing covering" in prompt


def test_system_prompt_omits_empty_skill_description(tmp_path: Path) -> None:
    _write_skill(tmp_path, "extra", "widget", "---\nname: widget\ndescription:\n---\nwidget body")
    register_skill_dir(tmp_path)

    prompt = render_system_prompt(scan_mode="quick", is_root=True)

    assert "- extra/widget\n" in prompt
    assert "- extra/widget: " not in prompt


def test_registered_root_skill_is_discoverable_and_valid(tmp_path: Path) -> None:
    _write_root_skill(tmp_path, "widget", "widget body")
    register_skill_dir(tmp_path)

    assert "widget" in get_all_skill_names()
    assert get_available_skills()["root"] == [{"name": "widget", "description": ""}]
    assert validate_requested_skills(["widget"]) is None
    assert validate_requested_skills(["root/widget"]) is None
    assert load_skills(["widget"]) == {"widget": "widget body"}
    assert load_skills(["root/widget"]) == {"widget": "widget body"}


def test_ambiguous_bare_skill_requires_qualified_name(tmp_path: Path) -> None:
    _write_skill(tmp_path, "alpha", "widget", "alpha body")
    _write_skill(tmp_path, "beta", "widget", "beta body")
    register_skill_dir(tmp_path)

    assert "widget" in get_all_skill_names()
    assert get_available_skills()["alpha"] == [{"name": "widget", "description": ""}]
    assert get_available_skills()["beta"] == [{"name": "widget", "description": ""}]
    assert validate_requested_skills(["alpha/widget"]) is None
    assert validate_requested_skills(["beta/widget"]) is None

    error = validate_requested_skills(["widget"])
    assert error is not None
    assert "Ambiguous skill name" in error
    assert "alpha/widget" in error
    assert "beta/widget" in error

    assert load_skills(["widget"]) == {}
    assert load_skills(["alpha/widget"]) == {"widget": "alpha body"}
    assert load_skills(["beta/widget"]) == {"widget": "beta body"}


def test_registered_dir_overrides_builtin_skill(tmp_path: Path) -> None:
    _write_skill(tmp_path, "coordination", "root_agent", "overridden root agent")
    register_skill_dir(tmp_path)

    loaded = load_skills(["coordination/root_agent"])
    assert loaded["root_agent"] == "overridden root agent"


def test_builtin_skill_still_loads_when_not_overridden(tmp_path: Path) -> None:
    _write_skill(tmp_path, "extra", "widget", "widget body")
    register_skill_dir(tmp_path)

    # A packaged skill the registered dir does not shadow still resolves.
    assert load_skills(["scan_modes/deep"]).get("deep")


def test_missing_skill_is_skipped(tmp_path: Path) -> None:
    register_skill_dir(tmp_path)
    assert load_skills(["does_not_exist"]) == {}


def test_resolve_skills_always_includes_analysis_baseline() -> None:
    resolved = _resolve_skills(requested=None)

    assert "analysis/counterevidence" in resolved
    assert "analysis/severity_calibration" in resolved


def test_resolve_skills_adds_diff_mode_only_when_diff_scoped() -> None:
    assert "scan_modes/diff" not in _resolve_skills(requested=None)
    diff_scoped = _resolve_skills(requested=None, is_diff_scoped=True)
    assert "scan_modes/diff" in diff_scoped
    # Diff scope overlays the depth mode rather than replacing it.
    assert "scan_modes/deep" in diff_scoped


def test_resolve_skills_gates_source_aware_skills_on_whitebox() -> None:
    blackbox = _resolve_skills(requested=None)
    assert "analysis/fix_verification" not in blackbox
    assert "analysis/source_aware_discovery" not in blackbox

    whitebox = _resolve_skills(requested=None, is_whitebox=True)
    assert "analysis/fix_verification" in whitebox
    assert "analysis/source_aware_discovery" in whitebox


def test_new_skill_files_load() -> None:
    names = [
        "analysis/counterevidence",
        "analysis/severity_calibration",
        "analysis/fix_verification",
        "analysis/source_aware_discovery",
        "scan_modes/diff",
    ]
    loaded = load_skills(names)
    for name in names:
        key = name.split("/")[-1]
        assert loaded.get(key), f"{name} failed to load"
