"""Tests for mcpaudit severity_config — Severity Customization."""

import json
import tempfile
from pathlib import Path

from mcpaudit.severity_config import (
    OverrideResult,
    RuleOverride,
    SeverityConfig,
    apply_severity_overrides,
    apply_severity_overrides_detailed,
    create_default_rc,
    format_config_json,
    format_config_text,
    load_severity_config,
    merge_configs,
    parse_severity_config,
    validate_config,
)


# --- Helpers ---

def _finding(rule_id="CMD-001", severity="HIGH", title="Test"):
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": f"Desc: {title}",
        "file": "server.py",
        "line": 10,
    }


def _write_rc(data):
    """Write a .mcpauditrc temp file."""
    f = tempfile.NamedTemporaryFile(
        suffix=".json", mode="w", delete=False
    )
    json.dump(data, f)
    f.flush()
    f.close()
    return Path(f.name)


# --- RuleOverride ---

class TestRuleOverride:
    def test_to_dict_severity(self):
        ov = RuleOverride(rule_id="X", severity="LOW")
        d = ov.to_dict()
        assert d["severity"] == "LOW"
        assert "ignore" not in d

    def test_to_dict_ignore(self):
        ov = RuleOverride(rule_id="X", ignore=True)
        d = ov.to_dict()
        assert d["ignore"] is True

    def test_to_dict_note(self):
        ov = RuleOverride(rule_id="X", note="reason")
        d = ov.to_dict()
        assert d["note"] == "reason"

    def test_to_dict_empty(self):
        ov = RuleOverride(rule_id="X")
        d = ov.to_dict()
        assert d == {}

    def test_to_dict_combined(self):
        ov = RuleOverride(
            rule_id="X", severity="CRITICAL", ignore=True, note="N"
        )
        d = ov.to_dict()
        assert d["severity"] == "CRITICAL"
        assert d["ignore"] is True
        assert d["note"] == "N"


# --- SeverityConfig ---

class TestSeverityConfig:
    def test_is_valid_default(self):
        cfg = SeverityConfig()
        assert cfg.is_valid is True

    def test_is_valid_with_errors(self):
        cfg = SeverityConfig(errors=["bad"])
        assert cfg.is_valid is False

    def test_ignored_rules(self):
        cfg = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", ignore=True),
            "B": RuleOverride(rule_id="B", severity="LOW"),
        })
        assert "A" in cfg.ignored_rules
        assert "B" not in cfg.ignored_rules

    def test_overridden_rules(self):
        cfg = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", severity="CRITICAL"),
            "B": RuleOverride(rule_id="B", ignore=True),
        })
        assert "A" in cfg.overridden_rules
        assert "B" not in cfg.overridden_rules

    def test_to_dict(self):
        cfg = SeverityConfig(
            rules={"X": RuleOverride(rule_id="X", severity="LOW")},
            threshold=80,
            min_severity="MEDIUM",
        )
        d = cfg.to_dict()
        assert d["threshold"] == 80
        assert d["min_severity"] == "MEDIUM"
        assert "X" in d["rules"]

    def test_to_dict_empty(self):
        cfg = SeverityConfig()
        d = cfg.to_dict()
        assert d["rules"] == {}


# --- validate_config ---

