"""Tests for health_check action, CLI command, and MCP tool.

Validates:
- Daemon handler returns correct structure with all fields
- Each field has the expected type
- Error handling: handler doesn't crash if database file is missing or stats fail
- CLI health subcommand exists and is callable
- MCP tool exists and returns formatted text
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
import pytest_asyncio

from claude_engram.daemon import MemoryDaemon


@pytest_asyncio.fixture()
async def daemon(tmp_path: Path) -> MemoryDaemon:
    """Create a MemoryDaemon with a temp database."""
    with patch("claude_engram.daemon.MemoryStorage") as mock_cls:
        from claude_engram.storage import MemoryStorage

        real_storage = MemoryStorage(db_path=tmp_path / "test.db")
        mock_cls.return_value = real_storage
        d = MemoryDaemon()
        d.storage = real_storage
        yield d
        real_storage.close()


class TestHealthCheckDaemon:
    """Tests for health_check daemon action."""

    @pytest.mark.asyncio()
    async def test_health_check_returns_correct_structure(self, daemon: MemoryDaemon) -> None:
        """Health check should return status ok and health dict with all required fields."""
        result = await daemon._process_request({"action": "health_check"})

        assert result["status"] == "ok"
        assert "health" in result

        health = result["health"]
        # Verify all required fields exist
        required_fields = [
            "db_size_bytes",
            "memory_count",
            "oldest_memory",
            "newest_memory",
            "wal_size_bytes",
            "socket_path",
            "socket_exists",
            "pid",
            "pid_file_exists",
            "embedding_model_loaded",
            "cache_size",
            "checkpoint_sessions",
            "uptime_info",
        ]
        for field in required_fields:
            assert field in health, f"Missing required field: {field}"

    @pytest.mark.asyncio()
    async def test_health_check_field_types(self, daemon: MemoryDaemon) -> None:
        """Each health field should have the expected type."""
        result = await daemon._process_request({"action": "health_check"})
        health = result["health"]

        # Integer fields
        assert isinstance(health["db_size_bytes"], int)
        assert isinstance(health["memory_count"], int)
        assert isinstance(health["wal_size_bytes"], int)
        assert isinstance(health["pid"], int)
        assert isinstance(health["cache_size"], int)
        assert isinstance(health["checkpoint_sessions"], int)

        # String fields
        assert isinstance(health["socket_path"], str)
        assert isinstance(health["uptime_info"], str)

        # Optional string fields (can be None or str)
        assert health["oldest_memory"] is None or isinstance(health["oldest_memory"], str)
        assert health["newest_memory"] is None or isinstance(health["newest_memory"], str)

        # Boolean fields
        assert isinstance(health["socket_exists"], bool)
        assert isinstance(health["pid_file_exists"], bool)
        assert isinstance(health["embedding_model_loaded"], bool)

    @pytest.mark.asyncio()
    async def test_health_check_with_memories(self, daemon: MemoryDaemon) -> None:
        """Health check should report memory count and timestamps when memories exist."""
        # Store a couple of memories
        await daemon._process_request(
            {"action": "store", "content": "Test memory 1", "category": "general"}
        )
        await daemon._process_request(
            {"action": "store", "content": "Test memory 2", "category": "lesson"}
        )

        result = await daemon._process_request({"action": "health_check"})
        health = result["health"]

        assert health["memory_count"] == 2
        assert health["oldest_memory"] is not None
        assert health["newest_memory"] is not None
        # Timestamps should be ISO format
        assert "T" in health["oldest_memory"]
        assert "T" in health["newest_memory"]

    @pytest.mark.asyncio()
    async def test_health_check_with_empty_database(self, daemon: MemoryDaemon) -> None:
        """Health check should handle empty database gracefully."""
        result = await daemon._process_request({"action": "health_check"})
        health = result["health"]

        assert health["memory_count"] == 0
        assert health["oldest_memory"] is None
        assert health["newest_memory"] is None

    @pytest.mark.asyncio()
    async def test_health_check_cache_size(self, daemon: MemoryDaemon) -> None:
        """Health check should report cache size correctly."""
        result = await daemon._process_request({"action": "health_check"})
        initial_cache_size = result["health"]["cache_size"]
        assert initial_cache_size == 0  # Initially empty

        # Directly add to cache via the cache object to ensure it's populated
        daemon.cache.put("test query", {"context": "test result", "count": 1})

        result = await daemon._process_request({"action": "health_check"})
        new_cache_size = result["health"]["cache_size"]

        # Cache size should now be 1
        assert new_cache_size == 1

    @pytest.mark.asyncio()
    async def test_health_check_checkpoint_sessions(self, daemon: MemoryDaemon) -> None:
        """Health check should report active checkpoint sessions."""
        result = await daemon._process_request({"action": "health_check"})
        assert result["health"]["checkpoint_sessions"] == 0

        # Start a checkpoint session
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "test-session-1",
                "profile": "balanced",
            }
        )

        result = await daemon._process_request({"action": "health_check"})
        assert result["health"]["checkpoint_sessions"] == 1

        # Start another session
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "test-session-2",
                "profile": "ultra",
            }
        )

        result = await daemon._process_request({"action": "health_check"})
        assert result["health"]["checkpoint_sessions"] == 2

    @pytest.mark.asyncio()
    async def test_health_check_error_handling(self, daemon: MemoryDaemon) -> None:
        """Health check should return error status on failure."""
        # Simulate a failure by mocking storage.stats to raise an exception
        with patch.object(daemon.storage, "stats", side_effect=Exception("DB error")):
            result = await daemon._process_request({"action": "health_check"})

            assert result["status"] == "error"
            assert "error" in result
            assert "DB error" in result["error"]


class TestHealthCheckCLI:
    """Tests for CLI health command."""

    def test_health_command_exists(self) -> None:
        """Health command should be registered in CLI."""
        from claude_engram.cli import main

        with (
            patch("sys.argv", ["claude-engram", "health", "--help"]),
            pytest.raises(SystemExit) as exc_info,
        ):
            main()

        # --help should exit with 0
        assert exc_info.value.code == 0

    @patch("claude_engram.cli.send_command")
    def test_health_command_sends_correct_action(self, mock_send: MagicMock) -> None:
        """Health command should send health_check action to daemon."""
        from claude_engram.cli import cmd_health

        mock_send.return_value = {
            "status": "ok",
            "health": {
                "db_size_bytes": 1024,
                "memory_count": 5,
                "oldest_memory": "2025-01-01T00:00:00",
                "newest_memory": "2025-01-02T00:00:00",
                "wal_size_bytes": 512,
                "socket_path": "/tmp/test.sock",
                "socket_exists": True,
                "pid": 12345,
                "pid_file_exists": True,
                "embedding_model_loaded": True,
                "cache_size": 3,
                "checkpoint_sessions": 2,
                "uptime_info": "pid file exists",
            },
        }

        args = argparse.Namespace(json_output=False)
        cmd_health(args)

        mock_send.assert_called_once_with({"action": "health_check"})

    @patch("claude_engram.cli.send_command")
    def test_health_command_json_output(
        self, mock_send: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Health command should support --json flag."""
        from claude_engram.cli import cmd_health

        response = {
            "status": "ok",
            "health": {
                "db_size_bytes": 2048,
                "memory_count": 10,
                "oldest_memory": None,
                "newest_memory": None,
                "wal_size_bytes": 0,
                "socket_path": "/tmp/test.sock",
                "socket_exists": True,
                "pid": 99999,
                "pid_file_exists": True,
                "embedding_model_loaded": False,
                "cache_size": 0,
                "checkpoint_sessions": 0,
                "uptime_info": "pid file exists",
            },
        }
        mock_send.return_value = response

        args = argparse.Namespace(json_output=True)
        cmd_health(args)

        captured = capsys.readouterr()
        # Output should be valid JSON
        output_json = json.loads(captured.out)
        assert output_json == response

    @patch("claude_engram.cli.send_command")
    def test_health_command_human_readable_output(
        self, mock_send: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Health command should print human-readable output by default."""
        from claude_engram.cli import cmd_health

        mock_send.return_value = {
            "status": "ok",
            "health": {
                "db_size_bytes": 12345678,
                "memory_count": 19,
                "oldest_memory": "2025-12-15T10:30:00",
                "newest_memory": "2026-01-31T14:22:00",
                "wal_size_bytes": 4096,
                "socket_path": "/tmp/claude-engram.sock",
                "socket_exists": True,
                "pid": 12345,
                "pid_file_exists": True,
                "embedding_model_loaded": True,
                "cache_size": 5,
                "checkpoint_sessions": 2,
                "uptime_info": "pid file exists",
            },
        }

        args = argparse.Namespace(json_output=False)
        cmd_health(args)

        captured = capsys.readouterr()
        output = captured.out

        # Should contain formatted output
        assert "Health Check:" in output
        assert "Database:" in output
        assert "19 memories" in output
        assert "MB" in output
        assert "Socket:" in output
        assert "/tmp/claude-engram.sock" in output
        assert "active" in output
        assert "PID:" in output
        assert "12345" in output

    @patch("claude_engram.cli.send_command")
    def test_health_command_error_handling(
        self, mock_send: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Health command should handle error responses gracefully."""
        from claude_engram.cli import cmd_health

        mock_send.return_value = {"status": "error", "error": "Database connection failed"}

        args = argparse.Namespace(json_output=False)
        cmd_health(args)

        captured = capsys.readouterr()
        assert "Health check failed" in captured.out
        assert "Database connection failed" in captured.out


class TestHealthCheckMCP:
    """Tests for MCP health_check tool."""

    @pytest.mark.asyncio()
    async def test_health_check_tool_exists(self) -> None:
        """Health check MCP tool should be registered."""
        from claude_engram.mcp_server import mcp

        # Check that health_check is in the registered tools
        tools = await mcp.list_tools()
        tool_names = [tool.name for tool in tools]
        assert "health_check" in tool_names

    @pytest.mark.asyncio()
    async def test_health_check_tool_returns_formatted_text(self) -> None:
        """Health check tool should return human-readable text."""
        from claude_engram.mcp_server import health_check

        mock_response = {
            "status": "ok",
            "health": {
                "db_size_bytes": 10485760,  # 10 MB
                "memory_count": 42,
                "oldest_memory": "2025-01-01T00:00:00",
                "newest_memory": "2026-01-31T23:59:59",
                "wal_size_bytes": 2048,
                "socket_path": "/tmp/test.sock",
                "socket_exists": True,
                "pid": 54321,
                "pid_file_exists": True,
                "embedding_model_loaded": True,
                "cache_size": 7,
                "checkpoint_sessions": 3,
                "uptime_info": "pid file exists",
            },
        }

        with patch("claude_engram.mcp_server._send_to_daemon", return_value=mock_response):
            result = await health_check()

            # Should be XML-formatted
            assert isinstance(result, str)
            assert "<health_status>" in result
            assert "</health_status>" in result
            assert 'memories="42"' in result
            assert 'size_mb="10.0"' in result
            assert 'wal_kb="2.0"' in result
            assert 'path="/tmp/test.sock"' in result
            assert 'status="active"' in result
            assert 'pid="54321"' in result
            assert 'status="loaded"' in result
            assert 'entries="7"' in result
            assert 'count="3"' in result  # checkpoint sessions

    @pytest.mark.asyncio()
    async def test_health_check_tool_error_handling(self) -> None:
        """Health check tool should handle errors gracefully."""
        from claude_engram.mcp_server import health_check

        with patch(
            "claude_engram.mcp_server._send_to_daemon",
            side_effect=ConnectionError("Daemon not running"),
        ):
            result = await health_check()

            assert "Health check unavailable" in result
            assert "Daemon not running" in result
