"""Tests for mcpaudit diff — Differential Scanning."""

import json
import tempfile
from pathlib import Path

from mcpaudit.diff import (
    DiffFinding,
    DiffResult,
    ScanDiff,
    ScoreDelta,
    format_diff_json,
    format_diff_sarif,
    format_diff_text,
)
from mcpaudit.models import Severity


# --- Helpers ---

def _scan_data(findings: list[dict], trust_score: int = 80, grade: str = "B") -> dict:
    """Create a mock scan result dict."""
    return {
        "summary": {
            "files_scanned": 1,
            "total_findings": len(findings),
            "trust_score": {"score": trust_score, "grade": grade},
        },
        "results": [
            {
                "file": "server.py",
                "findings": findings,
            }
        ],
    }


def _finding(rule_id: str = "CMD-001", severity: str = "HIGH",
             title: str = "Test", file: str = "server.py",
             line: int = 10, code: str = "subprocess.run(cmd)") -> dict:
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": f"Finding: {title}",
        "file": file,
        "line": line,
        "code_snippet": code,
    }


def _write_json(data: dict) -> Path:
    """Write dict to a temp JSON file."""
    f = tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False)
    json.dump(data, f)
    f.flush()
    f.close()
    return Path(f.name)


# --- Test ScanDiff basic ---

class TestScanDiffBasic:
    def test_identical_scans_no_changes(self):
        data = _scan_data([_finding()])
        differ = ScanDiff()
        result = differ.diff_data(data, data)
        assert result.total_new == 0
        assert result.total_resolved == 0
        assert result.total_modified == 0
        assert result.total_unchanged == 1

    def test_new_finding_detected(self):
        before = _scan_data([])
        after = _scan_data([_finding()])
        result = ScanDiff().diff_data(before, after)
        assert result.total_new == 1
        assert result.total_resolved == 0

    def test_resolved_finding_detected(self):
        before = _scan_data([_finding()])
        after = _scan_data([])
        result = ScanDiff().diff_data(before, after)
        assert result.total_resolved == 1
        assert result.total_new == 0

    def test_modified_severity(self):
        f1 = _finding(severity="HIGH")
        f2 = _finding(severity="CRITICAL")
        before = _scan_data([f1])
        after = _scan_data([f2])
        result = ScanDiff().diff_data(before, after)
        assert result.total_modified == 1
        assert result.modified_findings[0].previous_severity == "HIGH"

    def test_multiple_changes(self):
        before = _scan_data([
            _finding(rule_id="CMD-001", title="A"),
            _finding(rule_id="AUTH-001", title="B"),
        ])
        after = _scan_data([
            _finding(rule_id="AUTH-001", title="B"),
            _finding(rule_id="SQL-001", title="C"),
        ])
        result = ScanDiff().diff_data(before, after)
        assert result.total_new == 1  # SQL-001
        assert result.total_resolved == 1  # CMD-001

    def test_empty_scans(self):
        before = _scan_data([])
        after = _scan_data([])
        result = ScanDiff().diff_data(before, after)
        assert result.total_new == 0
        assert result.total_resolved == 0
        assert result.total_unchanged == 0


# --- Test DiffResult properties ---

class TestDiffResult:
    def test_exit_code_no_new(self):
        result = DiffResult()
        assert result.exit_code == 0

    def test_exit_code_new_high(self):
        result = DiffResult(new_findings=[
            DiffFinding(status="new", finding={"severity": "HIGH"})
        ])
        assert result.exit_code == 1

    def test_exit_code_new_critical(self):
        result = DiffResult(new_findings=[
            DiffFinding(status="new", finding={"severity": "CRITICAL"})
        ])
        assert result.exit_code == 2

    def test_to_dict_structure(self):
        result = DiffResult(file_before="a.json", file_after="b.json")
        d = result.to_dict()
        assert d["file_before"] == "a.json"
        assert "summary" in d
        assert "score_delta" in d
        assert "new_findings" in d
        assert "resolved_findings" in d

    def test_resolved_doesnt_affect_exit_code(self):
        result = DiffResult(resolved_findings=[
            DiffFinding(status="resolved", finding={"severity": "CRITICAL"})
        ])
        assert result.exit_code == 0


# --- Test ScoreDelta ---

class TestScoreDelta:
    def test_improvement(self):
        sd = ScoreDelta(before_score=60, after_score=80)
        assert sd.delta == 20
        assert sd.improved is True
        assert sd.degraded is False

    def test_degradation(self):
        sd = ScoreDelta(before_score=80, after_score=50)
        assert sd.delta == -30
        assert sd.improved is False
        assert sd.degraded is True

    def test_no_change(self):
        sd = ScoreDelta(before_score=75, after_score=75)
        assert sd.delta == 0
        assert sd.improved is False
        assert sd.degraded is False

    def test_to_dict(self):
        sd = ScoreDelta(before_score=60, after_score=80,
                        before_grade="C", after_grade="B")
        d = sd.to_dict()
        assert d["delta"] == 20
        assert d["before_grade"] == "C"
        assert d["improved"] is True

    def test_zero_scores(self):
        sd = ScoreDelta()
        assert sd.delta == 0


# --- Test file-based diff ---