class TestValidateConfig:
    def test_valid_config(self):
        data = {
            "rules": {"CMD-001": {"severity": "LOW"}},
            "threshold": 70,
        }
        assert validate_config(data) == []

    def test_not_a_dict(self):
        errors = validate_config("not a dict")
        assert len(errors) == 1
        assert "object" in errors[0]

    def test_rules_not_dict(self):
        errors = validate_config({"rules": "bad"})
        assert any("object" in e for e in errors)

    def test_invalid_severity(self):
        data = {"rules": {"X": {"severity": "BOGUS"}}}
        errors = validate_config(data)
        assert any("invalid severity" in e.lower() for e in errors)

    def test_invalid_threshold_type(self):
        data = {"rules": {}, "threshold": "high"}
        errors = validate_config(data)
        assert any("threshold" in e for e in errors)

    def test_threshold_out_of_range_negative(self):
        data = {"rules": {}, "threshold": -5}
        errors = validate_config(data)
        assert any("threshold" in e for e in errors)

    def test_threshold_out_of_range_over(self):
        data = {"rules": {}, "threshold": 101}
        errors = validate_config(data)
        assert any("threshold" in e for e in errors)

    def test_valid_threshold_boundary(self):
        assert validate_config({"rules": {}, "threshold": 0}) == []
        assert validate_config({"rules": {}, "threshold": 100}) == []

    def test_invalid_min_severity_type(self):
        data = {"rules": {}, "min_severity": 42}
        errors = validate_config(data)
        assert any("min_severity" in e for e in errors)

    def test_invalid_min_severity_value(self):
        data = {"rules": {}, "min_severity": "EXTREME"}
        errors = validate_config(data)
        assert any("min_severity" in e for e in errors)

    def test_valid_min_severity(self):
        data = {"rules": {}, "min_severity": "HIGH"}
        assert validate_config(data) == []

    def test_rule_value_not_dict(self):
        data = {"rules": {"X": "bad"}}
        errors = validate_config(data)
        assert any("object" in e for e in errors)

    def test_severity_not_string(self):
        data = {"rules": {"X": {"severity": 123}}}
        errors = validate_config(data)
        assert any("string" in e for e in errors)

    def test_ignore_not_bool(self):
        data = {"rules": {"X": {"ignore": "yes"}}}
        errors = validate_config(data)
        assert any("boolean" in e for e in errors)

    def test_empty_config(self):
        assert validate_config({}) == []

    def test_ignore_true_valid(self):
        data = {"rules": {"X": {"ignore": True}}}
        assert validate_config(data) == []


# --- load_severity_config ---

class TestLoadSeverityConfig:
    def test_load_valid_file(self):
        data = {"rules": {"CMD-001": {"severity": "LOW"}}, "threshold": 60}
        path = _write_rc(data)
        cfg = load_severity_config(path)
        assert cfg.is_valid
        assert "CMD-001" in cfg.rules
        assert cfg.threshold == 60

    def test_file_not_found(self):
        cfg = load_severity_config("/nonexistent/.mcpauditrc")
        assert not cfg.is_valid
        assert any("not found" in e for e in cfg.errors)

    def test_invalid_json(self):
        f = tempfile.NamedTemporaryFile(
            suffix=".json", mode="w", delete=False
        )
        f.write("{bad json")
        f.flush()
        f.close()
        cfg = load_severity_config(f.name)
        assert not cfg.is_valid
        assert any("JSON" in e for e in cfg.errors)

    def test_source_path_set(self):
        path = _write_rc({"rules": {}})
        cfg = load_severity_config(path)
        assert cfg.source_path == str(path)

    def test_load_with_ignore(self):
        data = {"rules": {"MCP-003": {"ignore": True}}}
        path = _write_rc(data)
        cfg = load_severity_config(path)
        assert cfg.is_valid
        assert cfg.rules["MCP-003"].ignore is True


# --- parse_severity_config ---

class TestParseSeverityConfig:
    def test_parse_basic(self):
        data = {
            "rules": {"CMD-001": {"severity": "CRITICAL"}},
            "threshold": 80,
            "min_severity": "MEDIUM",
        }
        cfg = parse_severity_config(data)
        assert cfg.is_valid
        assert cfg.rules["CMD-001"].severity == "CRITICAL"
        assert cfg.threshold == 80
        assert cfg.min_severity == "MEDIUM"

    def test_parse_case_insensitive_severity(self):
        data = {"rules": {"X": {"severity": "low"}}}
        cfg = parse_severity_config(data)
        assert cfg.rules["X"].severity == "LOW"

    def test_parse_ignore(self):
        data = {"rules": {"X": {"ignore": True}}}
        cfg = parse_severity_config(data)
        assert cfg.rules["X"].ignore is True

    def test_parse_note(self):
        data = {"rules": {"X": {"severity": "LOW", "note": "reason"}}}
        cfg = parse_severity_config(data)
        assert cfg.rules["X"].note == "reason"

    def test_invalid_config_returns_errors(self):
        data = {"rules": "bad"}
        cfg = parse_severity_config(data)
        assert not cfg.is_valid

    def test_source_path_preserved(self):
        cfg = parse_severity_config({}, source_path="/a/b/c")
        assert cfg.source_path == "/a/b/c"


