"""
Unit tests for Shaped MCP server tools.

All Shaped API calls are mocked — no network access or API key required.
Runs on every PR via the mcp-unit-tests job in regression-tests.yaml.
"""

import json
from unittest.mock import MagicMock, patch

import pytest
from shaped_mcp.server import (
    _fetch_docs_page,
    account_resource,
    engine_resource,
    shaped_build_query_prompt,
    shaped_create_engine,
    shaped_create_table,
    shaped_create_view,
    shaped_delete_engine,
    shaped_delete_table,
    shaped_delete_view,
    shaped_engine_data_summary_prompt,
    shaped_execute_saved_query,
    shaped_fetch_docs,
    shaped_get_engine,
    shaped_get_saved_query,
    shaped_get_table,
    shaped_get_view,
    shaped_global_query,
    shaped_insert_rows,
    shaped_list_engines,
    shaped_list_saved_queries,
    shaped_list_tables,
    shaped_list_views,
    shaped_query,
    shaped_query_table_prompt,
    shaped_query_view_prompt,
    shaped_update_engine,
    shaped_update_table,
    shaped_update_view,
    shaped_validate_engine_config,
    table_resource,
    view_resource,
)


@pytest.fixture(autouse=True)
def mock_client():
    """Mock Shaped Client and SHAPED_API_KEY for all tests."""
    client = MagicMock()
    with patch.dict("os.environ", {"SHAPED_API_KEY": "test-key-40-chars-long"}):
        with patch("shaped_mcp.server._get_client", return_value=client):
            yield client


@pytest.fixture(autouse=True)
def clear_docs_cache():
    """Clear the docs TTL cache before each test to prevent cross-test pollution."""
    import shaped_mcp.server as _srv

    _srv._docs_cache.clear()
    yield
    _srv._docs_cache.clear()


def test_shaped_list_engines(mock_client):
    """shaped_list_engines returns JSON list of engines."""
    mock_client.list_engines.return_value = {
        "engines": [
            {"engine_name": "test_engine", "status": "ACTIVE"},
        ]
    }
    result = shaped_list_engines()
    data = json.loads(result)
    assert "engines" in data
    assert data["engines"][0]["engine_name"] == "test_engine"
    mock_client.list_engines.assert_called_once()


def test_shaped_list_tables(mock_client):
    """shaped_list_tables returns JSON list of tables."""
    mock_client.list_tables.return_value = {"tables": [{"table_name": "items"}]}
    result = shaped_list_tables()
    data = json.loads(result)
    assert "tables" in data
    assert data["tables"][0]["table_name"] == "items"
    mock_client.list_tables.assert_called_once()


def test_shaped_list_views(mock_client):
    """shaped_list_views returns JSON list of views."""
    mock_client.list_views.return_value = {"views": [{"view_name": "items_view"}]}
    result = shaped_list_views()
    data = json.loads(result)
    assert "views" in data
    assert data["views"][0]["view_name"] == "items_view"
    mock_client.list_views.assert_called_once()


def test_shaped_list_engines_handles_error(mock_client):
    """shaped_list_engines returns error JSON on exception."""
    mock_client.list_engines.side_effect = ValueError("API error")
    result = shaped_list_engines()
    data = json.loads(result)
    assert "error" in data
    assert "API error" in data["error"]


def test_shaped_create_table(mock_client):
    """shaped_create_table parses config and calls create_table."""
    mock_client.create_table.return_value = {"table_name": "items"}
    result = shaped_create_table('{"name": "items", "schema_type": "item"}')
    data = json.loads(result)
    assert data["table_name"] == "items"
    mock_client.create_table.assert_called_once()


def test_shaped_create_table_invalid_json(mock_client):
    """shaped_create_table returns error JSON for invalid input."""
    result = shaped_create_table("not json")
    data = json.loads(result)
    assert "error" in data


