"""Tests for Compliance Profiles -- v1.3.0 feature.

Tests profile definition, rule filtering, severity filtering,
fail threshold checking, and CLI integration.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from mcpaudit.models import Finding, Severity
from mcpaudit.profiles import (
    PROFILE_CI,
    PROFILE_DEVELOPMENT,
    PROFILE_MCP_ONLY,
    PROFILE_NAMES,
    PROFILE_OWASP_AGENTIC,
    PROFILE_STANDARD,
    PROFILE_STRICT,
    PROFILES,
    ComplianceProfile,
    apply_profile_severity_filter,
    apply_profile_to_rules,
    check_fail_threshold,
    format_profiles_text,
    get_profile,
    list_profiles,
)
from mcpaudit.rules import get_all_rules


# ===========================================================================
# ComplianceProfile dataclass
# ===========================================================================

class TestComplianceProfile:
    """Tests for ComplianceProfile dataclass."""

    def test_strict_profile(self):
        p = PROFILE_STRICT
        assert p.name == "strict"
        assert p.enabled_rules is None  # all rules
        assert p.min_severity == Severity.LOW
        assert p.fail_threshold == 1

    def test_standard_profile(self):
        p = PROFILE_STANDARD
        assert p.name == "standard"
        assert p.enabled_rules is None
        assert p.min_severity == Severity.MEDIUM
        assert p.fail_threshold == 3

    def test_ci_profile(self):
        p = PROFILE_CI
        assert p.name == "ci"
        assert p.min_severity == Severity.HIGH
        assert p.output_format == "json"
        assert p.fail_threshold == 1

    def test_development_profile(self):
        p = PROFILE_DEVELOPMENT
        assert p.name == "development"
        assert p.min_severity == Severity.HIGH
        assert len(p.disabled_rules) > 0
        assert "MCP-001" in p.disabled_rules

    def test_owasp_agentic_profile(self):
        p = PROFILE_OWASP_AGENTIC
        assert p.name == "owasp-agentic"
        assert p.enabled_rules is not None
        assert "CMD-001" in p.enabled_rules
        assert "SQL-001" in p.enabled_rules
        assert "MCP-004" in p.enabled_rules

    def test_mcp_only_profile(self):
        p = PROFILE_MCP_ONLY
        assert p.name == "mcp-only"
        assert p.enabled_rules is not None
        assert all(r.startswith("MCP-") for r in p.enabled_rules)

    def test_to_dict(self):
        p = PROFILE_STRICT
        d = p.to_dict()
        assert d["name"] == "strict"
        assert d["min_severity"] == "LOW"
        assert d["fail_threshold"] == 1
        assert d["output_format"] == "text"

    def test_to_dict_with_enabled_rules(self):
        p = PROFILE_MCP_ONLY
        d = p.to_dict()
        assert isinstance(d["enabled_rules"], list)
        assert len(d["enabled_rules"]) > 0

    def test_frozen(self):
        p = PROFILE_STRICT
        with pytest.raises(AttributeError):
            p.name = "modified"


# ===========================================================================
# get_profile
# ===========================================================================

class TestGetProfile:
    """Tests for get_profile()."""

    def test_get_strict(self):
        p = get_profile("strict")
        assert p.name == "strict"

    def test_get_standard(self):
        p = get_profile("standard")
        assert p.name == "standard"

    def test_get_ci(self):
        p = get_profile("ci")
        assert p.name == "ci"

    def test_get_development(self):
        p = get_profile("development")
        assert p.name == "development"

    def test_get_owasp_agentic(self):
        p = get_profile("owasp-agentic")
        assert p.name == "owasp-agentic"

    def test_get_mcp_only(self):
        p = get_profile("mcp-only")
        assert p.name == "mcp-only"

    def test_case_insensitive(self):
        p = get_profile("STRICT")
        assert p.name == "strict"

    def test_with_whitespace(self):
        p = get_profile("  ci  ")
        assert p.name == "ci"

    def test_invalid_name_raises(self):
        with pytest.raises(ValueError) as exc_info:
            get_profile("nonexistent")
        assert "Unknown profile" in str(exc_info.value)
        assert "Available" in str(exc_info.value)

    def test_empty_name_raises(self):
        with pytest.raises(ValueError):
            get_profile("")


# ===========================================================================
# list_profiles
# ===========================================================================

class TestListProfiles:
    """Tests for list_profiles()."""

    def test_returns_list(self):
        profiles = list_profiles()
        assert isinstance(profiles, list)

    def test_has_six_profiles(self):
        profiles = list_profiles()
        assert len(profiles) == 6

    def test_sorted_by_name(self):
        profiles = list_profiles()
        names = [p.name for p in profiles]
        assert names == sorted(names)

    def test_all_are_compliance_profile(self):
        for p in list_profiles():
            assert isinstance(p, ComplianceProfile)


# ===========================================================================
# PROFILE_NAMES
# ===========================================================================

class TestProfileNames:
    """Tests for PROFILE_NAMES list."""

    def test_sorted(self):
        assert PROFILE_NAMES == sorted(PROFILE_NAMES)

    def test_contains_all(self):
        assert "strict" in PROFILE_NAMES
        assert "standard" in PROFILE_NAMES
        assert "ci" in PROFILE_NAMES
        assert "development" in PROFILE_NAMES
        assert "owasp-agentic" in PROFILE_NAMES
        assert "mcp-only" in PROFILE_NAMES


# ===========================================================================
# format_profiles_text
# ===========================================================================

class TestFormatProfilesText:
    """Tests for format_profiles_text()."""

    def test_header(self):
        output = format_profiles_text()
        assert "Compliance Profiles" in output

    def test_all_profiles_listed(self):
        output = format_profiles_text()
        for name in PROFILE_NAMES:
            assert name in output

    def test_shows_severity(self):
        output = format_profiles_text()
        assert "Min severity:" in output

    def test_shows_fail_threshold(self):
        output = format_profiles_text()
        assert "Fail threshold:" in output

    def test_shows_descriptions(self):
        output = format_profiles_text()
        assert "All rules enabled" in output
        assert "Production rules" in output
        assert "Fast scan" in output


# ===========================================================================
# apply_profile_to_rules
# ===========================================================================

class TestApplyProfileToRules:
    """Tests for apply_profile_to_rules()."""

    def test_strict_keeps_all(self):
        rules = get_all_rules()
        filtered = apply_profile_to_rules(PROFILE_STRICT, rules)
        assert len(filtered) == len(rules)

    def test_standard_keeps_all(self):
        rules = get_all_rules()
        filtered = apply_profile_to_rules(PROFILE_STANDARD, rules)
        assert len(filtered) == len(rules)

    def test_development_removes_mcp_rules(self):
        rules = get_all_rules()
        filtered = apply_profile_to_rules(PROFILE_DEVELOPMENT, rules)
        rule_ids = {r.rule_id for r in filtered}
        assert "MCP-001" not in rule_ids
        assert "MCP-005" not in rule_ids
        assert "MCP-019" not in rule_ids
        # Non-MCP rules should remain
        assert "CMD-001" in rule_ids
        assert "SQL-001" in rule_ids

    def test_mcp_only_filters(self):
        rules = get_all_rules()
        filtered = apply_profile_to_rules(PROFILE_MCP_ONLY, rules)
        for r in filtered:
            assert r.rule_id.startswith("MCP-")

    def test_owasp_agentic_subset(self):
        rules = get_all_rules()
        filtered = apply_profile_to_rules(PROFILE_OWASP_AGENTIC, rules)
        rule_ids = {r.rule_id for r in filtered}
        # These should be in the OWASP set
        assert "CMD-001" in rule_ids
        assert "SQL-001" in rule_ids
        assert "PATH-001" in rule_ids
        # This should NOT be (not an OWASP category rule)
        assert "CRYPTO-001" not in rule_ids

    def test_empty_rules_list(self):
        filtered = apply_profile_to_rules(PROFILE_STRICT, [])
        assert len(filtered) == 0

    def test_ci_profile_keeps_all_rules(self):
        """CI profile keeps all rules (filtering is by severity, not rule list)."""
        rules = get_all_rules()
        filtered = apply_profile_to_rules(PROFILE_CI, rules)
        assert len(filtered) == len(rules)


# ===========================================================================
# apply_profile_severity_filter
# ===========================================================================

class TestApplyProfileSeverityFilter:
    """Tests for apply_profile_severity_filter()."""

    def _make_findings(self) -> list[Finding]:
        return [
            Finding(rule_id="A", severity=Severity.CRITICAL, title="", description="", file="", line=1),
            Finding(rule_id="B", severity=Severity.HIGH, title="", description="", file="", line=2),
            Finding(rule_id="C", severity=Severity.MEDIUM, title="", description="", file="", line=3),
            Finding(rule_id="D", severity=Severity.LOW, title="", description="", file="", line=4),
            Finding(rule_id="E", severity=Severity.INFO, title="", description="", file="", line=5),
        ]

    def test_strict_keeps_low_and_above(self):
        findings = self._make_findings()
        filtered = apply_profile_severity_filter(PROFILE_STRICT, findings)
        # Strict profile min_severity=LOW, so INFO is excluded
        assert len(filtered) == 4
        severities = {f.severity for f in filtered}
        assert Severity.INFO not in severities
        assert Severity.LOW in severities

    def test_standard_filters_low_and_info(self):
        findings = self._make_findings()
        filtered = apply_profile_severity_filter(PROFILE_STANDARD, findings)
        severities = {f.severity for f in filtered}
        assert Severity.CRITICAL in severities
        assert Severity.HIGH in severities
        assert Severity.MEDIUM in severities
        assert Severity.LOW not in severities
        assert Severity.INFO not in severities

    def test_ci_filters_below_high(self):
        findings = self._make_findings()
        filtered = apply_profile_severity_filter(PROFILE_CI, findings)
        severities = {f.severity for f in filtered}
        assert Severity.CRITICAL in severities
        assert Severity.HIGH in severities
        assert Severity.MEDIUM not in severities
        assert Severity.LOW not in severities

    def test_development_filters_below_high(self):
        findings = self._make_findings()
        filtered = apply_profile_severity_filter(PROFILE_DEVELOPMENT, findings)
        severities = {f.severity for f in filtered}
        assert Severity.CRITICAL in severities
        assert Severity.HIGH in severities
        assert Severity.MEDIUM not in severities

    def test_empty_findings(self):
        filtered = apply_profile_severity_filter(PROFILE_STRICT, [])
        assert len(filtered) == 0

    def test_all_info_with_high_threshold(self):
        findings = [
            Finding(rule_id="X", severity=Severity.INFO, title="", description="", file="", line=1),
        ]
        filtered = apply_profile_severity_filter(PROFILE_CI, findings)
        assert len(filtered) == 0


# ===========================================================================
# check_fail_threshold
# ===========================================================================

class TestCheckFailThreshold:
    """Tests for check_fail_threshold()."""

    def test_strict_one_finding_fails(self):
        assert check_fail_threshold(PROFILE_STRICT, 1) is True

    def test_strict_zero_findings_passes(self):
        assert check_fail_threshold(PROFILE_STRICT, 0) is False

    def test_standard_two_findings_passes(self):
        assert check_fail_threshold(PROFILE_STANDARD, 2) is False

    def test_standard_three_findings_fails(self):
        assert check_fail_threshold(PROFILE_STANDARD, 3) is True

    def test_development_four_findings_passes(self):
        assert check_fail_threshold(PROFILE_DEVELOPMENT, 4) is False

    def test_development_five_findings_fails(self):
        assert check_fail_threshold(PROFILE_DEVELOPMENT, 5) is True

    def test_ci_one_finding_fails(self):
        assert check_fail_threshold(PROFILE_CI, 1) is True

    def test_ci_zero_passes(self):
        assert check_fail_threshold(PROFILE_CI, 0) is False


# ===========================================================================
# CLI integration
# ===========================================================================

class TestProfileCLI:
    """Tests for --profile and --list-profiles CLI flags."""

    def test_profile_flag_registered(self):
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["test.py", "--profile", "strict"])
        assert args.profile == "strict"

    def test_profile_ci_registered(self):
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["test.py", "--profile", "ci"])
        assert args.profile == "ci"

    def test_list_profiles_flag_registered(self):
        from mcpaudit.__main__ import build_parser
        parser = build_parser()
        args = parser.parse_args(["--list-profiles"])
        assert args.list_profiles is True

    def test_list_profiles_output(self, capsys):
        from mcpaudit.__main__ import main
        exit_code = main(["--list-profiles"])
        assert exit_code == 0
        captured = capsys.readouterr()
        assert "Compliance Profiles" in captured.out
        assert "strict" in captured.out
        assert "ci" in captured.out

    def test_profile_strict_scan(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "strict"])
        assert exit_code == 0

    def test_profile_ci_scan_json(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "ci"])
        captured = capsys.readouterr()
        # CI profile defaults to JSON output
        assert "{" in captured.out or exit_code == 0

    def test_profile_development_no_mcp(self, tmp_path: Path, capsys):
        """Development profile should not include MCP rules."""
        f = tmp_path / "test.py"
        f.write_text('app = Server("test")\n')
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "development"])
        # MCP rules should be disabled, so no MCP findings
        assert exit_code >= 0

    def test_profile_mcp_only(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "mcp-only"])
        assert exit_code == 0

    def test_profile_with_severity_override(self, tmp_path: Path, capsys):
        """CLI --severity should override profile severity."""
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "ci", "--severity", "low"])
        assert exit_code == 0

    def test_profile_with_exclude(self, tmp_path: Path, capsys):
        """CLI --exclude should work with profiles."""
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "strict", "--exclude", "CMD-001"])
        assert exit_code == 0

    def test_profile_owasp_agentic_scan(self, tmp_path: Path, capsys):
        f = tmp_path / "test.py"
        f.write_text("x = 1\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "owasp-agentic"])
        assert exit_code == 0

    def test_profile_strict_fails_on_finding(self, tmp_path: Path, capsys):
        """Strict profile should return non-zero on any finding."""
        f = tmp_path / "test.py"
        f.write_text("import hashlib\nh = hashlib.md5(data)\n")
        from mcpaudit.__main__ import main
        exit_code = main([str(f), "--profile", "strict"])
        assert exit_code >= 1


# ===========================================================================
# Profile registry
# ===========================================================================

class TestProfileRegistry:
    """Tests for the PROFILES registry dict."""

    def test_has_six_entries(self):
        assert len(PROFILES) == 6

    def test_keys_match_names(self):
        for key, profile in PROFILES.items():
            assert key == profile.name

    def test_all_profiles_are_frozen(self):
        for profile in PROFILES.values():
            with pytest.raises(AttributeError):
                profile.name = "hacked"
