"""Tests for Security Baseline Manager (v2.5.0)."""

from __future__ import annotations

import json
from datetime import datetime, timezone

import pytest

from mcpaudit.baseline_manager import (
    Baseline,
    BaselineDiff,
    BaselineEvolution,
    BaselineFinding,
    BaselineSnapshot,
    DiffFinding,
    ScanFinding,
    ScanResult,
    compare_to_baseline,
    compute_fingerprint,
    create_baseline,
    filter_baseline,
    format_baseline_json,
    format_baseline_text,
    format_diff_json,
    format_diff_text,
    format_evolution_json,
    format_evolution_text,
    merge_baselines,
    update_baseline,
)


# --- Helper Factories ---

def _make_scan_finding(rule_id: str = "CMD-001", severity: str = "HIGH",
                       title: str = "Test", file: str = "server.py",
                       code: str = "subprocess.run(cmd)") -> ScanFinding:
    fp = compute_fingerprint(rule_id, file, code)
    return ScanFinding(
        fingerprint=fp,
        rule_id=rule_id,
        severity=severity,
        title=title,
        file=file,
    )


def _make_scan_result(findings: list[ScanFinding] | None = None) -> ScanResult:
    if findings is None:
        findings = [_make_scan_finding()]
    return ScanResult(findings=findings)


def _make_baseline(findings: list[BaselineFinding] | None = None,
                   label: str = "test") -> Baseline:
    if findings is None:
        sf = _make_scan_finding()
        findings = [BaselineFinding(
            fingerprint=sf.fingerprint,
            rule_id=sf.rule_id,
            severity=sf.severity,
            title=sf.title,
            file=sf.file,
        )]
    return Baseline(label=label, findings=findings)


# --- Data Models ---

class TestBaselineFinding:
    def test_to_dict(self):
        f = BaselineFinding("abc123", "CMD-001", "HIGH", "Test", "server.py", "injection")
        d = f.to_dict()
        assert d["fingerprint"] == "abc123"
        assert d["rule_id"] == "CMD-001"
        assert d["severity"] == "HIGH"

    def test_from_dict(self):
        d = {"fingerprint": "abc", "rule_id": "X", "severity": "LOW", "title": "T"}
        f = BaselineFinding.from_dict(d)
        assert f.fingerprint == "abc"
        assert f.rule_id == "X"

    def test_from_dict_defaults(self):
        f = BaselineFinding.from_dict({})
        assert f.fingerprint == ""
        assert f.severity == "MEDIUM"


class TestBaseline:
    def test_empty(self):
        b = Baseline()
        assert b.count == 0
        assert b.fingerprints == set()
        assert b.version == 2

    def test_count(self):
        b = _make_baseline()
        assert b.count == 1

    def test_fingerprints(self):
        b = _make_baseline()
        assert len(b.fingerprints) == 1

    def test_severity_counts(self):
        b = Baseline(findings=[
            BaselineFinding("a", "X", "HIGH", "T1"),
            BaselineFinding("b", "Y", "HIGH", "T2"),
            BaselineFinding("c", "Z", "MEDIUM", "T3"),
        ])
        counts = b.severity_counts
        assert counts["HIGH"] == 2
        assert counts["MEDIUM"] == 1

    def test_to_dict(self):
        b = _make_baseline()
        d = b.to_dict()
        assert d["version"] == 2
        assert d["count"] == 1
        assert "findings" in d
        assert "created_at" in d

    def test_to_json(self):
        b = _make_baseline()
        text = b.to_json()
        data = json.loads(text)
        assert data["count"] == 1

    def test_from_dict(self):
        d = {"version": 2, "label": "test", "findings": [
            {"fingerprint": "a", "rule_id": "X", "severity": "HIGH", "title": "T"},
        ]}
        b = Baseline.from_dict(d)
        assert b.count == 1
        assert b.label == "test"

    def test_from_json(self):
        text = json.dumps({"version": 2, "findings": [
            {"fingerprint": "a", "rule_id": "X", "severity": "HIGH", "title": "T"},
        ]})
        b = Baseline.from_json(text)
        assert b.count == 1

    def test_roundtrip(self):
        b = _make_baseline()
        text = b.to_json()
        b2 = Baseline.from_json(text)
        assert b.count == b2.count
        assert b.label == b2.label

    def test_created_at_set(self):
        b = Baseline()
        assert b.created_at != ""
        assert b.updated_at == b.created_at

    def test_metadata(self):
        b = Baseline(metadata={"key": "value"})
        assert b.metadata["key"] == "value"