def test_shaped_update_table(mock_client):
    """shaped_update_table parses config and calls update_table."""
    mock_client.update_table.return_value = {"table_name": "items"}
    result = shaped_update_table('{"name": "items", "schema_type": "item"}')
    data = json.loads(result)
    assert data["table_name"] == "items"
    mock_client.update_table.assert_called_once()


def test_shaped_update_table_invalid_json(mock_client):
    """shaped_update_table returns error JSON for invalid input."""
    result = shaped_update_table("not json")
    data = json.loads(result)
    assert "error" in data


def test_shaped_create_view(mock_client):
    """shaped_create_view parses config and calls create_view."""
    mock_client.create_view.return_value = {"view_name": "items_view"}
    config = '{"type": "sql_view_config", "name": "items_view"}'
    result = shaped_create_view(config)
    data = json.loads(result)
    assert data["view_name"] == "items_view"
    mock_client.create_view.assert_called_once()


def test_shaped_update_view(mock_client):
    """shaped_update_view parses config and calls update_view."""
    mock_client.update_view.return_value = {"view_name": "items_view"}
    config = '{"type": "sql_view_config", "name": "items_view"}'
    result = shaped_update_view(config)
    data = json.loads(result)
    assert data["view_name"] == "items_view"
    mock_client.update_view.assert_called_once()


def test_shaped_update_view_invalid_json(mock_client):
    """shaped_update_view returns error JSON for invalid input."""
    result = shaped_update_view("not json")
    data = json.loads(result)
    assert "error" in data


def test_shaped_create_engine(mock_client):
    """shaped_create_engine validates config with EngineConfigV2
    then calls create_engine."""
    mock_client.create_engine.return_value = {"engine_name": "rec_engine"}
    config = '{"version": "v2", "name": "rec_engine", "data": {}}'
    result = shaped_create_engine(config)
    data = json.loads(result)
    assert data["engine_name"] == "rec_engine"
    mock_client.create_engine.assert_called_once()


def test_shaped_create_engine_invalid_config(mock_client):
    """shaped_create_engine returns error JSON for invalid config (bad version)."""
    config = '{"version": "v1", "name": "rec_engine"}'
    result = shaped_create_engine(config)
    data = json.loads(result)
    assert "error" in data


def test_shaped_list_saved_queries(mock_client):
    """shaped_list_saved_queries returns JSON list of saved queries."""
    mock_client.list_saved_queries.return_value = {
        "queries": [{"name": "recs_for_user"}]
    }
    result = shaped_list_saved_queries("my_engine")
    data = json.loads(result)
    assert "queries" in data
    assert data["queries"][0]["name"] == "recs_for_user"
    mock_client.list_saved_queries.assert_called_once_with("my_engine")


def test_shaped_list_saved_queries_handles_error(mock_client):
    """shaped_list_saved_queries returns error JSON on exception."""
    mock_client.list_saved_queries.side_effect = ValueError("not found")
    result = shaped_list_saved_queries("bad_engine")
    data = json.loads(result)
    assert "error" in data


def test_shaped_get_saved_query(mock_client):
    """shaped_get_saved_query returns saved query details."""
    mock_client.get_saved_query_info.return_value = {
        "name": "recs_for_user",
        "parameters": ["user_id"],
    }
    result = shaped_get_saved_query("my_engine", "recs_for_user")
    data = json.loads(result)
    assert data["name"] == "recs_for_user"
    assert "user_id" in data["parameters"]
    mock_client.get_saved_query_info.assert_called_once_with(
        "my_engine", "recs_for_user"
    )


def test_shaped_execute_saved_query(mock_client):
    """shaped_execute_saved_query calls execute_saved_query with parsed parameters."""
    mock_client.execute_saved_query.return_value = {"items": [{"id": "i1"}]}
    result = shaped_execute_saved_query(
        "my_engine", "recs_for_user", parameters='{"user_id": "u123"}'
    )
    data = json.loads(result)
    assert "items" in data
    mock_client.execute_saved_query.assert_called_once_with(
        engine_name="my_engine",
        query_name="recs_for_user",
        parameters={"user_id": "u123"},
        return_metadata=False,
        return_explanation=False,
        return_journey_explanations=False,
    )


