"""Tests for Configuration Drift Detector."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from mcpaudit.config_drift import (
    ConfigSnapshot,
    DriftItem,
    DriftReport,
    DriftSeverity,
    _compare_dicts,
    _compare_lists,
    _compute_hash,
    _extract_endpoints,
    _extract_env_vars,
    _extract_permissions,
    _extract_tools_from_config,
    _find_config_file,
    _severity_for_category,
    _recommendation_for_drift,
    check_drift,
    check_drift_from_file,
    create_snapshot,
    create_snapshot_from_dict,
    format_drift_json,
    format_drift_text,
    load_snapshot,
    save_snapshot,
)


# --- Data Models ---

class TestDriftSeverity:
    def test_critical(self):
        assert DriftSeverity.CRITICAL == "critical"

    def test_high(self):
        assert DriftSeverity.HIGH == "high"

    def test_medium(self):
        assert DriftSeverity.MEDIUM == "medium"

    def test_low(self):
        assert DriftSeverity.LOW == "low"


class TestDriftItem:
    def test_to_dict(self):
        item = DriftItem(
            category="tools",
            change_type="added",
            key="new_tool",
            new_value="definition",
            severity=DriftSeverity.HIGH,
        )
        d = item.to_dict()
        assert d["category"] == "tools"
        assert d["change_type"] == "added"
        assert d["key"] == "new_tool"
        assert d["severity"] == "high"


class TestDriftReport:
    def test_empty_report(self):
        r = DriftReport()
        assert r.total_drifts == 0
        assert r.critical_count == 0
        assert r.high_count == 0
        assert r.has_critical is False
        assert r.exit_code == 0

    def test_with_critical(self):
        r = DriftReport(drifts=[
            DriftItem("permissions", "modified", "key", severity=DriftSeverity.CRITICAL),
        ])
        assert r.critical_count == 1
        assert r.has_critical is True
        assert r.exit_code == 2

    def test_with_high(self):
        r = DriftReport(drifts=[
            DriftItem("tools", "added", "key", severity=DriftSeverity.HIGH),
        ])
        assert r.high_count == 1
        assert r.exit_code == 1

    def test_severity_counts(self):
        r = DriftReport(drifts=[
            DriftItem("a", "added", "1", severity=DriftSeverity.CRITICAL),
            DriftItem("b", "added", "2", severity=DriftSeverity.HIGH),
            DriftItem("c", "added", "3", severity=DriftSeverity.MEDIUM),
            DriftItem("d", "added", "4", severity=DriftSeverity.LOW),
        ])
        counts = r.severity_counts
        assert counts["critical"] == 1
        assert counts["high"] == 1
        assert counts["medium"] == 1
        assert counts["low"] == 1

    def test_to_dict(self):
        r = DriftReport(baseline_path="/a", current_path="/b")
        d = r.to_dict()
        assert d["baseline_path"] == "/a"
        assert d["current_path"] == "/b"
        assert "drifts" in d


class TestConfigSnapshot:
    def test_to_dict(self):
        s = ConfigSnapshot(path="/test", timestamp="2024-01-01T00:00:00Z")
        d = s.to_dict()
        assert d["path"] == "/test"
        assert d["timestamp"] == "2024-01-01T00:00:00Z"

    def test_from_dict(self):
        data = {
            "path": "/test",
            "timestamp": "2024-01-01T00:00:00Z",
            "config_hash": "abc123",
            "tools": {"tool1": {}},
            "permissions": {},
            "env_vars": {},
            "endpoints": [],
            "raw_config": {},
        }
        s = ConfigSnapshot.from_dict(data)
        assert s.path == "/test"
        assert s.config_hash == "abc123"
        assert "tool1" in s.tools

    def test_from_dict_missing_keys(self):
        s = ConfigSnapshot.from_dict({})
        assert s.path == ""
        assert s.tools == {}


# --- Config Extraction ---

class TestExtractTools:
    def test_mcp_servers_format(self):
        config = {
            "mcpServers": {
                "server1": {
                    "tools": {"read_file": {}, "write_file": {}},
                }
            }
        }
        tools = _extract_tools_from_config(config)
        assert "server1.read_file" in tools
        assert "server1.write_file" in tools

    def test_tools_as_list(self):
        config = {
            "mcpServers": {
                "server1": {
                    "tools": ["read_file", "write_file"],
                }
            }
        }
        tools = _extract_tools_from_config(config)
        assert "server1.read_file" in tools

    def test_tools_as_list_of_dicts(self):
        config = {
            "mcpServers": {
                "s1": {
                    "tools": [{"name": "read_file", "desc": "Read files"}],
                }
            }
        }
        tools = _extract_tools_from_config(config)
        assert "s1.read_file" in tools

    def test_top_level_tools(self):
        config = {"tools": {"my_tool": {"desc": "test"}}}
        tools = _extract_tools_from_config(config)
        assert "my_tool" in tools

    def test_empty_config(self):
        tools = _extract_tools_from_config({})
        assert tools == {}


class TestExtractPermissions:
    def test_server_permissions(self):
        config = {
            "mcpServers": {
                "s1": {"permissions": {"read": True, "write": False}},
            }
        }
        perms = _extract_permissions(config)
        assert "s1" in perms

    def test_allowed_directories(self):
        config = {
            "mcpServers": {
                "s1": {"allowedDirectories": ["/tmp", "/home"]},
            }
        }
        perms = _extract_permissions(config)
        assert "s1.allowedDirectories" in perms

    def test_global_permissions(self):
        config = {"permissions": {"admin": True}}
        perms = _extract_permissions(config)
        assert "_global" in perms


class TestExtractEnvVars:
    def test_server_env(self):
        config = {
            "mcpServers": {
                "s1": {"env": {"API_KEY": "secret", "DEBUG": "true"}},
            }
        }
        envs = _extract_env_vars(config)
        assert "s1.API_KEY" in envs
        assert envs["s1.API_KEY"] == "secret"

    def test_top_level_env(self):
        config = {"env": {"GLOBAL_VAR": "value"}}
        envs = _extract_env_vars(config)
        assert "GLOBAL_VAR" in envs


class TestExtractEndpoints:
    def test_server_commands(self):
        config = {
            "mcpServers": {
                "s1": {"command": "python", "args": ["server.py", "--port", "8080"]},
            }
        }
        endpoints = _extract_endpoints(config)
        assert len(endpoints) == 1
        assert "python" in endpoints[0]
        assert "server.py" in endpoints[0]


# --- Hash ---

class TestComputeHash:
    def test_deterministic(self):
        data = {"key": "value"}
        h1 = _compute_hash(data)
        h2 = _compute_hash(data)
        assert h1 == h2

    def test_different_data(self):
        h1 = _compute_hash({"a": 1})
        h2 = _compute_hash({"b": 2})
        assert h1 != h2


# --- Severity Assignment ---

class TestSeverityForCategory:
    def test_tools_added(self):
        assert _severity_for_category("tools", "added") == DriftSeverity.HIGH

    def test_permissions_any(self):
        assert _severity_for_category("permissions", "added") == DriftSeverity.CRITICAL
        assert _severity_for_category("permissions", "modified") == DriftSeverity.CRITICAL

    def test_env_vars_modified(self):
        assert _severity_for_category("env_vars", "modified") == DriftSeverity.HIGH

    def test_env_vars_removed(self):
        assert _severity_for_category("env_vars", "removed") == DriftSeverity.LOW

    def test_endpoints_added(self):
        assert _severity_for_category("endpoints", "added") == DriftSeverity.HIGH


# --- Recommendations ---

class TestRecommendations:
    def test_tools_added_recommendation(self):
        item = DriftItem("tools", "added", "key")
        rec = _recommendation_for_drift(item)
        assert "security" in rec.lower() or "review" in rec.lower()

    def test_permissions_modified_recommendation(self):
        item = DriftItem("permissions", "modified", "key")
        rec = _recommendation_for_drift(item)
        assert len(rec) > 0


# --- Dict Comparison ---

class TestCompareDicts:
    def test_no_changes(self):
        drifts = _compare_dicts({"a": 1}, {"a": 1}, "tools")
        assert len(drifts) == 0

    def test_added(self):
        drifts = _compare_dicts({}, {"new": "val"}, "tools")
        assert len(drifts) == 1
        assert drifts[0].change_type == "added"

    def test_removed(self):
        drifts = _compare_dicts({"old": "val"}, {}, "tools")
        assert len(drifts) == 1
        assert drifts[0].change_type == "removed"

    def test_modified(self):
        drifts = _compare_dicts({"k": "old"}, {"k": "new"}, "tools")
        assert len(drifts) == 1
        assert drifts[0].change_type == "modified"
        assert drifts[0].old_value == "old"
        assert drifts[0].new_value == "new"


# --- List Comparison ---

class TestCompareLists:
    def test_no_changes(self):
        drifts = _compare_lists(["a", "b"], ["a", "b"], "endpoints")
        assert len(drifts) == 0

    def test_added(self):
        drifts = _compare_lists(["a"], ["a", "b"], "endpoints")
        assert len(drifts) == 1
        assert drifts[0].change_type == "added"

    def test_removed(self):
        drifts = _compare_lists(["a", "b"], ["a"], "endpoints")
        assert len(drifts) == 1
        assert drifts[0].change_type == "removed"


# --- Snapshot Creation ---

class TestCreateSnapshot:
    def test_from_json_file(self, tmp_path):
        config = {
            "mcpServers": {
                "test-server": {
                    "command": "python",
                    "args": ["server.py"],
                    "tools": {"read": {}, "write": {}},
                    "env": {"API_KEY": "test"},
                }
            }
        }
        f = tmp_path / "mcp_config.json"
        f.write_text(json.dumps(config))
        snap = create_snapshot(f)
        assert len(snap.tools) == 2
        assert "test-server.API_KEY" in snap.env_vars
        assert snap.config_hash != ""

    def test_from_directory(self, tmp_path):
        config = {"mcpServers": {"s": {"command": "node", "tools": ["t1"]}}}
        f = tmp_path / "mcp_config.json"
        f.write_text(json.dumps(config))
        snap = create_snapshot(tmp_path)
        assert snap.config_hash != ""

    def test_nonexistent_file(self, tmp_path):
        snap = create_snapshot(tmp_path / "nonexistent.json")
        assert snap.config_hash == ""

    def test_from_dict(self):
        config = {
            "mcpServers": {
                "s1": {"command": "python", "tools": {"t1": {}}, "env": {"KEY": "val"}},
            }
        }
        snap = create_snapshot_from_dict(config, path="/test")
        assert snap.path == "/test"
        assert "s1.t1" in snap.tools


# --- Save/Load Snapshot ---

class TestSaveLoadSnapshot:
    def test_roundtrip(self, tmp_path):
        original = ConfigSnapshot(
            path="/test",
            timestamp="2024-01-01T00:00:00Z",
            config_hash="abc123",
            tools={"tool1": {"desc": "test"}},
            permissions={"read": True},
            env_vars={"KEY": "val"},
            endpoints=["s1: python server.py"],
        )
        out = tmp_path / "snapshot.json"
        save_snapshot(original, out)
        loaded = load_snapshot(out)
        assert loaded.path == original.path
        assert loaded.config_hash == original.config_hash
        assert loaded.tools == original.tools


# --- Drift Check ---

class TestCheckDrift:
    def test_no_drift(self):
        baseline = ConfigSnapshot(tools={"t1": {}}, env_vars={"K": "v"})
        current = ConfigSnapshot(tools={"t1": {}}, env_vars={"K": "v"})
        report = check_drift(baseline, current)
        assert report.total_drifts == 0

    def test_tool_added(self):
        baseline = ConfigSnapshot(tools={"t1": {}})
        current = ConfigSnapshot(tools={"t1": {}, "t2": {}})
        report = check_drift(baseline, current)
        assert report.total_drifts == 1
        assert report.drifts[0].category == "tools"
        assert report.drifts[0].change_type == "added"

    def test_tool_removed(self):
        baseline = ConfigSnapshot(tools={"t1": {}, "t2": {}})
        current = ConfigSnapshot(tools={"t1": {}})
        report = check_drift(baseline, current)
        assert report.total_drifts == 1
        assert report.drifts[0].change_type == "removed"

    def test_permission_changed(self):
        baseline = ConfigSnapshot(permissions={"s1": {"read": True}})
        current = ConfigSnapshot(permissions={"s1": {"read": True, "write": True}})
        report = check_drift(baseline, current)
        assert report.total_drifts == 1
        assert report.drifts[0].severity == DriftSeverity.CRITICAL

    def test_env_var_added(self):
        baseline = ConfigSnapshot(env_vars={})
        current = ConfigSnapshot(env_vars={"NEW_KEY": "val"})
        report = check_drift(baseline, current)
        assert report.total_drifts == 1

    def test_endpoint_added(self):
        baseline = ConfigSnapshot(endpoints=["s1: python"])
        current = ConfigSnapshot(endpoints=["s1: python", "s2: node"])
        report = check_drift(baseline, current)
        assert report.total_drifts == 1

    def test_multiple_drifts(self):
        baseline = ConfigSnapshot(
            tools={"t1": {}},
            permissions={"s1": {"read": True}},
            env_vars={"K": "v"},
        )
        current = ConfigSnapshot(
            tools={"t1": {}, "t2": {}},
            permissions={"s1": {"read": True, "write": True}},
            env_vars={"K": "v2"},
        )
        report = check_drift(baseline, current)
        assert report.total_drifts == 3


# --- Drift from File ---

class TestCheckDriftFromFile:
    def test_full_flow(self, tmp_path):
        # Create baseline
        baseline_config = {
            "mcpServers": {
                "s1": {"command": "python", "tools": {"t1": {}}, "env": {"K": "v"}},
            }
        }
        baseline_snap = create_snapshot_from_dict(baseline_config, "/baseline")
        baseline_file = tmp_path / "baseline.json"
        save_snapshot(baseline_snap, baseline_file)

        # Create current config (with drift)
        current_config = {
            "mcpServers": {
                "s1": {
                    "command": "python",
                    "tools": {"t1": {}, "t2_new": {}},
                    "env": {"K": "v", "SECRET": "leaked"},
                },
            }
        }
        config_file = tmp_path / "mcp_config.json"
        config_file.write_text(json.dumps(current_config))

        report = check_drift_from_file(baseline_file, config_file)
        assert report.total_drifts >= 2  # new tool + new env var


# --- Find Config File ---

class TestFindConfigFile:
    def test_file_path(self, tmp_path):
        f = tmp_path / "test.json"
        f.write_text("{}")
        assert _find_config_file(f) == f

    def test_directory_with_config(self, tmp_path):
        f = tmp_path / "mcp_config.json"
        f.write_text("{}")
        result = _find_config_file(tmp_path)
        assert result == f

    def test_directory_without_config(self, tmp_path):
        result = _find_config_file(tmp_path)
        assert result is None

    def test_claude_desktop_config(self, tmp_path):
        f = tmp_path / "claude_desktop_config.json"
        f.write_text("{}")
        result = _find_config_file(tmp_path)
        assert result == f


# --- Formatters ---

class TestFormatDriftText:
    def test_no_drift(self):
        r = DriftReport()
        text = format_drift_text(r)
        assert "No configuration drift" in text

    def test_with_drifts(self):
        r = DriftReport(drifts=[
            DriftItem("tools", "added", "new_tool",
                      new_value="def", severity=DriftSeverity.HIGH,
                      recommendation="Review"),
        ])
        text = format_drift_text(r)
        assert "TOOLS" in text
        assert "added" in text

    def test_severity_breakdown(self):
        r = DriftReport(
            baseline_path="/a",
            current_path="/b",
            drifts=[
                DriftItem("tools", "added", "t1", severity=DriftSeverity.HIGH),
                DriftItem("permissions", "modified", "p1", severity=DriftSeverity.CRITICAL),
            ],
        )
        text = format_drift_text(r)
        assert "critical=1" in text
        assert "high=1" in text


class TestFormatDriftJson:
    def test_valid_json(self):
        r = DriftReport(
            baseline_path="/a",
            current_path="/b",
            drifts=[
                DriftItem("tools", "added", "t1", severity=DriftSeverity.HIGH),
            ],
        )
        text = format_drift_json(r)
        data = json.loads(text)
        assert data["total_drifts"] == 1
        assert len(data["drifts"]) == 1
