"""Integration test for MCP endpoint routing.

This test verifies that /mcp/ and /mcp/{bank_id}/ expose different tool sets,
and that URLs with or without trailing slashes both work (no 307 redirect).
"""

import json
from unittest.mock import patch

import httpx
import pytest
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client


@pytest.mark.asyncio
async def test_mcp_endpoint_routing_integration(memory):
    """Test that multi-bank and single-bank endpoints expose different tools using StreamableHTTP.

    This is a regression test for issue #317 where /mcp/{bank_id}/ was incorrectly
    exposing all tools (including list_banks) and bank_id parameters.
    """
    from hindsight_api.api import create_app

    # Create app with MCP enabled
    app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

    # Use the app's lifespan context to properly initialize MCP servers
    async with app.router.lifespan_context(app):
        # Create an HTTPX client that routes to our ASGI app
        from httpx import ASGITransport

        async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
            # Test 1: Multi-bank endpoint /mcp/
            async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()
                    multi_result = await session.list_tools()

                    multi_tools = {t.name for t in multi_result.tools}

                    # Multi-bank should have all tools including bank management and mental models
                    assert "retain" in multi_tools
                    assert "recall" in multi_tools
                    assert "reflect" in multi_tools
                    assert "list_banks" in multi_tools, "Multi-bank should expose list_banks"
                    assert "create_bank" in multi_tools, "Multi-bank should expose create_bank"
                    assert "list_mental_models" in multi_tools, "Multi-bank should expose list_mental_models"
                    assert "create_mental_model" in multi_tools, "Multi-bank should expose create_mental_model"
                    assert "get_mental_model" in multi_tools, "Multi-bank should expose get_mental_model"
                    assert "update_mental_model" in multi_tools, "Multi-bank should expose update_mental_model"
                    assert "delete_mental_model" in multi_tools, "Multi-bank should expose delete_mental_model"
                    assert "refresh_mental_model" in multi_tools, "Multi-bank should expose refresh_mental_model"

                    # Multi-bank retain should have bank_id parameter
                    retain_tool = next((t for t in multi_result.tools if t.name == "retain"), None)
                    assert retain_tool is not None
                    multi_params = set(retain_tool.inputSchema.get("properties", {}).keys())
                    assert "bank_id" in multi_params, "Multi-bank retain should have bank_id parameter"

            # Test 2: Single-bank endpoint /mcp/test-bank/
            async with streamable_http_client("http://test/mcp/test-bank/", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()
                    single_result = await session.list_tools()

                    single_tools = {t.name for t in single_result.tools}

                    # Single-bank should have scoped tools including mental models (no bank management)
                    assert "retain" in single_tools
                    assert "recall" in single_tools
                    assert "reflect" in single_tools
                    assert "list_mental_models" in single_tools, "Single-bank should expose list_mental_models"
                    assert "create_mental_model" in single_tools, "Single-bank should expose create_mental_model"
                    assert "list_banks" not in single_tools, "Single-bank should NOT expose list_banks"
                    assert "create_bank" not in single_tools, "Single-bank should NOT expose create_bank"

                    # Single-bank retain should NOT have bank_id parameter
                    retain_tool = next((t for t in single_result.tools if t.name == "retain"), None)
                    assert retain_tool is not None
                    single_params = set(retain_tool.inputSchema.get("properties", {}).keys())
                    assert "bank_id" not in single_params, "Single-bank retain should NOT have bank_id parameter"


@pytest.mark.asyncio
async def test_mcp_no_trailing_slash_works(memory):
    """Test that /mcp (no trailing slash) discovers tools without 307 redirect.

    Starlette's Mount class redirects /mcp to /mcp/ with a 307 Temporary Redirect.
    Many MCP clients don't follow POST redirects, causing 0 tools to be discovered.
    MCPMiddleware wraps the app directly (no Mount), so the redirect never happens.
    """
    from hindsight_api.api import create_app

    app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

    async with app.router.lifespan_context(app):
        from httpx import ASGITransport

        async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
            # /mcp (no slash) should work the same as /mcp/
            async with streamable_http_client("http://test/mcp", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()
                    result = await session.list_tools()

                    tools = {t.name for t in result.tools}
                    assert len(tools) >= 11, f"Expected at least 11 tools from /mcp, got {len(tools)}: {tools}"
                    assert "retain" in tools
                    assert "recall" in tools
                    assert "list_banks" in tools

            # /mcp/my-bank (single-bank, no slash) should also work
            async with streamable_http_client("http://test/mcp/my-bank", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()
                    result = await session.list_tools()

                    tools = {t.name for t in result.tools}
                    assert "retain" in tools
                    assert "list_banks" not in tools, "Single-bank /mcp/my-bank should NOT expose list_banks"


@pytest.mark.asyncio
async def test_mcp_tool_execution_through_client(memory):
    """Test that tools can be called (not just discovered) through the MCP client.

    This verifies the full pipeline: HTTP → middleware → FastMCP → tool → engine → response.
    Previous tests only checked tool discovery (list_tools), not actual execution.
    """
    from httpx import ASGITransport

    from hindsight_api.api import create_app

    app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
            async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()

                    # Execute list_banks tool
                    result = await session.call_tool("list_banks", arguments={})
                    assert result is not None
                    assert len(result.content) > 0
                    # The result text should be valid JSON with a "banks" key
                    import json

                    response_text = result.content[0].text
                    parsed = json.loads(response_text)
                    assert "banks" in parsed


@pytest.mark.asyncio
async def test_mcp_mental_model_validation_through_client(memory):
    """Test that input validation works through the real MCP transport.

    Verifies that invalid inputs return error messages without crashing,
    and that the engine is never called with invalid data.
    """
    from httpx import ASGITransport

    from hindsight_api.api import create_app

    app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
            async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()

                    # Test: empty name should return validation error
                    import json

                    result = await session.call_tool(
                        "create_mental_model",
                        arguments={"name": "", "source_query": "test query"},
                    )
                    assert result is not None
                    parsed = json.loads(result.content[0].text)
                    assert "error" in parsed
                    assert "name cannot be empty" in parsed["error"]

                    # Test: max_tokens out of range should return validation error
                    result = await session.call_tool(
                        "create_mental_model",
                        arguments={"name": "Test", "source_query": "test query", "max_tokens": 0},
                    )
                    parsed = json.loads(result.content[0].text)
                    assert "error" in parsed
                    assert "max_tokens must be between 256 and 8192" in parsed["error"]


@pytest.mark.asyncio
async def test_mcp_bank_named_sse_routes_to_single_bank(memory):
    """Test that a bank named 'sse' routes to single-bank mode.

    Regression test: the old MCP_ENDPOINTS blocklist prevented banks named 'sse'
    or 'messages' from being accessed via path routing. They fell through to
    multi-bank mode instead.
    """
    from httpx import ASGITransport

    from hindsight_api.api import create_app

    app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
            async with streamable_http_client("http://test/mcp/sse/", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()
                    result = await session.list_tools()
                    tools = {t.name for t in result.tools}

                    # Should be single-bank mode (no bank management tools)
                    assert "retain" in tools
                    assert "recall" in tools
                    assert "list_banks" not in tools, "Bank 'sse' should route to single-bank mode"
                    assert "create_bank" not in tools

                    # retain should NOT have bank_id parameter (single-bank mode)
                    retain_tool = next(t for t in result.tools if t.name == "retain")
                    params = set(retain_tool.inputSchema.get("properties", {}).keys())
                    assert "bank_id" not in params


@pytest.mark.asyncio
async def test_mcp_bank_named_messages_routes_to_single_bank(memory):
    """Test that a bank named 'messages' routes to single-bank mode.

    Same regression test as test_mcp_bank_named_sse_routes_to_single_bank but for 'messages'.
    """
    from httpx import ASGITransport

    from hindsight_api.api import create_app

    app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

    async with app.router.lifespan_context(app):
        async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
            async with streamable_http_client("http://test/mcp/messages/", http_client=http_client) as (
                read_stream,
                write_stream,
                _,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()
                    result = await session.list_tools()
                    tools = {t.name for t in result.tools}

                    assert "retain" in tools
                    assert "list_banks" not in tools, "Bank 'messages' should route to single-bank mode"


@pytest.mark.asyncio
async def test_mcp_tool_execution_with_different_mcp_and_tenant_tokens(memory):
    """Test that MCP tool calls work when MCP_AUTH_TOKEN and TENANT_API_KEY differ.

    Regression test for https://github.com/vectorize-io/hindsight/issues/627
    When both HINDSIGHT_API_MCP_AUTH_TOKEN and ApiKeyTenantExtension are configured
    with different values, tool calls should succeed because MCP transport auth
    already validated the token — the tenant extension should not re-validate.
    """
    from httpx import ASGITransport

    from hindsight_api.api import create_app
    from hindsight_api.extensions import ApiKeyTenantExtension

    mcp_token = "mcp-secret-token"
    tenant_key = "tenant-secret-key"

    # Configure ApiKeyTenantExtension with a different key than the MCP token
    tenant_ext = ApiKeyTenantExtension({"api_key": tenant_key})
    memory._tenant_extension = tenant_ext

    # Patch MCP_AUTH_TOKEN so the MCP middleware uses legacy auth
    with patch("hindsight_api.api.mcp.MCP_AUTH_TOKEN", mcp_token):
        app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

        async with app.router.lifespan_context(app):
            # Pass auth header via the httpx client (streamable_http_client doesn't accept headers)
            async with httpx.AsyncClient(
                transport=ASGITransport(app=app),
                base_url="http://test",
                headers={"Authorization": f"Bearer {mcp_token}"},
            ) as http_client:
                async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
                    read_stream,
                    write_stream,
                    _,
                ):
                    async with ClientSession(read_stream, write_stream) as session:
                        await session.initialize()

                        # list_tools should work
                        tools_result = await session.list_tools()
                        tool_names = {t.name for t in tools_result.tools}
                        assert "get_bank" in tool_names

                        # Tool execution should work (this was failing before the fix)
                        result = await session.call_tool("list_banks", arguments={})
                        assert result is not None
                        assert len(result.content) > 0
                        parsed = json.loads(result.content[0].text)
                        assert "banks" in parsed
                        assert "error" not in parsed, f"Tool call failed with: {parsed.get('error')}"


@pytest.mark.asyncio
async def test_mcp_rejects_wrong_mcp_token_even_if_matches_tenant_key(memory):
    """Test that an invalid MCP token is rejected even if it matches the tenant key.

    When MCP_AUTH_TOKEN is set, the MCP middleware should validate against that token,
    not the tenant API key.
    """
    from httpx import ASGITransport

    from hindsight_api.api import create_app
    from hindsight_api.extensions import ApiKeyTenantExtension

    mcp_token = "mcp-secret-token"
    tenant_key = "tenant-secret-key"

    tenant_ext = ApiKeyTenantExtension({"api_key": tenant_key})
    memory._tenant_extension = tenant_ext

    with patch("hindsight_api.api.mcp.MCP_AUTH_TOKEN", mcp_token):
        app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)

        async with app.router.lifespan_context(app):
            async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
                # Try connecting with the tenant key (wrong for MCP auth)
                response = await http_client.post(
                    "http://test/mcp/",
                    headers={
                        "Authorization": f"Bearer {tenant_key}",
                        "Content-Type": "application/json",
                        "Accept": "application/json, text/event-stream",
                    },
                    json={"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
                )
                assert response.status_code == 401
