"""Tests for the Blocklist Engine."""

from __future__ import annotations

import json
import os
import tempfile
from unittest.mock import MagicMock, patch

import pytest

from navil.blocklist import BlocklistEntry, BlocklistManager, REDIS_PATTERNS_KEY, REDIS_VERSION_KEY


# ── BlocklistEntry ───────────────────────────────────────────


class TestBlocklistEntry:
    """Tests for the BlocklistEntry dataclass."""

    def test_creation(self):
        entry = BlocklistEntry(
            pattern_id="BL-TEST-001",
            pattern_type="tool_name",
            value="inject_backdoor",
            severity="CRITICAL",
            description="Test entry",
            confidence=0.95,
        )
        assert entry.pattern_id == "BL-TEST-001"
        assert entry.pattern_type == "tool_name"
        assert entry.confidence == 0.95
        assert entry.created_at != ""  # auto-populated

    def test_to_dict(self):
        entry = BlocklistEntry(
            pattern_id="BL-TEST-001",
            pattern_type="tool_name",
            value="test",
            severity="HIGH",
            description="desc",
            confidence=0.8,
            created_at="2026-01-01T00:00:00Z",
        )
        d = entry.to_dict()
        assert d["pattern_id"] == "BL-TEST-001"
        assert d["confidence"] == 0.8

    def test_from_dict(self):
        d = {
            "pattern_id": "BL-TEST-002",
            "pattern_type": "argument_pattern",
            "value": ".*\\.ssh/.*",
            "severity": "CRITICAL",
            "description": "SSH key access",
            "confidence": 0.9,
        }
        entry = BlocklistEntry.from_dict(d)
        assert entry.pattern_id == "BL-TEST-002"
        assert entry.pattern_type == "argument_pattern"

    def test_roundtrip(self):
        entry = BlocklistEntry(
            pattern_id="BL-RT-001",
            pattern_type="tool_sequence",
            value="read_file,fetch_url",
            severity="HIGH",
            description="Read-then-send",
            confidence=0.85,
        )
        d = entry.to_dict()
        restored = BlocklistEntry.from_dict(d)
        assert restored.pattern_id == entry.pattern_id
        assert restored.value == entry.value
        assert restored.confidence == entry.confidence


# ── BlocklistManager Loading ─────────────────────────────────


class TestBlocklistManagerLoad:
    """Tests for loading blocklist data."""

    def test_load_from_default_file(self):
        mgr = BlocklistManager()
        loaded = mgr.load_from_file()
        assert loaded > 0
        assert mgr.pattern_count > 0
        assert mgr.version >= 1

    def test_load_from_custom_file(self):
        data = {
            "version": 42,
            "patterns": [
                {
                    "pattern_id": "TEST-001",
                    "pattern_type": "tool_name",
                    "value": "test_tool",
                    "severity": "LOW",
                    "description": "Test",
                    "confidence": 0.5,
                }
            ],
        }
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(data, f)
            f.flush()
            path = f.name

        try:
            mgr = BlocklistManager()
            loaded = mgr.load_from_file(path)
            assert loaded == 1
            assert mgr.version == 42
        finally:
            os.unlink(path)

    def test_load_nonexistent_file_raises(self):
        mgr = BlocklistManager()
        with pytest.raises(FileNotFoundError):
            mgr.load_from_file("/nonexistent/path.json")

    def test_load_from_redis(self):
        mock_redis = MagicMock()
        patterns = [
            {
                "pattern_id": "REDIS-001",
                "pattern_type": "tool_name",
                "value": "redis_tool",
                "severity": "HIGH",
                "description": "From Redis",
                "confidence": 0.8,
            }
        ]
        mock_redis.get.side_effect = lambda key: (
            json.dumps(patterns).encode() if key == REDIS_PATTERNS_KEY else b"5"
        )

        mgr = BlocklistManager(redis_client=mock_redis)
        loaded = mgr.load_from_redis()
        assert loaded == 1
        assert mgr.version == 5

    def test_load_from_redis_empty(self):
        mock_redis = MagicMock()
        mock_redis.get.return_value = None

        mgr = BlocklistManager(redis_client=mock_redis)
        loaded = mgr.load_from_redis()
        assert loaded == 0

    def test_load_from_redis_no_client(self):
        mgr = BlocklistManager()
        loaded = mgr.load_from_redis()
        assert loaded == 0


# ── BlocklistManager Save ────────────────────────────────────


class TestBlocklistManagerSave:
    """Tests for saving blocklist data."""

    def test_save_to_redis(self):
        mock_redis = MagicMock()
        mock_pipe = MagicMock()
        mock_redis.pipeline.return_value = mock_pipe
        mock_pipe.execute.return_value = [True, 3]  # SET result, INCR result

        mgr = BlocklistManager(redis_client=mock_redis)
        mgr._entries["TEST-001"] = BlocklistEntry(
            pattern_id="TEST-001",
            pattern_type="tool_name",
            value="test",
            severity="HIGH",
            description="Test",
            confidence=0.9,
        )

        result = mgr.save_to_redis()
        assert result is True
        assert mgr.version == 3
        mock_pipe.set.assert_called_once()
        mock_pipe.incr.assert_called_once_with(REDIS_VERSION_KEY)

    def test_save_to_redis_no_client(self):
        mgr = BlocklistManager()
        result = mgr.save_to_redis()
        assert result is False

    def test_save_to_file(self):
        mgr = BlocklistManager()
        mgr._entries["TEST-001"] = BlocklistEntry(
            pattern_id="TEST-001",
            pattern_type="tool_name",
            value="test",
            severity="HIGH",
            description="Test",
            confidence=0.9,
        )
        mgr._version = 7

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            path = f.name

        try:
            mgr.save_to_file(path)
            with open(path) as fh:
                data = json.load(fh)
            assert data["version"] == 7
            assert len(data["patterns"]) == 1
        finally:
            os.unlink(path)