class TestDiffFiles:
    def test_diff_files(self):
        before = _write_json(_scan_data([_finding()]))
        after = _write_json(_scan_data([]))
        result = ScanDiff().diff_files(before, after)
        assert result.total_resolved == 1
        assert result.file_before == str(before)

    def test_diff_files_new(self):
        before = _write_json(_scan_data([]))
        after = _write_json(_scan_data([_finding()]))
        result = ScanDiff().diff_files(before, after)
        assert result.total_new == 1

    def test_diff_files_identical(self):
        data = _scan_data([_finding()])
        f1 = _write_json(data)
        f2 = _write_json(data)
        result = ScanDiff().diff_files(f1, f2)
        assert result.total_new == 0
        assert result.total_resolved == 0


# --- Test severity delta ---

class TestSeverityDelta:
    def test_severity_delta_new_critical(self):
        before = _scan_data([])
        after = _scan_data([_finding(severity="CRITICAL")])
        result = ScanDiff().diff_data(before, after)
        assert result.severity_delta["CRITICAL"] == 1

    def test_severity_delta_resolved(self):
        before = _scan_data([_finding(severity="HIGH")])
        after = _scan_data([])
        result = ScanDiff().diff_data(before, after)
        assert result.severity_delta["HIGH"] == -1

    def test_severity_delta_unchanged(self):
        f = _finding(severity="MEDIUM")
        data = _scan_data([f])
        result = ScanDiff().diff_data(data, data)
        assert result.severity_delta["MEDIUM"] == 0


# --- Test trust score extraction ---

class TestTrustScoreExtraction:
    def test_trust_score_from_summary(self):
        before = _scan_data([], trust_score=90, grade="A")
        after = _scan_data([_finding()], trust_score=70, grade="B")
        result = ScanDiff().diff_data(before, after)
        assert result.score_delta.before_score == 90
        assert result.score_delta.after_score == 70

    def test_trust_score_estimated(self):
        before = {"results": [{"findings": []}]}
        after = {"results": [{"findings": [_finding(severity="CRITICAL")]}]}
        result = ScanDiff().diff_data(before, after)
        assert result.score_delta.before_score == 100
        assert result.score_delta.after_score < 100


# --- Test DiffFinding ---

class TestDiffFinding:
    def test_to_dict_new(self):
        df = DiffFinding(status="new", finding={"rule_id": "CMD-001"})
        d = df.to_dict()
        assert d["status"] == "new"
        assert d["finding"]["rule_id"] == "CMD-001"
        assert "previous_severity" not in d

    def test_to_dict_modified(self):
        df = DiffFinding(status="modified",
                         finding={"rule_id": "CMD-001", "severity": "CRITICAL"},
                         previous_severity="HIGH")
        d = df.to_dict()
        assert d["previous_severity"] == "HIGH"


# --- Test extract_findings from various formats ---

class TestExtractFindings:
    def test_standard_format(self):
        data = _scan_data([_finding()])
        differ = ScanDiff()
        findings = differ._extract_findings(data)
        assert len(findings) == 1

    def test_baseline_format(self):
        data = {"findings": [{"rule_id": "CMD-001", "fingerprint": "abc"}]}
        differ = ScanDiff()
        findings = differ._extract_findings(data)
        assert len(findings) == 1

    def test_empty_format(self):
        data = {}
        differ = ScanDiff()
        findings = differ._extract_findings(data)
        assert len(findings) == 0

    def test_guard_output_format(self):
        data = {"findings": [{"rule_id": "GUARD-001", "severity": "CRITICAL"}]}
        differ = ScanDiff()
        findings = differ._extract_findings(data)
        assert len(findings) == 1


# --- Test Formatting ---

class TestDiffFormatting:
    def test_format_text_empty(self):
        result = DiffResult()
        text = format_diff_text(result)
        assert "Scan Comparison" in text

    def test_format_text_with_new(self):
        result = DiffResult(new_findings=[
            DiffFinding(status="new", finding=_finding())
        ])
        text = format_diff_text(result)
        assert "NEW FINDINGS" in text
        assert "CMD-001" in text

    def test_format_text_with_resolved(self):
        result = DiffResult(resolved_findings=[
            DiffFinding(status="resolved", finding=_finding())
        ])
        text = format_diff_text(result)
        assert "RESOLVED" in text

    def test_format_text_with_modified(self):
        result = DiffResult(modified_findings=[
            DiffFinding(status="modified", finding=_finding(severity="CRITICAL"),
                        previous_severity="HIGH")
        ])
        text = format_diff_text(result)
        assert "MODIFIED" in text

    def test_format_text_with_score(self):
        result = DiffResult(
            score_delta=ScoreDelta(before_score=80, after_score=90,
                                   before_grade="B", after_grade="A")
        )
        text = format_diff_text(result)
        assert "80" in text
        assert "90" in text

    def test_format_json(self):
        result = DiffResult(file_before="a.json", file_after="b.json")
        output = format_diff_json(result)
        data = json.loads(output)
        assert data["file_before"] == "a.json"

    def test_format_sarif_structure(self):
        result = DiffResult(new_findings=[
            DiffFinding(status="new", finding=_finding(severity="HIGH"))
        ])
        sarif = format_diff_sarif(result)
        data = json.loads(sarif)
        assert data["version"] == "2.1.0"
        assert len(data["runs"][0]["results"]) == 1

    def test_format_sarif_empty(self):
        result = DiffResult()
        sarif = format_diff_sarif(result)
        data = json.loads(sarif)
        assert len(data["runs"][0]["results"]) == 0