class TestDiffFinding:
    def test_to_dict(self):
        f = DiffFinding("abc", "CMD-001", "HIGH", "Test", "server.py", "new")
        d = f.to_dict()
        assert d["status"] == "new"
        assert d["fingerprint"] == "abc"


class TestBaselineDiff:
    def test_empty(self):
        d = BaselineDiff()
        assert d.has_changes is False
        assert d.regression_count == 0
        assert d.improvement_count == 0

    def test_has_changes_with_new(self):
        d = BaselineDiff(new_findings=[
            DiffFinding("a", "X", "HIGH", "T", status="new"),
        ])
        assert d.has_changes is True
        assert d.regression_count == 1

    def test_has_changes_with_resolved(self):
        d = BaselineDiff(resolved_findings=[
            DiffFinding("a", "X", "HIGH", "T", status="resolved"),
        ])
        assert d.has_changes is True
        assert d.improvement_count == 1

    def test_to_dict(self):
        d = BaselineDiff(baseline_count=5, scan_count=3, drift_score=40)
        dd = d.to_dict()
        assert dd["baseline_count"] == 5
        assert dd["scan_count"] == 3
        assert dd["drift_score"] == 40


class TestBaselineSnapshot:
    def test_to_dict(self):
        s = BaselineSnapshot("2024-01-01T00:00:00Z", "v1", 10, {"HIGH": 5})
        d = s.to_dict()
        assert d["finding_count"] == 10
        assert d["label"] == "v1"

    def test_from_dict(self):
        d = {"timestamp": "t", "label": "l", "finding_count": 3,
             "severity_counts": {}, "fingerprints": ["a", "b"]}
        s = BaselineSnapshot.from_dict(d)
        assert s.finding_count == 3
        assert len(s.fingerprints) == 2


class TestBaselineEvolution:
    def test_empty(self):
        e = BaselineEvolution()
        assert e.total_snapshots == 0
        assert e.trend == "insufficient_data"

    def test_improving(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 10),
            BaselineSnapshot("t2", "v2", 5),
        ])
        assert e.trend == "improving"

    def test_worsening(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 5),
            BaselineSnapshot("t2", "v2", 10),
        ])
        assert e.trend == "worsening"

    def test_stable(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 5),
            BaselineSnapshot("t2", "v2", 5),
        ])
        assert e.trend == "stable"

    def test_add_snapshot(self):
        e = BaselineEvolution()
        b = _make_baseline()
        snap = e.add_snapshot(b, "v1")
        assert e.total_snapshots == 1
        assert snap.finding_count == b.count

    def test_to_dict(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 10),
        ])
        d = e.to_dict()
        assert d["total_snapshots"] == 1
        assert "snapshots" in d

    def test_roundtrip(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 10, {"HIGH": 5}, {"a", "b"}),
        ])
        text = e.to_json()
        e2 = BaselineEvolution.from_json(text)
        assert e2.total_snapshots == 1
        assert e2.snapshots[0].finding_count == 10


class TestScanFinding:
    def test_to_dict(self):
        f = ScanFinding("abc", "CMD-001", "HIGH", "Test")
        d = f.to_dict()
        assert d["fingerprint"] == "abc"
        assert d["rule_id"] == "CMD-001"


class TestScanResult:
    def test_empty(self):
        r = ScanResult()
        assert r.count == 0
        assert r.timestamp != ""

    def test_count(self):
        r = _make_scan_result()
        assert r.count == 1

    def test_to_dict(self):
        r = _make_scan_result()
        d = r.to_dict()
        assert d["count"] == 1
        assert "findings" in d


# --- Fingerprinting ---