def test_shaped_execute_saved_query_empty_params(mock_client):
    """shaped_execute_saved_query passes None when parameters is empty."""
    mock_client.execute_saved_query.return_value = {"items": []}
    shaped_execute_saved_query("my_engine", "simple_query", parameters="{}")
    mock_client.execute_saved_query.assert_called_once_with(
        engine_name="my_engine",
        query_name="simple_query",
        parameters=None,
        return_metadata=False,
        return_explanation=False,
        return_journey_explanations=False,
    )


def test_shaped_global_query(mock_client):
    """shaped_global_query calls the global query API endpoint."""
    mock_response = MagicMock()
    mock_response.model_dump.return_value = {"rows": [{"id": "i1"}]}
    mock_client._query_api.post_global_query_query_post.return_value = mock_response
    mock_client._api_key = "test-key"

    with patch(
        "shaped.autogen.models.global_query_request.GlobalQueryRequest",
        return_value=MagicMock(),
    ):
        result = shaped_global_query("SELECT * FROM data.my_engine.items LIMIT 10")

    mock_client._query_api.post_global_query_query_post.assert_called_once()
    data = json.loads(result)
    assert "rows" in data


def test_shaped_global_query_handles_error(mock_client):
    """shaped_global_query returns error JSON on exception."""
    mock_client._query_api.post_global_query_query_post.side_effect = ValueError(
        "bad query"
    )
    result = shaped_global_query("INVALID SQL")
    data = json.loads(result)
    assert "error" in data


# ---------------------------------------------------------------------------
# Docs tool tests
# ---------------------------------------------------------------------------


def test_shaped_fetch_docs_shapedql(mock_client):
    """shaped_fetch_docs fetches the ShapedQL page from docs.shaped.ai."""
    mock_response = MagicMock()
    mock_response.headers = {"content-type": "text/plain"}
    mock_response.text = "ShapedQL reference content"
    mock_response.raise_for_status = MagicMock()

    with patch("shaped_mcp.server.httpx.get", return_value=mock_response) as mock_get:
        result = shaped_fetch_docs("shapedql")

    mock_get.assert_called_once_with(
        "https://docs.shaped.ai/docs/v2/query_reference/shapedql",
        follow_redirects=True,
        timeout=15,
    )
    assert result == "ShapedQL reference content"


def test_shaped_fetch_docs_html_stripped(mock_client):
    """shaped_fetch_docs strips HTML tags when content-type is text/html."""
    mock_response = MagicMock()
    mock_response.headers = {"content-type": "text/html; charset=utf-8"}
    mock_response.text = (
        "<html><body><p>Hello docs</p><script>bad()</script></body></html>"
    )
    mock_response.raise_for_status = MagicMock()

    with patch("shaped_mcp.server.httpx.get", return_value=mock_response):
        result = shaped_fetch_docs("shapedql")

    assert "Hello docs" in result
    assert "<p>" not in result
    assert "bad()" not in result


def test_shaped_fetch_docs_unknown_slug(mock_client):
    """shaped_fetch_docs uses unknown slugs as sub-paths."""
    mock_response = MagicMock()
    mock_response.headers = {"content-type": "text/plain"}
    mock_response.text = "some content"
    mock_response.raise_for_status = MagicMock()

    with patch("shaped_mcp.server.httpx.get", return_value=mock_response) as mock_get:
        shaped_fetch_docs("v2/some/page")

    mock_get.assert_called_once_with(
        "https://docs.shaped.ai/docs/v2/some/page",
        follow_redirects=True,
        timeout=15,
    )


def test_fetch_docs_page_rejects_external_domain():
    """_fetch_docs_page rejects slugs that would escape docs.shaped.ai."""
    _fetch_docs_page("../../etc/passwd")
    # URL will be https://docs.shaped.ai/docs/../../etc/passwd — still
    # starts with docs.shaped.ai, so this passes the whitelist.
    # Test that a truly external path is rejected.
    result2 = _fetch_docs_page("https://evil.com/steal")
    data = json.loads(result2)
    assert "error" in data


