"""Tests for the Finding Deduplicator module."""

from __future__ import annotations

import json

import pytest

from mcpaudit.finding_deduplicator import (
    DedupGroup,
    DedupStats,
    FindingDeduplicator,
    FindingFingerprint,
    _higher_severity,
)


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

def _finding(
    rule_id: str = "SEC-001",
    severity: str = "HIGH",
    file: str = "server.py",
    line: int = 10,
    code_snippet: str = "",
    title: str = "test",
) -> dict:
    result = {
        "rule_id": rule_id,
        "severity": severity,
        "file": file,
        "line": line,
        "title": title,
    }
    if code_snippet:
        result["code_snippet"] = code_snippet
    return result


# ===========================================================================
# FindingFingerprint
# ===========================================================================

class TestFindingFingerprint:
    def test_hash_deterministic(self):
        fp1 = FindingFingerprint("SEC-001", "server.py", 10)
        fp2 = FindingFingerprint("SEC-001", "server.py", 10)
        assert fp1.hash == fp2.hash

    def test_hash_different_for_different_inputs(self):
        fp1 = FindingFingerprint("SEC-001", "server.py", 10)
        fp2 = FindingFingerprint("SEC-001", "server.py", 11)
        assert fp1.hash != fp2.hash

    def test_hash_length(self):
        fp = FindingFingerprint("SEC-001", "server.py", 10)
        assert len(fp.hash) == 16


# ===========================================================================
# _higher_severity
# ===========================================================================

class TestHigherSeverity:
    def test_critical_vs_low(self):
        assert _higher_severity("CRITICAL", "LOW") == "CRITICAL"

    def test_low_vs_critical(self):
        assert _higher_severity("LOW", "CRITICAL") == "CRITICAL"

    def test_same(self):
        assert _higher_severity("HIGH", "HIGH") == "HIGH"

    def test_unknown(self):
        assert _higher_severity("UNKNOWN", "HIGH") == "HIGH"


# ===========================================================================
# DedupStats
# ===========================================================================

class TestDedupStats:
    def test_defaults(self):
        s = DedupStats()
        assert s.compression_ratio == 1.0
        assert s.dupes_removed == 0

    def test_compression_ratio(self):
        s = DedupStats(total_input=10, unique_output=5)
        assert s.compression_ratio == 0.5

    def test_dupes_removed(self):
        s = DedupStats(total_input=10, unique_output=7)
        assert s.dupes_removed == 3

    def test_to_dict(self):
        s = DedupStats(total_input=10, unique_output=8, exact_dupes=2)
        d = s.to_dict()
        assert d["total_input"] == 10
        assert d["exact_dupes"] == 2


# ===========================================================================
# DedupGroup
# ===========================================================================

class TestDedupGroup:
    def test_count_single(self):
        g = DedupGroup(canonical=_finding())
        assert g.count == 1

    def test_count_with_dupes(self):
        g = DedupGroup(canonical=_finding(), duplicates=[_finding(), _finding()])
        assert g.count == 3

    def test_files(self):
        g = DedupGroup(
            canonical=_finding(file="a.py"),
            duplicates=[_finding(file="b.py"), _finding(file="a.py")],
        )
        assert g.files == ["a.py", "b.py"]


# ===========================================================================
# FindingDeduplicator - exact dedup
# ===========================================================================

class TestExactDedup:
    def test_no_dupes(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", line=1),
            _finding(rule_id="A-002", line=2),
        ]
        result, removed = d.exact_dedup(findings)
        assert len(result) == 2
        assert removed == 0

    def test_exact_duplicate(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="A-001", line=10),
        ]
        result, removed = d.exact_dedup(findings)
        assert len(result) == 1
        assert removed == 1

    def test_keeps_higher_severity(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", line=10, severity="LOW"),
            _finding(rule_id="A-001", line=10, severity="CRITICAL"),
        ]
        result, removed = d.exact_dedup(findings)
        assert result[0]["severity"] == "CRITICAL"

    def test_empty(self):
        d = FindingDeduplicator()
        result, removed = d.exact_dedup([])
        assert result == []
        assert removed == 0


# ===========================================================================
# FindingDeduplicator - fuzzy dedup
# ===========================================================================