class TestComputeFingerprint:
    def test_deterministic(self):
        fp1 = compute_fingerprint("CMD-001", "server.py", "subprocess.run(cmd)")
        fp2 = compute_fingerprint("CMD-001", "server.py", "subprocess.run(cmd)")
        assert fp1 == fp2

    def test_different_rule(self):
        fp1 = compute_fingerprint("CMD-001", "server.py", "code")
        fp2 = compute_fingerprint("SQL-001", "server.py", "code")
        assert fp1 != fp2

    def test_different_file(self):
        fp1 = compute_fingerprint("CMD-001", "server.py", "code")
        fp2 = compute_fingerprint("CMD-001", "handler.py", "code")
        assert fp1 != fp2

    def test_uses_basename(self):
        fp1 = compute_fingerprint("CMD-001", "/home/user/server.py", "code")
        fp2 = compute_fingerprint("CMD-001", "/opt/app/server.py", "code")
        assert fp1 == fp2

    def test_hex_string(self):
        fp = compute_fingerprint("X", "f.py", "code")
        assert len(fp) == 16
        assert all(c in "0123456789abcdef" for c in fp)

    def test_strips_whitespace(self):
        fp1 = compute_fingerprint("X", "f.py", "  code  ")
        fp2 = compute_fingerprint("X", "f.py", "code")
        assert fp1 == fp2

    def test_empty_file(self):
        fp = compute_fingerprint("X", "", "code")
        assert len(fp) == 16


# --- create_baseline ---

class TestCreateBaseline:
    def test_empty_scan(self):
        scan = ScanResult(findings=[])
        baseline = create_baseline(scan)
        assert baseline.count == 0

    def test_single_finding(self):
        scan = _make_scan_result()
        baseline = create_baseline(scan)
        assert baseline.count == 1

    def test_with_label(self):
        scan = _make_scan_result()
        baseline = create_baseline(scan, label="v1.0")
        assert baseline.label == "v1.0"

    def test_metadata(self):
        scan = _make_scan_result()
        baseline = create_baseline(scan)
        assert "source_count" in baseline.metadata

    def test_multiple_findings(self):
        findings = [
            _make_scan_finding(rule_id="CMD-001"),
            _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)"),
            _make_scan_finding(rule_id="PATH-001", code="open(user_path)"),
        ]
        scan = _make_scan_result(findings)
        baseline = create_baseline(scan)
        assert baseline.count == 3


# --- compare_to_baseline ---

