"""Tests for mcpaudit Rule Generation Engine (rule_gen.py).

Covers: templates, generation from template, generation from finding,
code validity, CLI integration, edge cases.
Minimum: 35 tests.
"""

from __future__ import annotations

import ast
import json
from pathlib import Path

import pytest

from mcpaudit.rule_gen import (
    BUILTIN_TEMPLATES,
    GeneratedRule,
    RuleTemplate,
    _escape_python_string,
    _guess_category,
    _pattern_from_snippet,
    _sanitize_class_name,
    _sanitize_rule_id,
    _validate_severity,
    generate_rule_from_finding,
    generate_rule_from_template,
    get_template,
    list_templates,
    save_generated_rule,
)


# ===================================================================
# Template listing and retrieval
# ===================================================================

class TestTemplates:
    def test_list_templates_returns_all_five(self):
        templates = list_templates()
        assert len(templates) == 5

    def test_list_templates_sorted(self):
        templates = list_templates()
        assert templates == sorted(templates)

    def test_list_templates_contains_expected(self):
        templates = list_templates()
        for name in [
            "auth_check",
            "command_injection",
            "input_validation",
            "path_traversal",
            "secret_detection",
        ]:
            assert name in templates

    def test_get_template_known(self):
        t = get_template("command_injection")
        assert t is not None
        assert isinstance(t, RuleTemplate)
        assert t.cwe == "CWE-78"

    def test_get_template_unknown_returns_none(self):
        assert get_template("nonexistent") is None

    def test_all_templates_have_required_fields(self):
        for name, t in BUILTIN_TEMPLATES.items():
            assert t.name, f"{name} missing name"
            assert t.description, f"{name} missing description"
            assert t.pattern, f"{name} missing pattern"
            assert t.severity, f"{name} missing severity"
            assert t.cwe, f"{name} missing cwe"
            assert t.category, f"{name} missing category"

    def test_all_templates_severity_valid(self):
        valid = {"CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"}
        for name, t in BUILTIN_TEMPLATES.items():
            assert t.severity in valid, f"{name} has invalid severity {t.severity}"


# ===================================================================
# Generation from template
# ===================================================================

class TestGenerateFromTemplate:
    def test_basic_generation(self):
        result = generate_rule_from_template(
            "command_injection",
            {"rule_id": "MY-001"},
        )
        assert isinstance(result, GeneratedRule)
        assert result.rule_id == "MY-001"
        assert result.severity == "CRITICAL"
        assert result.template_used == "command_injection"

    def test_generated_code_is_valid_python(self):
        result = generate_rule_from_template(
            "path_traversal",
            {"rule_id": "CUSTOM-002"},
        )
        # Must parse without syntax errors
        ast.parse(result.source_code)

    def test_generated_code_contains_rule_id(self):
        result = generate_rule_from_template(
            "secret_detection",
            {"rule_id": "SEC-100"},
        )
        assert "SEC-100" in result.source_code

    def test_generated_code_contains_register_rules(self):
        result = generate_rule_from_template(
            "auth_check",
            {"rule_id": "AUTH-050"},
        )
        assert "def register_rules()" in result.source_code

    def test_custom_description(self):
        result = generate_rule_from_template(
            "input_validation",
            {"rule_id": "INP-010", "description": "Custom desc here"},
        )
        assert result.description == "Custom desc here"
        assert "Custom desc here" in result.source_code

    def test_custom_severity_override(self):
        result = generate_rule_from_template(
            "command_injection",
            {"rule_id": "CMD-099", "severity": "low"},
        )
        assert result.severity == "LOW"
        assert "Severity.LOW" in result.source_code

    def test_custom_pattern_override(self):
        result = generate_rule_from_template(
            "command_injection",
            {"rule_id": "CMD-098", "pattern": r"eval\("},
        )
        assert result.pattern == r"eval\("

    def test_unknown_template_raises(self):
        with pytest.raises(ValueError, match="Unknown template"):
            generate_rule_from_template("fake_template", {"rule_id": "X-1"})

    def test_missing_rule_id_raises(self):
        with pytest.raises(ValueError, match="rule_id is required"):
            generate_rule_from_template("command_injection", {})

    def test_invalid_severity_raises(self):
        with pytest.raises(ValueError, match="Invalid severity"):
            generate_rule_from_template(
                "command_injection",
                {"rule_id": "X-1", "severity": "EXTREME"},
            )

    def test_to_dict(self):
        result = generate_rule_from_template(
            "command_injection",
            {"rule_id": "MY-001"},
        )
        d = result.to_dict()
        assert d["rule_id"] == "MY-001"
        assert d["template_used"] == "command_injection"
        assert "source_code" not in d

    def test_all_templates_generate_valid_python(self):
        for name in list_templates():
            result = generate_rule_from_template(
                name,
                {"rule_id": f"TEST-{name[:3].upper()}"},
            )
            ast.parse(result.source_code)