# ── BlocklistManager Merge ───────────────────────────────────


class TestBlocklistManagerMerge:
    """Tests for merging blocklist entries."""

    def test_merge_new_entries(self):
        mgr = BlocklistManager()
        entries = [
            BlocklistEntry("NEW-001", "tool_name", "tool_a", "HIGH", "A", 0.8),
            BlocklistEntry("NEW-002", "tool_name", "tool_b", "MEDIUM", "B", 0.7),
        ]
        changes = mgr.merge(entries)
        assert changes == 2
        assert mgr.pattern_count == 2

    def test_merge_conflict_keeps_higher_confidence(self):
        mgr = BlocklistManager()
        mgr._entries["CONF-001"] = BlocklistEntry(
            "CONF-001", "tool_name", "tool_a", "HIGH", "Original", 0.7
        )

        higher = [BlocklistEntry("CONF-001", "tool_name", "tool_a", "HIGH", "Updated", 0.9)]
        changes = mgr.merge(higher)
        assert changes == 1
        assert mgr._entries["CONF-001"].confidence == 0.9
        assert mgr._entries["CONF-001"].description == "Updated"

    def test_merge_conflict_keeps_existing_when_lower(self):
        mgr = BlocklistManager()
        mgr._entries["CONF-001"] = BlocklistEntry(
            "CONF-001", "tool_name", "tool_a", "HIGH", "Original", 0.9
        )

        lower = [BlocklistEntry("CONF-001", "tool_name", "tool_a", "HIGH", "Lower", 0.5)]
        changes = mgr.merge(lower)
        assert changes == 0
        assert mgr._entries["CONF-001"].description == "Original"


# ── BlocklistManager Match ───────────────────────────────────


class TestBlocklistManagerMatch:
    """Tests for matching tool calls against the blocklist."""

    @pytest.fixture
    def loaded_manager(self):
        mgr = BlocklistManager()
        mgr.load_from_file()
        return mgr

    def test_match_tool_name(self, loaded_manager):
        matches = loaded_manager.match("inject_backdoor")
        assert len(matches) >= 1
        assert any(m.pattern_id == "BL-TOOL-001" for m in matches)

    def test_match_tool_name_no_match(self, loaded_manager):
        matches = loaded_manager.match("safe_normal_tool")
        assert len(matches) == 0

    def test_match_argument_pattern(self, loaded_manager):
        matches = loaded_manager.match("read_file", {"path": "~/.ssh/id_rsa"})
        assert len(matches) >= 1
        assert any(m.pattern_type == "argument_pattern" for m in matches)

    def test_match_aws_credentials(self, loaded_manager):
        matches = loaded_manager.match("read_file", {"path": "/home/user/.aws/credentials"})
        assert len(matches) >= 1

    def test_match_etc_shadow(self, loaded_manager):
        matches = loaded_manager.match("read_file", {"path": "/etc/shadow"})
        assert len(matches) >= 1

    def test_match_sequence_member(self, loaded_manager):
        matches = loaded_manager.match("fetch_url")
        seq_matches = [m for m in matches if m.pattern_type == "tool_sequence"]
        assert len(seq_matches) >= 1

    def test_matches_sorted_by_confidence(self, loaded_manager):
        # Add entries with varying confidence
        loaded_manager.merge([
            BlocklistEntry("SORT-001", "tool_name", "test_sort", "HIGH", "A", 0.5),
            BlocklistEntry("SORT-002", "tool_name", "test_sort", "HIGH", "B", 0.9),
            BlocklistEntry("SORT-003", "tool_name", "test_sort", "HIGH", "C", 0.7),
        ])
        matches = loaded_manager.match("test_sort")
        assert len(matches) >= 2
        for i in range(len(matches) - 1):
            assert matches[i].confidence >= matches[i + 1].confidence


# ── BlocklistManager Search ──────────────────────────────────


class TestBlocklistManagerSearch:
    """Tests for searching blocklist entries."""

    @pytest.fixture
    def loaded_manager(self):
        mgr = BlocklistManager()
        mgr.load_from_file()
        return mgr

    def test_search_by_id(self, loaded_manager):
        results = loaded_manager.search("BL-TOOL-001")
        assert len(results) >= 1

    def test_search_by_description(self, loaded_manager):
        results = loaded_manager.search("SSH")
        assert len(results) >= 1

    def test_search_by_value(self, loaded_manager):
        results = loaded_manager.search("inject_backdoor")
        assert len(results) >= 1

    def test_search_case_insensitive(self, loaded_manager):
        results = loaded_manager.search("ssh")
        assert len(results) >= 1

    def test_search_no_results(self, loaded_manager):
        results = loaded_manager.search("zzz_nonexistent_zzz")
        assert len(results) == 0


# ── BlocklistManager Status ──────────────────────────────────


class TestBlocklistManagerStatus:
    """Tests for status reporting."""

    def test_status_after_load(self):
        mgr = BlocklistManager()
        mgr.load_from_file()
        status = mgr.status()

        assert status["version"] >= 1
        assert status["pattern_count"] > 0
        assert "by_type" in status
        assert "by_severity" in status
        assert "tool_name" in status["by_type"]
        assert status["last_update"] != ""