# --- apply_severity_overrides ---

class TestApplySeverityOverrides:
    def test_no_overrides(self):
        findings = [_finding()]
        cfg = SeverityConfig()
        result = apply_severity_overrides(findings, cfg)
        assert len(result) == 1

    def test_ignore_rule(self):
        findings = [_finding(rule_id="CMD-001")]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(rule_id="CMD-001", ignore=True),
        })
        result = apply_severity_overrides(findings, cfg)
        assert len(result) == 0

    def test_change_severity(self):
        findings = [_finding(rule_id="CMD-001", severity="HIGH")]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(
                rule_id="CMD-001", severity="CRITICAL"
            ),
        })
        result = apply_severity_overrides(findings, cfg)
        assert len(result) == 1
        assert result[0]["severity"] == "CRITICAL"
        assert result[0]["severity_original"] == "HIGH"

    def test_no_change_same_severity(self):
        findings = [_finding(severity="HIGH")]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(rule_id="CMD-001", severity="HIGH"),
        })
        result = apply_severity_overrides(findings, cfg)
        assert "severity_original" not in result[0]

    def test_min_severity_filter(self):
        findings = [
            _finding(severity="CRITICAL", rule_id="A"),
            _finding(severity="HIGH", rule_id="B"),
            _finding(severity="LOW", rule_id="C"),
            _finding(severity="INFO", rule_id="D"),
        ]
        cfg = SeverityConfig(min_severity="HIGH")
        result = apply_severity_overrides(findings, cfg)
        sevs = {f["severity"] for f in result}
        assert "LOW" not in sevs
        assert "INFO" not in sevs
        assert "CRITICAL" in sevs
        assert "HIGH" in sevs

    def test_min_severity_medium(self):
        findings = [
            _finding(severity="MEDIUM", rule_id="A"),
            _finding(severity="LOW", rule_id="B"),
        ]
        cfg = SeverityConfig(min_severity="MEDIUM")
        result = apply_severity_overrides(findings, cfg)
        assert len(result) == 1
        assert result[0]["severity"] == "MEDIUM"

    def test_combined_ignore_and_override(self):
        findings = [
            _finding(rule_id="A", severity="HIGH"),
            _finding(rule_id="B", severity="MEDIUM"),
        ]
        cfg = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", ignore=True),
            "B": RuleOverride(rule_id="B", severity="CRITICAL"),
        })
        result = apply_severity_overrides(findings, cfg)
        assert len(result) == 1
        assert result[0]["severity"] == "CRITICAL"

    def test_does_not_mutate_original(self):
        findings = [_finding(severity="HIGH")]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(
                rule_id="CMD-001", severity="LOW"
            ),
        })
        apply_severity_overrides(findings, cfg)
        assert findings[0]["severity"] == "HIGH"

    def test_empty_findings(self):
        cfg = SeverityConfig()
        result = apply_severity_overrides([], cfg)
        assert result == []

    def test_no_matching_rule(self):
        findings = [_finding(rule_id="UNKNOWN-999")]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(rule_id="CMD-001", ignore=True),
        })
        result = apply_severity_overrides(findings, cfg)
        assert len(result) == 1


# --- apply_severity_overrides_detailed ---