# ===================================================================
# Generation from finding
# ===================================================================

class TestGenerateFromFinding:
    def test_basic_finding_generation(self):
        finding = {
            "rule_id": "CMD-001",
            "severity": "CRITICAL",
            "title": "Shell injection detected",
            "description": "subprocess with shell=True",
            "code_snippet": "subprocess.run(cmd, shell=True)",
            "cwe": "CWE-78",
        }
        result = generate_rule_from_finding(finding)
        assert result.rule_id == "CMD-001"
        assert result.severity == "CRITICAL"
        assert result.template_used == "from_finding"

    def test_finding_code_is_valid_python(self):
        finding = {
            "rule_id": "PATH-005",
            "severity": "HIGH",
            "title": "Path traversal",
            "description": "Unvalidated path join",
            "code_snippet": "pathlib.Path(base) / user_input",
            "cwe": "CWE-22",
        }
        result = generate_rule_from_finding(finding)
        ast.parse(result.source_code)

    def test_finding_missing_rule_id_raises(self):
        with pytest.raises(ValueError, match="rule_id"):
            generate_rule_from_finding({"severity": "HIGH"})

    def test_finding_invalid_severity_raises(self):
        with pytest.raises(ValueError, match="Invalid severity"):
            generate_rule_from_finding({
                "rule_id": "X-1",
                "severity": "ULTRA",
            })

    def test_finding_defaults_severity_to_medium(self):
        result = generate_rule_from_finding({
            "rule_id": "X-1",
            "title": "Some finding",
        })
        assert result.severity == "MEDIUM"

    def test_finding_empty_snippet(self):
        result = generate_rule_from_finding({
            "rule_id": "X-2",
            "code_snippet": "",
        })
        # Should not crash, generates a catch-all pattern
        ast.parse(result.source_code)

    def test_finding_category_guessed_from_id(self):
        result = generate_rule_from_finding({
            "rule_id": "CMD-010",
            "title": "Foo",
        })
        assert result.category == "injection"

    def test_finding_category_guessed_from_description(self):
        result = generate_rule_from_finding({
            "rule_id": "CUSTOM-1",
            "description": "Missing authentication check",
        })
        assert result.category == "authentication"


# ===================================================================
# save_generated_rule
# ===================================================================

class TestSaveGeneratedRule:
    def test_save_creates_file(self, tmp_path: Path):
        result = generate_rule_from_template(
            "command_injection",
            {"rule_id": "MY-001"},
        )
        filepath = save_generated_rule(result, tmp_path)
        assert filepath.exists()
        assert filepath.suffix == ".py"
        assert filepath.name == "my_001.py"

    def test_save_content_matches(self, tmp_path: Path):
        result = generate_rule_from_template(
            "path_traversal",
            {"rule_id": "PT-050"},
        )
        filepath = save_generated_rule(result, tmp_path)
        content = filepath.read_text(encoding="utf-8")
        assert content == result.source_code

    def test_save_creates_directory(self, tmp_path: Path):
        subdir = tmp_path / "rules" / "custom"
        result = generate_rule_from_template(
            "secret_detection",
            {"rule_id": "SEC-001"},
        )
        filepath = save_generated_rule(result, subdir)
        assert subdir.exists()
        assert filepath.exists()


# ===================================================================
# Helper functions
# ===================================================================

class TestHelpers:
    def test_sanitize_class_name_basic(self):
        assert _sanitize_class_name("MY-001") == "My001Rule"

    def test_sanitize_class_name_complex(self):
        assert _sanitize_class_name("CMD-INJECT-99") == "CmdInject99Rule"

    def test_sanitize_rule_id(self):
        assert _sanitize_rule_id("  my-001 ") == "MY-001"

    def test_escape_python_string(self):
        assert _escape_python_string('hello "world"') == 'hello \\"world\\"'
        assert _escape_python_string("line\nbreak") == "line\\nbreak"

    def test_validate_severity_valid(self):
        for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
            _validate_severity(sev)  # Should not raise

    def test_validate_severity_invalid(self):
        with pytest.raises(ValueError):
            _validate_severity("EXTREME")

    def test_pattern_from_snippet_basic(self):
        p = _pattern_from_snippet("pathlib.Path(base)")
        assert "pathlib" in p

    def test_pattern_from_snippet_empty(self):
        p = _pattern_from_snippet("")
        assert p == r".*"

    def test_guess_category_cmd_prefix(self):
        assert _guess_category("CMD-001", "Something") == "injection"

    def test_guess_category_from_description(self):
        assert _guess_category("X-1", "secret key exposed") == "secrets"

    def test_guess_category_default(self):
        assert _guess_category("FOO-1", "something random") == "custom"