def test_shaped_fetch_docs_http_error(mock_client):
    """shaped_fetch_docs returns error JSON on HTTP error status."""
    import httpx as _httpx

    mock_request = MagicMock()
    mock_err_response = MagicMock()
    mock_err_response.status_code = 404
    with patch("shaped_mcp.server.httpx.get") as mock_get:
        mock_get.side_effect = _httpx.HTTPStatusError(
            "Not Found", request=mock_request, response=mock_err_response
        )
        result = shaped_fetch_docs("shapedql")

    data = json.loads(result)
    assert "error" in data
    assert "404" in data["error"]


# ---------------------------------------------------------------------------
# Query / table / view / engine tool tests (previously untested)
# ---------------------------------------------------------------------------


def test_shaped_query(mock_client):
    """shaped_query returns items from execute_query."""
    mock_client.execute_query.return_value = {"items": [{"id": "i1"}]}
    result = shaped_query("my_engine", "SELECT * FROM engine.my_engine.items LIMIT 5")
    data = json.loads(result)
    assert "items" in data
    mock_client.execute_query.assert_called_once()


def test_shaped_query_handles_error(mock_client):
    """shaped_query returns error JSON on exception."""
    mock_client.execute_query.side_effect = ValueError("engine not found")
    result = shaped_query("bad_engine", "SELECT 1")
    data = json.loads(result)
    assert "error" in data
    assert "engine not found" in data["error"]


def test_shaped_insert_rows(mock_client):
    """shaped_insert_rows calls insert_table_rows and returns result."""
    mock_client.insert_table_rows.return_value = {"inserted": 2}
    result = shaped_insert_rows("items", '[{"id": "1"}, {"id": "2"}]')
    data = json.loads(result)
    assert data["inserted"] == 2
    mock_client.insert_table_rows.assert_called_once()


def test_shaped_get_table(mock_client):
    """shaped_get_table returns table details including name."""
    mock_client._api_key = "test-key"
    mock_client._table_api.get_table_details_tables_table_name_get.return_value = {
        "name": "items",
        "status": "ACTIVE",
    }
    result = shaped_get_table("items")
    data = json.loads(result)
    assert data["name"] == "items"
    mock_client._table_api.get_table_details_tables_table_name_get.assert_called_once_with(
        table_name="items",
        x_api_key="test-key",
    )


def test_shaped_delete_table(mock_client):
    """shaped_delete_table calls delete_table once."""
    mock_client.delete_table.return_value = {"deleted": True}
    shaped_delete_table("items")
    mock_client.delete_table.assert_called_once_with("items")


def test_shaped_get_view(mock_client):
    """shaped_get_view returns view details including view_name."""
    mock_client.get_view.return_value = {"view_name": "items_view", "status": "ACTIVE"}
    result = shaped_get_view("items_view")
    data = json.loads(result)
    assert data["view_name"] == "items_view"
    mock_client.get_view.assert_called_once_with("items_view")


def test_shaped_delete_view(mock_client):
    """shaped_delete_view calls delete_view once."""
    mock_client.delete_view.return_value = {"deleted": True}
    shaped_delete_view("items_view")
    mock_client.delete_view.assert_called_once_with("items_view")


def test_shaped_update_engine(mock_client):
    """shaped_update_engine validates config then calls update_engine."""
    mock_client.update_engine.return_value = {"engine_name": "rec_engine"}
    config = '{"version": "v2", "name": "rec_engine", "data": {}}'
    result = shaped_update_engine(config)
    data = json.loads(result)
    assert data["engine_name"] == "rec_engine"
    mock_client.update_engine.assert_called_once()