class TestFuzzyDedup:
    def test_nearby_lines(self):
        d = FindingDeduplicator(fuzzy_line_threshold=5)
        findings = [
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="A-001", line=12),
        ]
        result, removed = d.fuzzy_dedup(findings)
        assert len(result) == 1
        assert removed == 1

    def test_far_lines(self):
        d = FindingDeduplicator(fuzzy_line_threshold=5)
        findings = [
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="A-001", line=100),
        ]
        result, removed = d.fuzzy_dedup(findings)
        assert len(result) == 2
        assert removed == 0

    def test_different_rules(self):
        d = FindingDeduplicator(fuzzy_line_threshold=5)
        findings = [
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="B-001", line=11),
        ]
        result, removed = d.fuzzy_dedup(findings)
        assert len(result) == 2

    def test_different_files(self):
        d = FindingDeduplicator(fuzzy_line_threshold=5)
        findings = [
            _finding(rule_id="A-001", file="a.py", line=10),
            _finding(rule_id="A-001", file="b.py", line=10),
        ]
        result, removed = d.fuzzy_dedup(findings)
        assert len(result) == 2

    def test_empty(self):
        d = FindingDeduplicator()
        result, removed = d.fuzzy_dedup([])
        assert result == []
        assert removed == 0


# ===========================================================================
# FindingDeduplicator - cross-file dedup
# ===========================================================================

class TestCrossFileDedup:
    def test_same_snippet_different_files(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", file="a.py", code_snippet="subprocess.run(cmd, shell=True)"),
            _finding(rule_id="A-001", file="b.py", code_snippet="subprocess.run(cmd, shell=True)"),
        ]
        result, removed = d.cross_file_dedup(findings)
        assert len(result) == 1
        assert removed == 1

    def test_different_snippets(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", file="a.py", code_snippet="subprocess.run(cmd, shell=True)"),
            _finding(rule_id="A-001", file="b.py", code_snippet="subprocess.call(cmd)"),
        ]
        result, removed = d.cross_file_dedup(findings)
        assert len(result) == 2

    def test_no_snippet_kept(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", file="a.py"),
            _finding(rule_id="A-001", file="b.py"),
        ]
        result, removed = d.cross_file_dedup(findings)
        assert len(result) == 2


# ===========================================================================
# FindingDeduplicator - full pipeline
# ===========================================================================

class TestFullDedup:
    def test_full_pipeline(self):
        d = FindingDeduplicator(fuzzy_line_threshold=3)
        findings = [
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="A-001", line=12),
            _finding(rule_id="B-001", line=20),
        ]
        result, stats = d.deduplicate(findings)
        assert stats.total_input == 4
        assert stats.unique_output <= 2
        assert stats.dupes_removed >= 2

    def test_no_dupes(self):
        d = FindingDeduplicator()
        findings = [
            _finding(rule_id="A-001", line=1),
            _finding(rule_id="B-001", line=100),
        ]
        result, stats = d.deduplicate(findings)
        assert stats.unique_output == 2
        assert stats.dupes_removed == 0

    def test_empty(self):
        d = FindingDeduplicator()
        result, stats = d.deduplicate([])
        assert stats.total_input == 0
        assert stats.unique_output == 0


# ===========================================================================
# FindingDeduplicator - groups
# ===========================================================================

class TestGroupDuplicates:
    def test_no_groups(self):
        d = FindingDeduplicator()
        groups = d.group_duplicates([_finding()])
        assert len(groups) == 1
        assert groups[0].count == 1

    def test_groups(self):
        d = FindingDeduplicator()
        groups = d.group_duplicates([
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="A-001", line=10),
            _finding(rule_id="B-001", line=20),
        ])
        assert len(groups) == 2


# ===========================================================================
# FindingDeduplicator - export
# ===========================================================================

class TestDedupExport:
    def test_to_json(self):
        d = FindingDeduplicator()
        result, stats = d.deduplicate([_finding()])
        j = d.to_json(result, stats)
        data = json.loads(j)
        assert "stats" in data
        assert "findings" in data

    def test_markdown_report(self):
        d = FindingDeduplicator()
        stats = DedupStats(total_input=10, unique_output=7, exact_dupes=2, fuzzy_dupes=1)
        report = d.markdown_report(stats)
        assert "# Deduplication Report" in report
        assert "Compression" in report
        assert "Exact" in report
