"""Tests for tools/skill_linter.py — the advisory SKILL.md convention linter."""


from tools.skill_linter import (
    ERROR,
    WARNING,
    lint_content,
    lint_skill,
)

# A clean, peer-shaped SKILL.md that should produce zero findings.
CLEAN = """---
name: my-skill
description: Search arXiv papers by keyword, author, or ID.
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
  hermes:
    tags: [arxiv, research]
    related_skills: []
---

# My Skill

## Overview
Does a thing.

## When to Use
- When the user wants X.

## Procedure
1. Use `read_file` to load it.
"""


def _rules(findings):
    return {f.rule for f in findings}


def test_clean_skill_has_no_findings():
    assert lint_content(CLEAN) == []


def test_description_too_long_is_warning():
    long_desc = "x" * 80
    content = CLEAN.replace(
        "Search arXiv papers by keyword, author, or ID.", long_desc
    )
    findings = lint_content(content)
    assert "description-length" in _rules(findings)
    assert all(f.severity == WARNING for f in findings)


def test_marketing_words_flagged():
    content = CLEAN.replace(
        "Search arXiv papers by keyword, author, or ID.",
        "A powerful comprehensive tool.",
    )
    findings = lint_content(content)
    assert "description-marketing" in _rules(findings)


def test_shell_utility_reference_in_prose_flagged():
    content = CLEAN.replace("Use `read_file` to load it.", "Use `grep` to find it.")
    findings = lint_content(content)
    assert "shell-utility-reference" in _rules(findings)


def test_shell_utility_inside_code_block_not_flagged():
    # A fenced code block legitimately shows grep; prose check must skip it.
    content = CLEAN + "\n```bash\ngrep -r foo .\n```\n"
    findings = lint_content(content)
    assert "shell-utility-reference" not in _rules(findings)


def test_missing_metadata_block_warns():
    content = """---
name: bare-skill
description: Does a thing briefly.
---

# Bare Skill

## When to Use
- now
"""
    findings = lint_content(content)
    rules = _rules(findings)
    assert "missing-metadata" in rules


def test_missing_when_to_use_section_warns():
    content = CLEAN.replace("## When to Use\n- When the user wants X.\n", "")
    findings = lint_content(content)
    assert "missing-section" in _rules(findings)


def test_bad_name_format_is_error():
    content = CLEAN.replace("name: my-skill", "name: My_Skill!")
    findings = lint_content(content)
    assert "name-format" in _rules(findings)
    assert any(f.severity == ERROR for f in findings)


def test_name_dir_mismatch_is_error(tmp_path):
    skill_dir = tmp_path / "actual-dir"
    skill_dir.mkdir()
    findings = lint_content(CLEAN, skill_dir=skill_dir)  # name is my-skill
    assert "name-dir-mismatch" in _rules(findings)
    assert any(f.severity == ERROR for f in findings)


def test_dangling_reference_link_flagged(tmp_path):
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    content = CLEAN + "\nSee references/missing.md for detail.\n"
    findings = lint_content(content, skill_dir=skill_dir)
    assert "dangling-reference" in _rules(findings)


def test_present_reference_link_not_flagged(tmp_path):
    skill_dir = tmp_path / "my-skill"
    (skill_dir / "references").mkdir(parents=True)
    (skill_dir / "references" / "detail.md").write_text("x")
    content = CLEAN + "\nSee references/detail.md for detail.\n"
    findings = lint_content(content, skill_dir=skill_dir)
    assert "dangling-reference" not in _rules(findings)


def test_posix_primitive_without_platforms_warns(tmp_path):
    skill_dir = tmp_path / "my-skill"
    (skill_dir / "scripts").mkdir(parents=True)
    (skill_dir / "scripts" / "run.py").write_text("import fcntl\nfcntl.flock(1, 2)\n")
    findings = lint_content(CLEAN, skill_dir=skill_dir)
    assert "platforms-gating" in _rules(findings)


def test_posix_primitive_with_platforms_ok(tmp_path):
    skill_dir = tmp_path / "my-skill"
    (skill_dir / "scripts").mkdir(parents=True)
    (skill_dir / "scripts" / "run.py").write_text("import fcntl\n")
    content = CLEAN.replace(
        "version: 1.0.0", "version: 1.0.0\nplatforms: [linux, macos]"
    )
    findings = lint_content(content, skill_dir=skill_dir)
    assert "platforms-gating" not in _rules(findings)


def test_forbidden_file_flagged(tmp_path):
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    (skill_dir / "README.md").write_text("# readme")
    findings = lint_content(CLEAN, skill_dir=skill_dir)
    assert "forbidden-file" in _rules(findings)


def test_invalid_platforms_value_warns():
    content = CLEAN.replace(
        "version: 1.0.0", "version: 1.0.0\nplatforms: [linux, solaris]"
    )
    findings = lint_content(content)
    assert "platforms-value" in _rules(findings)


def test_lint_skill_reads_from_disk(tmp_path):
    skill_dir = tmp_path / "my-skill"
    skill_dir.mkdir()
    skill_md = skill_dir / "SKILL.md"
    skill_md.write_text(CLEAN)
    findings = lint_skill(skill_md)
    assert findings == []


def test_author_caps_warned():
    content = CLEAN.replace("author: Hermes Agent", "author: hermes agent")
    findings = lint_content(content)
    assert "author-caps" in _rules(findings)


def test_incident_log_shape_flagged_and_rule_shape_not():
    # A body narrating incidents by PR number is a log, not a lesson; the same lesson stated as a
    # rule + why with no numbers passes. Density-gated so one citation in a long body is fine.
    log = CLEAN.replace(
        "1. Use `read_file` to load it.",
        "In #12345 the watcher died; #23456 was the same; see PR #34567 and issue #45678 for the fix.",
    )
    rule = CLEAN.replace(
        "1. Use `read_file` to load it.",
        "Launch the watcher from a directory that outlives the watch; a deleted cwd reads as a stall.",
    )
    assert "incident-log-shape" in _rules(lint_content(log))
    assert "incident-log-shape" not in _rules(lint_content(rule))


def test_references_sprawl_flagged_above_cap(tmp_path):
    from tools.skill_linter import _MAX_REFERENCE_FILES
    skill_dir = tmp_path / "my-skill"
    refs = skill_dir / "references"
    refs.mkdir(parents=True)
    for i in range(_MAX_REFERENCE_FILES + 1):
        (refs / f"note-{i}.md").write_text("x")
    (skill_dir / "SKILL.md").write_text(CLEAN)
    assert "references-sprawl" in _rules(lint_skill(skill_dir / "SKILL.md"))
    (refs / f"note-{_MAX_REFERENCE_FILES}.md").unlink()
    assert "references-sprawl" not in _rules(lint_skill(skill_dir / "SKILL.md"))


def test_oversized_body_flagged_above_budget_and_not_below():
    # skill_view loads SKILL.md whole and it rides in context for the rest of the session, so the
    # body has a soft budget. Threshold-relative on purpose: the number is a calibration, not a
    # contract. The finding names the size so the author sees how far over they are.
    from tools.skill_linter import _BODY_SOFT_BUDGET_CHARS
    filler = "- Prefer the native tool; the shell path loses the structured result.\n"
    over = CLEAN + filler * (_BODY_SOFT_BUDGET_CHARS // len(filler) + 1)
    under = CLEAN + filler * (_BODY_SOFT_BUDGET_CHARS // len(filler) // 2)
    found = [f for f in lint_content(over) if f.rule == "oversized-body"]
    assert found and found[0].severity == WARNING
    assert "oversized-body" not in _rules(lint_content(under))