def test_shaped_delete_engine(mock_client):
    """shaped_delete_engine calls delete_engine once."""
    mock_client.delete_engine.return_value = {"deleted": True}
    shaped_delete_engine("rec_engine")
    mock_client.delete_engine.assert_called_once_with("rec_engine")


def test_shaped_get_engine(mock_client):
    """shaped_get_engine returns engine details including engine_name."""
    mock_client.get_engine.return_value = {
        "engine_name": "rec_engine",
        "status": "ACTIVE",
    }
    result = shaped_get_engine("rec_engine")
    data = json.loads(result)
    assert data["engine_name"] == "rec_engine"
    mock_client.get_engine.assert_called_once_with("rec_engine")


# ---------------------------------------------------------------------------
# Explanation flags and validate tests
# ---------------------------------------------------------------------------


def test_shaped_query_with_explanation(mock_client):
    """shaped_query passes return_explanation and return_journey_explanations
    to execute_query."""
    mock_client.execute_query.return_value = {
        "items": [{"id": "i1"}],
        "explanation": {"total_execution_time_ms": 12},
    }
    result = shaped_query(
        "my_engine",
        "SELECT * FROM engine.my_engine.items LIMIT 5",
        return_explanation=True,
        return_journey_explanations=True,
    )
    data = json.loads(result)
    assert "explanation" in data
    mock_client.execute_query.assert_called_once_with(
        engine_name="my_engine",
        query="SELECT * FROM engine.my_engine.items LIMIT 5",
        parameters=None,
        return_metadata=False,
        return_explanation=True,
        return_journey_explanations=True,
    )


def test_shaped_execute_saved_query_with_explanation(mock_client):
    """shaped_execute_saved_query passes explanation flags to execute_saved_query."""
    mock_client.execute_saved_query.return_value = {
        "items": [{"id": "i1"}],
        "explanation": {"total_execution_time_ms": 8},
    }
    result = shaped_execute_saved_query(
        "my_engine",
        "recs_for_user",
        parameters='{"user_id": "u1"}',
        return_explanation=True,
    )
    data = json.loads(result)
    assert "explanation" in data
    mock_client.execute_saved_query.assert_called_once_with(
        engine_name="my_engine",
        query_name="recs_for_user",
        parameters={"user_id": "u1"},
        return_metadata=False,
        return_explanation=True,
        return_journey_explanations=False,
    )


def test_shaped_validate_engine_config_valid(mock_client):
    """shaped_validate_engine_config returns valid=True and parsed config
    for a valid config."""
    config = '{"version": "v2", "name": "rec_engine", "data": {}}'
    result = shaped_validate_engine_config(config)
    data = json.loads(result)
    assert data["valid"] is True
    assert "parsed" in data


def test_shaped_validate_engine_config_invalid(mock_client):
    """shaped_validate_engine_config returns valid=False with error
    for an invalid config."""
    config = '{"version": "v1", "name": "rec_engine"}'
    result = shaped_validate_engine_config(config)
    data = json.loads(result)
    assert data["valid"] is False
    assert "error" in data


def test_shaped_validate_engine_config_invalid_json(mock_client):
    """shaped_validate_engine_config returns valid=False for malformed JSON."""
    result = shaped_validate_engine_config("not json")
    data = json.loads(result)
    assert data["valid"] is False
    assert "error" in data


# ---------------------------------------------------------------------------
# Resource tests
# ---------------------------------------------------------------------------


def test_account_resource(mock_client):
    """shaped://account returns engines, tables, and views keys."""
    mock_client.list_engines.return_value = {"engines": []}
    mock_client.list_tables.return_value = {"tables": []}
    mock_client.list_views.return_value = {"views": []}
    result = account_resource()
    data = json.loads(result)
    assert "engines" in data
    assert "tables" in data
    assert "views" in data


def test_account_resource_handles_error(mock_client):
    """shaped://account returns error JSON on exception."""
    mock_client.list_engines.side_effect = ValueError("API error")
    result = account_resource()
    data = json.loads(result)
    assert "error" in data