class TestCompareToBaseline:
    def test_no_changes(self):
        sf = _make_scan_finding()
        scan = _make_scan_result([sf])
        baseline = create_baseline(scan)
        # Re-scan with same findings
        scan2 = _make_scan_result([sf])
        diff = compare_to_baseline(scan2, baseline)
        assert diff.has_changes is False
        assert len(diff.unchanged_findings) == 1

    def test_new_finding(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        baseline = create_baseline(_make_scan_result([sf1]))
        scan = _make_scan_result([sf1, sf2])
        diff = compare_to_baseline(scan, baseline)
        assert len(diff.new_findings) == 1
        assert diff.new_findings[0].rule_id == "SQL-001"

    def test_resolved_finding(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        baseline = create_baseline(_make_scan_result([sf1, sf2]))
        scan = _make_scan_result([sf1])
        diff = compare_to_baseline(scan, baseline)
        assert len(diff.resolved_findings) == 1
        assert diff.resolved_findings[0].rule_id == "SQL-001"

    def test_drifted_severity(self):
        sf = _make_scan_finding(rule_id="CMD-001", severity="HIGH")
        baseline = create_baseline(_make_scan_result([sf]))
        # Same fingerprint but different severity
        sf_drifted = ScanFinding(
            fingerprint=sf.fingerprint,
            rule_id="CMD-001",
            severity="CRITICAL",
            title="Test",
            file="server.py",
        )
        scan = _make_scan_result([sf_drifted])
        diff = compare_to_baseline(scan, baseline)
        assert len(diff.drifted_findings) == 1
        assert diff.drifted_findings[0].severity == "CRITICAL"

    def test_drift_score(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        baseline = create_baseline(_make_scan_result([sf1]))
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        scan = _make_scan_result([sf2])
        diff = compare_to_baseline(scan, baseline)
        assert diff.drift_score > 0

    def test_drift_score_zero_unchanged(self):
        sf = _make_scan_finding()
        baseline = create_baseline(_make_scan_result([sf]))
        scan = _make_scan_result([sf])
        diff = compare_to_baseline(scan, baseline)
        assert diff.drift_score == 0

    def test_baseline_count(self):
        sf = _make_scan_finding()
        baseline = create_baseline(_make_scan_result([sf]))
        scan = _make_scan_result([sf])
        diff = compare_to_baseline(scan, baseline)
        assert diff.baseline_count == 1
        assert diff.scan_count == 1

    def test_empty_baseline(self):
        baseline = create_baseline(ScanResult(findings=[]))
        sf = _make_scan_finding()
        scan = _make_scan_result([sf])
        diff = compare_to_baseline(scan, baseline)
        assert len(diff.new_findings) == 1

    def test_empty_scan(self):
        sf = _make_scan_finding()
        baseline = create_baseline(_make_scan_result([sf]))
        scan = ScanResult(findings=[])
        diff = compare_to_baseline(scan, baseline)
        assert len(diff.resolved_findings) == 1


# --- update_baseline ---

class TestUpdateBaseline:
    def test_accept_new(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        baseline = create_baseline(_make_scan_result([sf1]))
        diff = compare_to_baseline(_make_scan_result([sf1, sf2]), baseline)
        updated = update_baseline(baseline, diff)
        assert updated.count == 2

    def test_accept_resolved(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        baseline = create_baseline(_make_scan_result([sf1, sf2]))
        diff = compare_to_baseline(_make_scan_result([sf1]), baseline)
        updated = update_baseline(baseline, diff)
        assert updated.count == 1

    def test_no_accept_new(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        baseline = create_baseline(_make_scan_result([sf1]))
        diff = compare_to_baseline(_make_scan_result([sf1, sf2]), baseline)
        updated = update_baseline(baseline, diff, accept_new=False)
        assert updated.count == 1

    def test_no_accept_resolved(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        baseline = create_baseline(_make_scan_result([sf1, sf2]))
        diff = compare_to_baseline(_make_scan_result([sf1]), baseline)
        updated = update_baseline(baseline, diff, accept_resolved=False)
        assert updated.count == 2

    def test_updated_at_changes(self):
        baseline = _make_baseline()
        diff = BaselineDiff()
        updated = update_baseline(baseline, diff)
        assert updated.updated_at >= baseline.updated_at

    def test_drifted_severity_updated(self):
        sf = _make_scan_finding(rule_id="CMD-001", severity="HIGH")
        baseline = create_baseline(_make_scan_result([sf]))
        sf_drifted = ScanFinding(
            fingerprint=sf.fingerprint,
            rule_id="CMD-001",
            severity="CRITICAL",
            title="Test",
            file="server.py",
        )
        diff = compare_to_baseline(_make_scan_result([sf_drifted]), baseline)
        updated = update_baseline(baseline, diff)
        sev = {f.severity for f in updated.findings}
        assert "CRITICAL" in sev


# --- merge_baselines ---

class TestMergeBaselines:
    def test_merge_disjoint(self):
        sf1 = _make_scan_finding(rule_id="CMD-001")
        sf2 = _make_scan_finding(rule_id="SQL-001", code="cursor.execute(q)")
        b1 = create_baseline(_make_scan_result([sf1]))
        b2 = create_baseline(_make_scan_result([sf2]))
        merged = merge_baselines(b1, b2)
        assert merged.count == 2

    def test_merge_deduplicates(self):
        sf = _make_scan_finding(rule_id="CMD-001")
        b1 = create_baseline(_make_scan_result([sf]))
        b2 = create_baseline(_make_scan_result([sf]))
        merged = merge_baselines(b1, b2)
        assert merged.count == 1

    def test_base_wins_on_conflict(self):
        sf = _make_scan_finding(rule_id="CMD-001", severity="HIGH")
        b1 = create_baseline(_make_scan_result([sf]))
        sf2 = ScanFinding(
            fingerprint=sf.fingerprint,
            rule_id="CMD-001",
            severity="LOW",
            title="Test",
            file="server.py",
        )
        b2 = create_baseline(_make_scan_result([sf2]))
        merged = merge_baselines(b1, b2)
        assert merged.findings[0].severity == "HIGH"

    def test_label_merged(self):
        b1 = Baseline(label="a")
        b2 = Baseline(label="b")
        merged = merge_baselines(b1, b2)
        assert "merged" in merged.label


# --- filter_baseline ---

class TestFilterBaseline:
    def test_filter_by_severity(self):
        b = Baseline(findings=[
            BaselineFinding("a", "X", "HIGH", "T1"),
            BaselineFinding("b", "Y", "MEDIUM", "T2"),
            BaselineFinding("c", "Z", "HIGH", "T3"),
        ])
        filtered = filter_baseline(b, severity="HIGH")
        assert filtered.count == 2

    def test_filter_by_rule_id(self):
        b = Baseline(findings=[
            BaselineFinding("a", "CMD-001", "HIGH", "T1"),
            BaselineFinding("b", "SQL-001", "HIGH", "T2"),
            BaselineFinding("c", "CMD-002", "HIGH", "T3"),
        ])
        filtered = filter_baseline(b, rule_id="CMD")
        assert filtered.count == 2

    def test_filter_by_file(self):
        b = Baseline(findings=[
            BaselineFinding("a", "X", "HIGH", "T1", file="server.py"),
            BaselineFinding("b", "Y", "HIGH", "T2", file="handler.py"),
        ])
        filtered = filter_baseline(b, file_pattern="server")
        assert filtered.count == 1

    def test_no_match(self):
        b = Baseline(findings=[
            BaselineFinding("a", "X", "HIGH", "T1"),
        ])
        filtered = filter_baseline(b, severity="LOW")
        assert filtered.count == 0

    def test_all_filters(self):
        b = Baseline(findings=[
            BaselineFinding("a", "CMD-001", "HIGH", "T1", file="server.py"),
            BaselineFinding("b", "SQL-001", "HIGH", "T2", file="server.py"),
            BaselineFinding("c", "CMD-002", "MEDIUM", "T3", file="handler.py"),
        ])
        filtered = filter_baseline(b, severity="HIGH", rule_id="CMD", file_pattern="server")
        assert filtered.count == 1


# --- Formatters ---

class TestFormatBaselineText:
    def test_basic(self):
        b = _make_baseline()
        text = format_baseline_text(b)
        assert "Security Baseline" in text
        assert "Findings:" in text

    def test_with_label(self):
        b = Baseline(label="v1.0")
        text = format_baseline_text(b)
        assert "v1.0" in text

    def test_empty(self):
        b = Baseline()
        text = format_baseline_text(b)
        assert "Findings: 0" in text

    def test_severity_counts(self):
        b = Baseline(findings=[
            BaselineFinding("a", "X", "HIGH", "T1"),
            BaselineFinding("b", "Y", "HIGH", "T2"),
        ])
        text = format_baseline_text(b)
        assert "HIGH" in text


class TestFormatBaselineJson:
    def test_valid_json(self):
        b = _make_baseline()
        text = format_baseline_json(b)
        data = json.loads(text)
        assert data["count"] == 1


class TestFormatDiffText:
    def test_no_changes(self):
        d = BaselineDiff()
        text = format_diff_text(d)
        assert "No changes" in text

    def test_with_new(self):
        d = BaselineDiff(new_findings=[
            DiffFinding("a", "CMD-001", "HIGH", "Test", status="new"),
        ])
        text = format_diff_text(d)
        assert "NEW" in text

    def test_with_resolved(self):
        d = BaselineDiff(resolved_findings=[
            DiffFinding("a", "CMD-001", "HIGH", "Test", status="resolved"),
        ])
        text = format_diff_text(d)
        assert "RESOLVED" in text

    def test_with_drifted(self):
        d = BaselineDiff(drifted_findings=[
            DiffFinding("a", "CMD-001", "CRITICAL", "Test", status="drifted"),
        ])
        text = format_diff_text(d)
        assert "DRIFTED" in text


class TestFormatDiffJson:
    def test_valid_json(self):
        d = BaselineDiff(baseline_count=5, scan_count=3)
        text = format_diff_json(d)
        data = json.loads(text)
        assert data["baseline_count"] == 5


class TestFormatEvolutionText:
    def test_basic(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 10),
        ])
        text = format_evolution_text(e)
        assert "Evolution" in text
        assert "v1" in text


class TestFormatEvolutionJson:
    def test_valid_json(self):
        e = BaselineEvolution(snapshots=[
            BaselineSnapshot("t1", "v1", 10),
        ])
        text = format_evolution_json(e)
        data = json.loads(text)
        assert data["total_snapshots"] == 1
