"""Core tests for MCP Bridge - registry, instance manager, meta-tools."""

from __future__ import annotations

import os
import tempfile
from pathlib import Path

import pytest

from mcp_bridge.credentials.store import CompositeStore, EnvironmentStore, MemoryStore
from mcp_bridge.metrics.collector import MetricsCollector
from mcp_bridge.registry.catalog import ServerCatalog
from mcp_bridge.registry.models import (
    ConfigOption,
    ServerCategory,
    ServerEntry,
    ToolFilter,
    TransportType,
)


@pytest.fixture
def tmp_dir():
    """Create a temporary directory for test data."""
    with tempfile.TemporaryDirectory() as d:
        yield Path(d)


@pytest.fixture
async def catalog(tmp_dir, monkeypatch):
    """Create a test catalog with temporary database (no default catalog files)."""
    db_path = tmp_dir / "test.db"

    # Override the settings so catalog files aren't loaded from the real catalog/ dir
    from mcp_bridge.config import settings as settings_mod

    settings_mod.reset_settings()
    monkeypatch.setenv("MCP_BRIDGE_DATA_DIR", str(tmp_dir))
    monkeypatch.setenv("MCP_BRIDGE_CATALOG_DIR", str(tmp_dir / "empty_catalog"))
    (tmp_dir / "empty_catalog").mkdir()
    settings_mod.reset_settings()

    cat = ServerCatalog(db_path=db_path)
    await cat.initialize()
    yield cat
    await cat.close()
    settings_mod.reset_settings()


@pytest.fixture
async def metrics(tmp_dir):
    """Create a test metrics collector."""
    db_path = tmp_dir / "metrics.db"
    mc = MetricsCollector(db_path=db_path)
    await mc.initialize()
    yield mc
    await mc.close()


def _sample_server(server_id: str = "test-server", name: str = "Test Server") -> ServerEntry:
    """Create a sample server entry for testing."""
    return ServerEntry(
        id=server_id,
        name=name,
        description="A test MCP server",
        category=ServerCategory.UTILITIES,
        command="echo",
        args=["hello"],
        transport=TransportType.STDIO,
        tags=["test", "utility"],
        configurable_options=[
            ConfigOption(
                name="verbose",
                type="boolean",
                description="Enable verbose output",
                default=False,
            )
        ],
        max_instances=3,
    )


# --- Registry Tests ---


class TestServerCatalog:
    """Tests for the server catalog/registry."""

    @pytest.mark.asyncio
    async def test_register_and_get_server(self, catalog):
        """Test registering and retrieving a server."""
        server = _sample_server()
        await catalog.register_server(server)

        result = await catalog.get_server("test-server")
        assert result is not None
        assert result.id == "test-server"
        assert result.name == "Test Server"
        assert result.category == ServerCategory.UTILITIES

    @pytest.mark.asyncio
    async def test_list_servers(self, catalog):
        """Test listing servers with filters."""
        await catalog.register_server(_sample_server("server-1", "Browser Server"))
        await catalog.register_server(
            ServerEntry(
                id="server-2",
                name="DB Server",
                category=ServerCategory.DATA_ANALYTICS,
                command="echo",
                tags=["database"],
            )
        )

        # List all
        all_servers = await catalog.list_servers(enabled_only=False)
        assert len(all_servers) == 2

        # Filter by category
        utility_servers = await catalog.list_servers(category="utilities")
        assert len(utility_servers) == 1
        assert utility_servers[0].id == "server-1"

        # Search
        search_results = await catalog.list_servers(search="Browser")
        assert len(search_results) == 1

    @pytest.mark.asyncio
    async def test_enable_disable_server(self, catalog):
        """Test enabling and disabling servers."""
        server = _sample_server()
        await catalog.register_server(server)

        # Disable
        await catalog.disable_server("test-server")
        result = await catalog.get_server("test-server")
        assert result is not None
        assert result.enabled is False

        # Should not appear in enabled-only listing
        enabled = await catalog.list_servers(enabled_only=True)
        assert len(enabled) == 0

        # Re-enable
        await catalog.enable_server("test-server")
        result = await catalog.get_server("test-server")
        assert result is not None
        assert result.enabled is True

    @pytest.mark.asyncio
    async def test_unregister_server(self, catalog):
        """Test removing a server."""
        await catalog.register_server(_sample_server())
        assert await catalog.get_server("test-server") is not None

        success = await catalog.unregister_server("test-server")
        assert success is True
        assert await catalog.get_server("test-server") is None

    @pytest.mark.asyncio
    async def test_update_tool_filter(self, catalog):
        """Test updating tool filters."""
        await catalog.register_server(_sample_server())

        tool_filter = ToolFilter(enabled_tools=["tool1", "tool2"], disabled_tools=None)
        await catalog.update_tool_filter("test-server", tool_filter)

        result = await catalog.get_server("test-server")
        assert result is not None
        assert result.tool_filter.enabled_tools == ["tool1", "tool2"]

    @pytest.mark.asyncio
    async def test_import_mcp_json(self, catalog):
        """Test importing from MCP JSON config format."""
        config = {
            "mcpServers": {
                "My Playwright": {
                    "command": "npx",
                    "args": ["@playwright/mcp"],
                    "env": {"HEADLESS": "true"},
                },
                "My GitHub": {
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-github"],
                    "env": {"GITHUB_TOKEN": "xxx"},
                },
            }
        }

        imported = await catalog.import_from_mcp_json(config, source="test")
        assert len(imported) == 2

        # Verify they're in the catalog
        pw = await catalog.get_server("my-playwright")
        assert pw is not None
        assert pw.command == "npx"

    @pytest.mark.asyncio
    async def test_list_categories(self, catalog):
        """Test listing categories with counts."""
        await catalog.register_server(_sample_server("s1"))
        await catalog.register_server(
            ServerEntry(
                id="s2",
                name="S2",
                category=ServerCategory.DATA_ANALYTICS,
                command="echo",
            )
        )
        await catalog.register_server(
            ServerEntry(
                id="s3",
                name="S3",
                category=ServerCategory.DATA_ANALYTICS,
                command="echo",
            )
        )

        categories = await catalog.list_categories()
        assert len(categories) == 2

        cat_counts = {c["category"]: c["count"] for c in categories}
        assert cat_counts["utilities"] == 1
        assert cat_counts["data-analytics"] == 2

    @pytest.mark.asyncio
    async def test_server_count(self, catalog):
        """Test getting server count."""
        assert await catalog.get_server_count() == 0
        await catalog.register_server(_sample_server())
        assert await catalog.get_server_count() == 1