def test_engine_resource(mock_client):
    """shaped://engines/{name} returns engine and sample keys."""
    mock_client.get_engine.return_value = {"engine_name": "rec_engine"}
    mock_client._query_api.post_global_query_query_post.return_value = MagicMock(
        model_dump=lambda **_: {"rows": []}
    )
    mock_client._api_key = "test-key"
    result = engine_resource("rec_engine")
    data = json.loads(result)
    assert "engine" in data
    assert "sample" in data
    assert "items" in data["sample"]
    assert "users" in data["sample"]
    assert "interactions" in data["sample"]


def test_table_resource(mock_client):
    """shaped://tables/{name} returns table and sample keys."""
    mock_client._table_api.get_table_details_tables_table_name_get.return_value = {
        "name": "items",
        "uri": "https://example.com/tables/items",
        "status": "ACTIVE",
    }
    mock_client._query_api.post_global_query_query_post.return_value = MagicMock(
        model_dump=lambda **_: {"rows": []}
    )
    mock_client._api_key = "test-key"
    result = table_resource("items")
    data = json.loads(result)
    assert "table" in data
    assert "sample" in data


def test_view_resource(mock_client):
    """shaped://views/{name} returns view and sample keys."""
    mock_client.get_view.return_value = {"view_name": "items_view"}
    mock_client._query_api.post_global_query_query_post.return_value = MagicMock(
        model_dump=lambda **_: {"rows": []}
    )
    mock_client._api_key = "test-key"
    result = view_resource("items_view")
    data = json.loads(result)
    assert "view" in data
    assert "sample" in data


# ---------------------------------------------------------------------------
# Prompt tests
# ---------------------------------------------------------------------------


def test_shaped_build_query_prompt_contains_engine_and_use_case(mock_client):
    """shaped-build-query prompt includes the engine name and use case."""
    result = shaped_build_query_prompt(
        engine_name="rec_engine", use_case="find similar items"
    )
    assert "rec_engine" in result
    assert "find similar items" in result


def test_shaped_build_query_prompt_with_user_id(mock_client):
    """shaped-build-query prompt includes personalisation text when user_id is given."""
    result = shaped_build_query_prompt(
        engine_name="rec_engine", use_case="recommendations", user_id="u123"
    )
    assert "u123" in result
    assert "personalized" in result.lower()


def test_shaped_build_query_prompt_without_user_id(mock_client):
    """shaped-build-query prompt mentions content-based strategy when no user_id."""
    result = shaped_build_query_prompt(
        engine_name="rec_engine", use_case="recommendations"
    )
    assert "content-based" in result.lower()
    assert "No user_id" in result


def test_shaped_engine_data_summary_prompt(mock_client):
    """
    shaped-engine-data-summary prompt includes the engine name and all three tables.
    """
    result = shaped_engine_data_summary_prompt(engine_name="rec_engine")
    assert "rec_engine" in result
    assert "items" in result
    assert "users" in result
    assert "interactions" in result


def test_shaped_query_table_prompt_with_query(mock_client):
    """shaped-query-table prompt includes the table name and user query."""
    result = shaped_query_table_prompt(table_name="items", query="show me blue items")
    assert "items" in result
    assert "show me blue items" in result


def test_shaped_query_table_prompt_default(mock_client):
    """shaped-query-table prompt provides a default SELECT when no query given."""
    result = shaped_query_table_prompt(table_name="items")
    assert "items" in result
    assert "SELECT *" in result


def test_shaped_query_view_prompt_with_query(mock_client):
    """shaped-query-view prompt includes the view name and user query."""
    result = shaped_query_view_prompt(
        view_name="items_view", query="show me recent items"
    )
    assert "items_view" in result
    assert "show me recent items" in result


def test_shaped_query_view_prompt_default(mock_client):
    """shaped-query-view prompt provides a default SELECT when no query given."""
    result = shaped_query_view_prompt(view_name="items_view")
    assert "items_view" in result
    assert "SELECT *" in result
