"""Tests for the Rule Impact Analyzer (mcpaudit.rule_impact).

Tests per-rule hit counts, false positive estimation, effectiveness
scoring, recommendations, and output formatting.
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.rule_impact import (
    ImpactReport,
    RuleHit,
    RuleImpact,
    analyze_rule_impact,
    analyze_rule_impact_from_file,
    analyze_rule_impact_from_scan,
    compute_effectiveness,
    estimate_fp_rate,
    format_impact_json,
    format_impact_text,
    recommend_action,
)


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

def _make_finding(
    rule_id: str = "CMD-001",
    severity: str = "MEDIUM",
    file: str = "server.py",
    line: int = 10,
    title: str = "Test",
) -> dict:
    return {
        "rule_id": rule_id,
        "severity": severity,
        "file": file,
        "line": line,
        "title": title,
    }


def _make_findings_batch(
    rule_id: str = "CMD-001",
    severity: str = "MEDIUM",
    count: int = 5,
    file_prefix: str = "src/",
) -> list[dict]:
    return [
        _make_finding(
            rule_id=rule_id,
            severity=severity,
            file=f"{file_prefix}file_{i}.py",
            line=i * 10 + 1,
        )
        for i in range(count)
    ]


# ---------------------------------------------------------------------------
# RuleHit
# ---------------------------------------------------------------------------

class TestRuleHit:
    def test_to_dict(self):
        h = RuleHit("CMD-001", "server.py", 42, "HIGH", "Test")
        d = h.to_dict()
        assert d["rule_id"] == "CMD-001"
        assert d["file"] == "server.py"
        assert d["line"] == 42
        assert d["severity"] == "HIGH"

    def test_defaults(self):
        h = RuleHit("X-001", "f.py", 1)
        assert h.severity == "INFO"
        assert h.title == ""


# ---------------------------------------------------------------------------
# RuleImpact
# ---------------------------------------------------------------------------

class TestRuleImpact:
    def test_to_dict(self):
        ri = RuleImpact(
            rule_id="CMD-001", severity="HIGH",
            hit_count=10, file_count=3, total_loc=1000,
            effectiveness=0.01, fp_rate=0.15,
            recommendation="tune", reason="test",
            files_affected=["a.py", "b.py"],
            impact_score=42.5,
        )
        d = ri.to_dict()
        assert d["rule_id"] == "CMD-001"
        assert d["hit_count"] == 10
        assert d["recommendation"] == "tune"
        assert d["effectiveness"] == 0.01
        assert d["fp_rate"] == 0.15

    def test_files_limited_to_ten(self):
        files = [f"file_{i}.py" for i in range(20)]
        ri = RuleImpact(
            rule_id="X", severity="LOW",
            files_affected=files,
        )
        d = ri.to_dict()
        assert len(d["files_affected"]) == 10


# ---------------------------------------------------------------------------
# ImpactReport
# ---------------------------------------------------------------------------

class TestImpactReport:
    def test_to_dict(self):
        report = ImpactReport(
            total_rules=3, total_findings=10, total_loc=5000,
            rules_to_tune=["A-001"],
            rules_to_disable=["B-001"],
            rules_to_keep=["C-001"],
        )
        d = report.to_dict()
        assert d["total_rules"] == 3
        assert d["rules_to_tune"] == ["A-001"]

    def test_empty_report(self):
        report = ImpactReport()
        d = report.to_dict()
        assert d["total_rules"] == 0
        assert d["rules"] == []


# ---------------------------------------------------------------------------
# compute_effectiveness
# ---------------------------------------------------------------------------

class TestComputeEffectiveness:
    def test_zero_loc(self):
        assert compute_effectiveness(10, 0) == 0.0

    def test_basic(self):
        eff = compute_effectiveness(10, 1000)
        assert abs(eff - 0.01) < 0.001

    def test_high_ratio(self):
        eff = compute_effectiveness(100, 100)
        assert abs(eff - 1.0) < 0.001

    def test_zero_hits(self):
        assert compute_effectiveness(0, 1000) == 0.0


# ---------------------------------------------------------------------------
# estimate_fp_rate
# ---------------------------------------------------------------------------

class TestEstimateFpRate:
    def test_no_hits(self):
        assert estimate_fp_rate([]) == 0.0

    def test_info_severity_higher_fp(self):
        hits = [RuleHit("X", "src/main.py", 1, "INFO")]
        rate = estimate_fp_rate(hits, "INFO")
        assert rate > 0

    def test_test_files_increase_fp(self):
        hits = [
            RuleHit("X", "tests/test_main.py", 1, "HIGH"),
            RuleHit("X", "tests/test_other.py", 2, "HIGH"),
        ]
        rate = estimate_fp_rate(hits, "HIGH")
        assert rate > 0.05  # Above base rate

    def test_vendor_files_increase_fp(self):
        hits = [
            RuleHit("X", "vendor/lib.py", 1, "MEDIUM"),
            RuleHit("X", "vendor/other.py", 2, "MEDIUM"),
        ]
        rate = estimate_fp_rate(hits, "MEDIUM")
        assert rate > 0.05

    def test_production_code_low_fp(self):
        hits = [RuleHit("X", "src/core.py", 1, "CRITICAL")]
        rate = estimate_fp_rate(hits, "CRITICAL")
        assert rate <= 0.10

    def test_generated_code_high_fp(self):
        hits = [
            RuleHit("X", "generated/api.py", 1, "LOW"),
            RuleHit("X", "generated/models.py", 2, "LOW"),
        ]
        rate = estimate_fp_rate(hits, "LOW")
        assert rate > 0.05

    def test_mixed_paths(self):
        hits = [
            RuleHit("X", "src/main.py", 1),
            RuleHit("X", "tests/test_main.py", 2),
        ]
        rate = estimate_fp_rate(hits)
        assert 0 <= rate <= 1.0

    def test_rate_bounded(self):
        hits = [RuleHit("X", "vendor/generated/test_auto_gen.py", i)
                for i in range(10)]
        rate = estimate_fp_rate(hits, "INFO")
        assert 0.0 <= rate <= 1.0


# ---------------------------------------------------------------------------
# recommend_action
# ---------------------------------------------------------------------------

class TestRecommendAction:
    def test_no_hits_keep(self):
        rec, reason = recommend_action(0, 0.0, 0.0, "HIGH")
        assert rec == "keep"
        assert "No findings" in reason

    def test_critical_always_kept(self):
        rec, _ = recommend_action(5, 0.001, 0.1, "CRITICAL")
        assert rec == "keep"

    def test_critical_with_high_fp_tuned(self):
        rec, _ = recommend_action(5, 0.001, 0.35, "CRITICAL")
        assert rec == "tune"

    def test_very_high_fp_disable(self):
        rec, _ = recommend_action(10, 0.01, 0.60, "MEDIUM")
        assert rec == "disable"

    def test_noisy_rule_disable(self):
        rec, _ = recommend_action(100, 0.15, 0.05, "LOW")
        assert rec == "disable"

    def test_moderate_noise_low_severity_tune(self):
        rec, _ = recommend_action(50, 0.06, 0.1, "LOW")
        assert rec == "tune"

    def test_moderate_fp_tune(self):
        rec, _ = recommend_action(5, 0.001, 0.35, "HIGH")
        assert rec == "tune"

    def test_high_severity_keep(self):
        rec, _ = recommend_action(3, 0.001, 0.05, "HIGH")
        assert rec == "keep"

    def test_normal_keep(self):
        rec, _ = recommend_action(2, 0.001, 0.05, "MEDIUM")
        assert rec == "keep"


# ---------------------------------------------------------------------------
# analyze_rule_impact
# ---------------------------------------------------------------------------

class TestAnalyzeRuleImpact:
    def test_empty_findings(self):
        report = analyze_rule_impact([])
        assert report.total_rules == 0
        assert report.total_findings == 0

    def test_single_rule(self):
        findings = _make_findings_batch("CMD-001", "HIGH", 3)
        report = analyze_rule_impact(findings, total_loc=1000)
        assert report.total_rules == 1
        assert report.total_findings == 3
        assert report.rules[0].rule_id == "CMD-001"
        assert report.rules[0].hit_count == 3

    def test_multiple_rules(self):
        findings = (
            _make_findings_batch("CMD-001", "HIGH", 3) +
            _make_findings_batch("AUTH-001", "CRITICAL", 2)
        )
        report = analyze_rule_impact(findings, total_loc=5000)
        assert report.total_rules == 2
        assert report.total_findings == 5

    def test_effectiveness_computed(self):
        findings = _make_findings_batch("X-001", "HIGH", 10)
        report = analyze_rule_impact(findings, total_loc=1000)
        rule = report.rules[0]
        assert abs(rule.effectiveness - 0.01) < 0.001

    def test_file_count(self):
        findings = [
            _make_finding("X-001", file="a.py"),
            _make_finding("X-001", file="b.py"),
            _make_finding("X-001", file="a.py", line=20),
        ]
        report = analyze_rule_impact(findings)
        assert report.rules[0].file_count == 2

    def test_recommendations_categorized(self):
        findings = _make_findings_batch("CMD-001", "HIGH", 5)
        report = analyze_rule_impact(findings, total_loc=10000)
        assert "CMD-001" in report.rules_to_keep

    def test_top_noisy(self):
        findings = _make_findings_batch("NOISY-001", "INFO", 20)
        report = analyze_rule_impact(findings, total_loc=100)
        assert "NOISY-001" in report.top_noisy

    def test_top_effective(self):
        findings = _make_findings_batch("EFF-001", "CRITICAL", 5)
        report = analyze_rule_impact(findings, total_loc=10000)
        assert report.top_effective  # Non-empty
        assert "EFF-001" in report.top_effective

    def test_sorted_by_impact(self):
        findings = (
            _make_findings_batch("LOW-001", "LOW", 1) +
            _make_findings_batch("CRIT-001", "CRITICAL", 3)
        )
        report = analyze_rule_impact(findings, total_loc=5000)
        # Critical should have higher impact score
        assert report.rules[0].rule_id == "CRIT-001"

    def test_zero_loc(self):
        findings = _make_findings_batch("X-001", "HIGH", 5)
        report = analyze_rule_impact(findings, total_loc=0)
        assert report.rules[0].effectiveness == 0.0

    def test_total_loc_tracked(self):
        report = analyze_rule_impact([], total_loc=5000)
        assert report.total_loc == 5000


# ---------------------------------------------------------------------------
# analyze_rule_impact_from_scan
# ---------------------------------------------------------------------------

class TestAnalyzeFromScan:
    def test_empty_scan(self):
        report = analyze_rule_impact_from_scan({})
        assert report.total_findings == 0

    def test_with_findings(self):
        scan = {
            "results": [{
                "findings": [_make_finding("A-001", "HIGH")],
                "lines_scanned": 500,
            }],
        }
        report = analyze_rule_impact_from_scan(scan)
        assert report.total_findings == 1
        assert report.total_loc == 500

    def test_multiple_results(self):
        scan = {
            "results": [
                {"findings": [_make_finding("A-001")], "lines_scanned": 200},
                {"findings": [_make_finding("B-001")], "lines_scanned": 300},
            ],
        }
        report = analyze_rule_impact_from_scan(scan)
        assert report.total_findings == 2
        assert report.total_loc == 500

    def test_summary_loc_fallback(self):
        scan = {
            "summary": {"lines_scanned": 1000},
            "results": [{"findings": [_make_finding()]}],
        }
        report = analyze_rule_impact_from_scan(scan)
        assert report.total_loc == 1000


# ---------------------------------------------------------------------------
# analyze_rule_impact_from_file
# ---------------------------------------------------------------------------

class TestAnalyzeFromFile:
    def test_valid_file(self):
        scan = {
            "results": [{
                "findings": [_make_finding("X-001", "HIGH")],
                "lines_scanned": 500,
            }],
        }
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(scan, f)
            f.flush()
            report = analyze_rule_impact_from_file(f.name)
        assert report.total_findings == 1

    def test_nonexistent_file(self):
        with pytest.raises(FileNotFoundError):
            analyze_rule_impact_from_file("/tmp/nonexistent_impact_8888.json")


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

class TestFormatText:
    def test_basic(self):
        findings = _make_findings_batch("CMD-001", "HIGH", 3)
        report = analyze_rule_impact(findings, total_loc=1000)
        text = format_impact_text(report)
        assert "Rule Impact Analysis" in text
        assert "CMD-001" in text
        assert "Rules analyzed: 1" in text

    def test_with_recommendations(self):
        findings = _make_findings_batch("NOISY-001", "INFO", 100)
        report = analyze_rule_impact(findings, total_loc=200)
        text = format_impact_text(report)
        assert "DISABLE" in text or "TUNE" in text

    def test_empty(self):
        report = analyze_rule_impact([])
        text = format_impact_text(report)
        assert "Rules analyzed: 0" in text

    def test_loc_displayed(self):
        report = analyze_rule_impact(
            _make_findings_batch("X-001", "HIGH", 1),
            total_loc=5000,
        )
        text = format_impact_text(report)
        assert "5,000" in text


class TestFormatJson:
    def test_valid_json(self):
        findings = _make_findings_batch("CMD-001", "HIGH", 3)
        report = analyze_rule_impact(findings, total_loc=1000)
        j = format_impact_json(report)
        parsed = json.loads(j)
        assert parsed["total_rules"] == 1
        assert parsed["total_findings"] == 3

    def test_roundtrip(self):
        findings = (
            _make_findings_batch("A-001", "CRITICAL", 2) +
            _make_findings_batch("B-001", "LOW", 5)
        )
        report = analyze_rule_impact(findings, total_loc=10000)
        j = format_impact_json(report)
        parsed = json.loads(j)
        assert len(parsed["rules"]) == 2
        assert "rules_to_keep" in parsed

    def test_empty_report(self):
        report = analyze_rule_impact([])
        j = format_impact_json(report)
        parsed = json.loads(j)
        assert parsed["total_findings"] == 0
        assert parsed["rules"] == []
