"""Tests for MCP Scan Profiles (mcpaudit.scan_profiles).

Tests profile definitions, registry operations, serialization,
filtering, validation, and edge cases.
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.scan_profiles import (
    PROFILE_CI,
    PROFILE_COMPLIANCE,
    PROFILE_DEV,
    PROFILE_FULL,
    PROFILE_QUICK,
    ProfileRegistry,
    ScanProfile,
    apply_profile_filters,
    apply_profile_to_rules,
    format_profiles_json,
    format_profiles_text,
    get_profile,
    get_registry,
    list_profile_names,
    list_scan_profiles,
    load_profile_from_file,
    register_profile,
    validate_profile,
    _BUILTIN_PROFILES,
)
from mcpaudit.models import Finding, Severity


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_finding(
    rule_id: str = "CMD-001",
    severity: Severity = Severity.HIGH,
) -> Finding:
    return Finding(
        rule_id=rule_id,
        severity=severity,
        title="test",
        description="test desc",
        file="test.py",
        line=1,
    )


class _FakeRule:
    def __init__(self, rule_id: str):
        self.rule_id = rule_id


# ---------------------------------------------------------------------------
# ScanProfile data model
# ---------------------------------------------------------------------------

class TestScanProfile:
    def test_create_basic(self):
        p = ScanProfile(name="test", description="a test profile")
        assert p.name == "test"
        assert p.description == "a test profile"

    def test_defaults(self):
        p = ScanProfile(name="x")
        assert p.rules_enabled is None
        assert p.rules_disabled == []
        assert p.severity_threshold == "LOW"
        assert p.output_format == "text"
        assert p.options == {}

    def test_severity_property(self):
        p = ScanProfile(name="x", severity_threshold="HIGH")
        assert p.severity == Severity.HIGH

    def test_severity_property_low(self):
        p = ScanProfile(name="x", severity_threshold="LOW")
        assert p.severity == Severity.LOW

    def test_to_dict(self):
        p = ScanProfile(name="test", description="desc", severity_threshold="MEDIUM")
        d = p.to_dict()
        assert d["name"] == "test"
        assert d["severity_threshold"] == "MEDIUM"
        assert isinstance(d["rules_disabled"], list)

    def test_to_dict_keys(self):
        p = ScanProfile(name="x")
        d = p.to_dict()
        expected = {"name", "description", "rules_enabled", "rules_disabled",
                    "severity_threshold", "output_format", "options"}
        assert set(d.keys()) == expected

    def test_to_json(self):
        p = ScanProfile(name="test")
        j = p.to_json()
        parsed = json.loads(j)
        assert parsed["name"] == "test"

    def test_from_dict(self):
        d = {"name": "custom", "severity_threshold": "HIGH"}
        p = ScanProfile.from_dict(d)
        assert p.name == "custom"
        assert p.severity_threshold == "HIGH"

    def test_from_dict_defaults(self):
        d = {"name": "minimal"}
        p = ScanProfile.from_dict(d)
        assert p.rules_enabled is None
        assert p.severity_threshold == "LOW"

    def test_from_dict_no_name_raises(self):
        with pytest.raises(ValueError, match="name"):
            ScanProfile.from_dict({})

    def test_from_json(self):
        j = '{"name": "from_json", "output_format": "html"}'
        p = ScanProfile.from_json(j)
        assert p.name == "from_json"
        assert p.output_format == "html"

    def test_roundtrip(self):
        original = ScanProfile(
            name="round",
            description="roundtrip test",
            rules_enabled=["CMD-001", "SQL-001"],
            rules_disabled=["AUTH-001"],
            severity_threshold="HIGH",
            output_format="json",
            options={"timeout_seconds": 60},
        )
        j = original.to_json()
        restored = ScanProfile.from_json(j)
        assert restored.name == original.name
        assert restored.rules_enabled == original.rules_enabled
        assert restored.options == original.options


# ---------------------------------------------------------------------------
# Built-in profiles
# ---------------------------------------------------------------------------

class TestBuiltinProfiles:
    def test_quick_profile(self):
        assert PROFILE_QUICK.name == "quick"
        assert PROFILE_QUICK.severity_threshold == "HIGH"

    def test_full_profile(self):
        assert PROFILE_FULL.name == "full"
        assert PROFILE_FULL.severity_threshold == "INFO"

    def test_ci_profile(self):
        assert PROFILE_CI.name == "ci"
        assert PROFILE_CI.output_format == "json"
        assert PROFILE_CI.options.get("fail_on_findings") is True

    def test_compliance_profile(self):
        assert PROFILE_COMPLIANCE.name == "compliance"
        assert PROFILE_COMPLIANCE.output_format == "html"
        assert PROFILE_COMPLIANCE.rules_enabled is not None
        assert len(PROFILE_COMPLIANCE.rules_enabled) > 0

    def test_dev_profile(self):
        assert PROFILE_DEV.name == "dev"
        assert len(PROFILE_DEV.rules_disabled) > 0

    def test_builtin_count(self):
        assert len(_BUILTIN_PROFILES) == 5

    def test_all_builtins_have_names(self):
        for name, profile in _BUILTIN_PROFILES.items():
            assert profile.name == name


# ---------------------------------------------------------------------------
# ProfileRegistry
# ---------------------------------------------------------------------------

class TestProfileRegistry:
    def test_get_builtin(self):
        reg = ProfileRegistry()
        p = reg.get("quick")
        assert p.name == "quick"

    def test_get_case_insensitive(self):
        reg = ProfileRegistry()
        p = reg.get("QUICK")
        assert p.name == "quick"

    def test_get_unknown_raises(self):
        reg = ProfileRegistry()
        with pytest.raises(ValueError, match="Unknown profile"):
            reg.get("nonexistent")

    def test_register_custom(self):
        reg = ProfileRegistry()
        custom = ScanProfile(name="my_profile", description="custom")
        reg.register(custom)
        p = reg.get("my_profile")
        assert p.description == "custom"

    def test_register_empty_name_raises(self):
        reg = ProfileRegistry()
        with pytest.raises(ValueError, match="name"):
            reg.register(ScanProfile(name=""))

    def test_unregister(self):
        reg = ProfileRegistry()
        custom = ScanProfile(name="to_remove")
        reg.register(custom)
        assert reg.unregister("to_remove") is True
        assert reg.has("to_remove") is False

    def test_unregister_nonexistent(self):
        reg = ProfileRegistry()
        assert reg.unregister("nope") is False

    def test_list_profiles(self):
        reg = ProfileRegistry()
        profiles = reg.list_profiles()
        names = [p.name for p in profiles]
        assert "quick" in names
        assert "full" in names

    def test_list_names(self):
        reg = ProfileRegistry()
        names = reg.list_names()
        assert isinstance(names, list)
        assert "quick" in names
        assert names == sorted(names)

    def test_builtin_names(self):
        reg = ProfileRegistry()
        bn = reg.builtin_names
        assert "quick" in bn
        assert "full" in bn
        assert "ci" in bn

    def test_has(self):
        reg = ProfileRegistry()
        assert reg.has("quick") is True
        assert reg.has("nonexistent") is False

    def test_load_from_file(self):
        profile_data = {"name": "file_profile", "severity_threshold": "HIGH"}
        with tempfile.NamedTemporaryFile(
            suffix=".json", mode="w", delete=False
        ) as f:
            json.dump(profile_data, f)
            f.flush()
            reg = ProfileRegistry()
            p = reg.load_from_file(f.name)
            assert p.name == "file_profile"
            assert reg.has("file_profile")

    def test_load_from_file_not_found(self):
        reg = ProfileRegistry()
        with pytest.raises(FileNotFoundError):
            reg.load_from_file("/nonexistent/path.json")

    def test_save_to_file(self):
        reg = ProfileRegistry()
        with tempfile.NamedTemporaryFile(
            suffix=".json", mode="w", delete=False
        ) as f:
            reg.save_to_file("quick", f.name)
            content = Path(f.name).read_text()
            parsed = json.loads(content)
            assert parsed["name"] == "quick"


# ---------------------------------------------------------------------------
# Module-level functions
# ---------------------------------------------------------------------------

class TestModuleFunctions:
    def test_get_profile(self):
        p = get_profile("full")
        assert p.name == "full"

    def test_list_scan_profiles(self):
        profiles = list_scan_profiles()
        assert len(profiles) >= 5

    def test_list_profile_names(self):
        names = list_profile_names()
        assert "quick" in names

    def test_load_profile_from_file(self):
        data = {"name": "loaded", "output_format": "sarif"}
        with tempfile.NamedTemporaryFile(
            suffix=".json", mode="w", delete=False
        ) as f:
            json.dump(data, f)
            f.flush()
            p = load_profile_from_file(f.name)
            assert p.name == "loaded"

    def test_get_registry(self):
        reg = get_registry()
        assert isinstance(reg, ProfileRegistry)


# ---------------------------------------------------------------------------
# Filtering
# ---------------------------------------------------------------------------

class TestApplyProfileFilters:
    def test_severity_filter(self):
        profile = ScanProfile(name="test", severity_threshold="HIGH")
        findings = [
            _make_finding("A", Severity.CRITICAL),
            _make_finding("B", Severity.HIGH),
            _make_finding("C", Severity.MEDIUM),
            _make_finding("D", Severity.LOW),
        ]
        result = apply_profile_filters(profile, findings)
        severities = {f.severity for f in result}
        assert Severity.LOW not in severities
        assert Severity.MEDIUM not in severities

    def test_rules_enabled_filter(self):
        profile = ScanProfile(name="test", rules_enabled=["CMD-001"])
        findings = [
            _make_finding("CMD-001", Severity.HIGH),
            _make_finding("SQL-001", Severity.HIGH),
        ]
        result = apply_profile_filters(profile, findings)
        assert len(result) == 1
        assert result[0].rule_id == "CMD-001"

    def test_rules_disabled_filter(self):
        profile = ScanProfile(name="test", rules_disabled=["CMD-001"])
        findings = [
            _make_finding("CMD-001", Severity.HIGH),
            _make_finding("SQL-001", Severity.HIGH),
        ]
        result = apply_profile_filters(profile, findings)
        assert len(result) == 1
        assert result[0].rule_id == "SQL-001"

    def test_no_filter(self):
        profile = ScanProfile(name="test", severity_threshold="INFO")
        findings = [_make_finding("A", Severity.INFO)]
        result = apply_profile_filters(profile, findings)
        assert len(result) == 1


class TestApplyProfileToRules:
    def test_enabled_filter(self):
        profile = ScanProfile(name="test", rules_enabled=["CMD-001"])
        rules = [_FakeRule("CMD-001"), _FakeRule("SQL-001")]
        result = apply_profile_to_rules(profile, rules)
        assert len(result) == 1
        assert result[0].rule_id == "CMD-001"

    def test_disabled_filter(self):
        profile = ScanProfile(name="test", rules_disabled=["CMD-001"])
        rules = [_FakeRule("CMD-001"), _FakeRule("SQL-001")]
        result = apply_profile_to_rules(profile, rules)
        assert len(result) == 1
        assert result[0].rule_id == "SQL-001"

    def test_no_filter(self):
        profile = ScanProfile(name="test")
        rules = [_FakeRule("A"), _FakeRule("B")]
        result = apply_profile_to_rules(profile, rules)
        assert len(result) == 2


# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------

class TestValidateProfile:
    def test_valid_profile(self):
        p = ScanProfile(name="valid", severity_threshold="HIGH", output_format="json")
        errors = validate_profile(p)
        assert errors == []

    def test_empty_name(self):
        p = ScanProfile(name="", severity_threshold="HIGH")
        errors = validate_profile(p)
        assert any("name" in e.lower() for e in errors)

    def test_invalid_name_chars(self):
        p = ScanProfile(name="bad name!", severity_threshold="HIGH")
        errors = validate_profile(p)
        assert any("invalid" in e.lower() for e in errors)

    def test_invalid_severity(self):
        p = ScanProfile(name="test", severity_threshold="INVALID")
        errors = validate_profile(p)
        assert any("severity" in e.lower() for e in errors)

    def test_invalid_format(self):
        p = ScanProfile(name="test", output_format="xml")
        errors = validate_profile(p)
        assert any("format" in e.lower() for e in errors)

    def test_valid_formats(self):
        for fmt in ["text", "json", "html", "sarif"]:
            p = ScanProfile(name="test", output_format=fmt)
            errors = validate_profile(p)
            fmt_errors = [e for e in errors if "format" in e.lower()]
            assert fmt_errors == []

    def test_name_with_hyphens(self):
        p = ScanProfile(name="my-profile")
        errors = validate_profile(p)
        name_errors = [e for e in errors if "name" in e.lower() and "invalid" in e.lower()]
        assert name_errors == []

    def test_name_with_underscores(self):
        p = ScanProfile(name="my_profile")
        errors = validate_profile(p)
        name_errors = [e for e in errors if "name" in e.lower() and "invalid" in e.lower()]
        assert name_errors == []


# ---------------------------------------------------------------------------
# Formatters
# ---------------------------------------------------------------------------

class TestFormatProfilesText:
    def test_contains_profile_names(self):
        text = format_profiles_text()
        assert "quick" in text
        assert "full" in text
        assert "ci" in text

    def test_contains_descriptions(self):
        text = format_profiles_text()
        assert "Fast scan" in text

    def test_contains_separator(self):
        text = format_profiles_text()
        assert "=" * 60 in text


class TestFormatProfilesJson:
    def test_valid_json(self):
        result = format_profiles_json()
        parsed = json.loads(result)
        assert "profiles" in parsed
        assert isinstance(parsed["profiles"], list)

    def test_all_profiles_present(self):
        result = format_profiles_json()
        parsed = json.loads(result)
        names = [p["name"] for p in parsed["profiles"]]
        assert "quick" in names
        assert "full" in names

    def test_profile_structure(self):
        result = format_profiles_json()
        parsed = json.loads(result)
        p = parsed["profiles"][0]
        assert "name" in p
        assert "severity_threshold" in p
        assert "output_format" in p