# --- Credential Store Tests ---


class TestCredentialStore:
    """Tests for credential stores."""

    @pytest.mark.asyncio
    async def test_memory_store(self):
        """Test in-memory credential store."""
        store = MemoryStore()

        await store.set("API_KEY", "secret123")
        assert await store.get("API_KEY") == "secret123"
        assert await store.get("NONEXISTENT") is None

        keys = await store.list_keys()
        assert "API_KEY" in keys

        await store.delete("API_KEY")
        assert await store.get("API_KEY") is None

    @pytest.mark.asyncio
    async def test_environment_store(self):
        """Test environment variable store."""
        store = EnvironmentStore()

        os.environ["TEST_MCP_API_KEY"] = "env_secret"
        assert await store.get("TEST_MCP_API_KEY") == "env_secret"

        del os.environ["TEST_MCP_API_KEY"]

    @pytest.mark.asyncio
    async def test_composite_store(self):
        """Test composite store cascading."""
        memory = MemoryStore()
        env = EnvironmentStore()
        composite = CompositeStore([memory, env])

        # Set in environment
        os.environ["TEST_COMPOSITE_KEY"] = "from_env"

        # Memory is empty, should fall through to env
        assert await composite.get("TEST_COMPOSITE_KEY") == "from_env"

        # Set in memory (primary), should now return memory value
        await composite.set("TEST_COMPOSITE_KEY", "from_memory")
        assert await composite.get("TEST_COMPOSITE_KEY") == "from_memory"

        # Cleanup
        del os.environ["TEST_COMPOSITE_KEY"]


# --- Metrics Tests ---


class TestMetricsCollector:
    """Tests for the metrics collector."""

    @pytest.mark.asyncio
    async def test_record_and_query(self, metrics):
        """Test recording and querying tool call metrics."""
        await metrics.record_tool_call(
            agent_session_id="agent-1",
            instance_id="inst-1",
            server_id="playwright-mcp",
            tool_name="navigate",
            success=True,
            latency_ms=150.5,
            input_size_bytes=100,
            output_size_bytes=5000,
        )

        await metrics.record_tool_call(
            agent_session_id="agent-1",
            instance_id="inst-1",
            server_id="playwright-mcp",
            tool_name="click",
            success=False,
            latency_ms=50.0,
            error_message="Element not found",
        )

        # Get summary
        summary = await metrics.get_summary(hours=1)
        assert summary["total_calls"] == 2
        assert summary["success_rate"] == 50.0
        assert len(summary["per_server"]) == 1
        assert summary["per_server"][0]["server_id"] == "playwright-mcp"

        # Get recent calls
        calls = await metrics.get_recent_calls(limit=10)
        assert len(calls) == 2

    @pytest.mark.asyncio
    async def test_instance_events(self, metrics):
        """Test recording instance lifecycle events."""
        await metrics.record_instance_event(
            instance_id="inst-1",
            server_id="playwright-mcp",
            agent_session_id="agent-1",
            event_type="activated",
            details={"tools_count": 15},
        )

        summary = await metrics.get_summary(hours=1)
        assert "activated" in summary["instance_events"]
        assert summary["instance_events"]["activated"] == 1

    @pytest.mark.asyncio
    async def test_export(self, metrics):
        """Test exporting metrics as JSON."""
        await metrics.record_tool_call(
            agent_session_id="a1",
            instance_id="i1",
            server_id="s1",
            tool_name="t1",
            success=True,
            latency_ms=100,
        )

        export = await metrics.export_json(hours=1)
        assert "summary" in export
        assert "recent_calls" in export
        assert "exported_at" in export


# --- Model Tests ---


class TestModels:
    """Tests for Pydantic models."""

    def test_server_entry_creation(self):
        """Test creating a server entry."""
        entry = _sample_server()
        assert entry.id == "test-server"
        assert entry.transport == TransportType.STDIO
        assert len(entry.configurable_options) == 1

    def test_tool_filter(self):
        """Test tool filter model."""
        tf = ToolFilter(enabled_tools=["a", "b"], disabled_tools=None)
        assert tf.enabled_tools == ["a", "b"]

        # Serialize and deserialize
        data = tf.model_dump_json()
        tf2 = ToolFilter.model_validate_json(data)
        assert tf2.enabled_tools == ["a", "b"]

    def test_server_entry_json_roundtrip(self):
        """Test server entry serialization."""
        entry = _sample_server()
        data = entry.model_dump(mode="json")
        entry2 = ServerEntry(**data)
        assert entry2.id == entry.id
        assert entry2.command == entry.command