class TestApplySeverityOverridesDetailed:
    def test_counts_ignored(self):
        findings = [_finding(rule_id="A"), _finding(rule_id="B")]
        cfg = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", ignore=True),
        })
        result = apply_severity_overrides_detailed(findings, cfg)
        assert result.ignored_count == 1
        assert len(result.findings) == 1

    def test_counts_overridden(self):
        findings = [_finding(severity="HIGH")]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(
                rule_id="CMD-001", severity="LOW"
            ),
        })
        result = apply_severity_overrides_detailed(findings, cfg)
        assert result.overridden_count == 1

    def test_counts_filtered(self):
        findings = [_finding(severity="LOW")]
        cfg = SeverityConfig(min_severity="HIGH")
        result = apply_severity_overrides_detailed(findings, cfg)
        assert result.filtered_count == 1
        assert len(result.findings) == 0

    def test_changes_tracked(self):
        findings = [_finding()]
        cfg = SeverityConfig(rules={
            "CMD-001": RuleOverride(
                rule_id="CMD-001", severity="CRITICAL"
            ),
        })
        result = apply_severity_overrides_detailed(findings, cfg)
        assert any("CMD-001" in c for c in result.changes)

    def test_to_dict(self):
        result = OverrideResult(
            findings=[_finding()],
            ignored_count=1,
            overridden_count=2,
        )
        d = result.to_dict()
        assert d["ignored_count"] == 1
        assert d["overridden_count"] == 2


# --- merge_configs ---

class TestMergeConfigs:
    def test_override_wins(self):
        base = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", severity="LOW"),
        })
        over = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", severity="CRITICAL"),
        })
        merged = merge_configs(base, over)
        assert merged.rules["A"].severity == "CRITICAL"

    def test_base_preserved(self):
        base = SeverityConfig(rules={
            "A": RuleOverride(rule_id="A", severity="LOW"),
        })
        over = SeverityConfig(rules={
            "B": RuleOverride(rule_id="B", ignore=True),
        })
        merged = merge_configs(base, over)
        assert "A" in merged.rules
        assert "B" in merged.rules

    def test_threshold_override(self):
        base = SeverityConfig(threshold=50)
        over = SeverityConfig(threshold=90)
        merged = merge_configs(base, over)
        assert merged.threshold == 90

    def test_threshold_fallback_to_base(self):
        base = SeverityConfig(threshold=50)
        over = SeverityConfig()
        merged = merge_configs(base, over)
        assert merged.threshold == 50

    def test_min_severity_override(self):
        base = SeverityConfig(min_severity="LOW")
        over = SeverityConfig(min_severity="HIGH")
        merged = merge_configs(base, over)
        assert merged.min_severity == "HIGH"


# --- format_config_text ---

class TestFormatConfigText:
    def test_valid_config(self):
        cfg = SeverityConfig(
            rules={
                "A": RuleOverride(rule_id="A", severity="CRITICAL"),
                "B": RuleOverride(rule_id="B", ignore=True),
            },
            threshold=70,
        )
        text = format_config_text(cfg)
        assert "70" in text
        assert "CRITICAL" in text
        assert "Ignored" in text

    def test_invalid_config(self):
        cfg = SeverityConfig(errors=["bad config"])
        text = format_config_text(cfg)
        assert "INVALID" in text
        assert "bad config" in text

    def test_no_overrides(self):
        cfg = SeverityConfig()
        text = format_config_text(cfg)
        assert "No overrides" in text

    def test_source_path_shown(self):
        cfg = SeverityConfig(source_path="/a/b/.mcpauditrc")
        text = format_config_text(cfg)
        assert "/a/b/.mcpauditrc" in text


# --- format_config_json ---

class TestFormatConfigJson:
    def test_valid_json(self):
        cfg = SeverityConfig(
            rules={"X": RuleOverride(rule_id="X", severity="LOW")}
        )
        data = json.loads(format_config_json(cfg))
        assert data["is_valid"] is True
        assert "X" in data["rules"]

    def test_includes_errors(self):
        cfg = SeverityConfig(errors=["err1"])
        data = json.loads(format_config_json(cfg))
        assert data["is_valid"] is False
        assert "err1" in data["errors"]


# --- create_default_rc ---

class TestCreateDefaultRc:
    def test_valid_json(self):
        content = create_default_rc()
        data = json.loads(content)
        assert "rules" in data
        assert "threshold" in data

    def test_has_examples(self):
        content = create_default_rc()
        data = json.loads(content)
        assert len(data["rules"]) >= 1
