"""Test suite for the KAPSARC Data Portal MCP Server.

Tests all 10 tools, error paths, and edge cases against the live KAPSARC API.
Uses a single session-scoped MCP server process shared across all tests.
"""

from __future__ import annotations

import json
import sys

import pytest
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# ---------------------------------------------------------------------------
# Import helpers directly for unit testing
# ---------------------------------------------------------------------------
from kapsarc_mcp_server.server import _build_params, _format_dataset_summary

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DATASET = "national-accounts-supply-table-by-divisions"
NONEXISTENT = "this-dataset-does-not-exist-99999"
RESTRICTED_DATASET = "petroleum-wells-in-gcc-countries"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def get_text(result) -> str:
    """Extract text from a CallToolResult."""
    if result.content:
        return result.content[0].text
    # structuredContent path (dict/list tool returns in newer MCP SDKs)
    sc = getattr(result, "structuredContent", None)
    if sc is not None:
        return json.dumps(sc.get("result", sc))
    raise ValueError("No content or structuredContent in result")


def get_json(result) -> dict | list:
    """Extract and parse JSON from a dict/list-returning tool.

    Handles both the content[0].text path (pydantic_core serialised JSON)
    and the structuredContent path used by newer MCP SDK versions.
    """
    sc = getattr(result, "structuredContent", None)
    if sc is not None:
        # structuredContent wraps the value in {"result": ...}
        return sc.get("result", sc)
    return json.loads(result.content[0].text)


# ---------------------------------------------------------------------------
# Session-scoped fixture: one MCP server for all tests
# ---------------------------------------------------------------------------


@pytest.fixture(scope="session")
async def session():
    server_params = StdioServerParameters(
        command=sys.executable,
        args=["-m", "kapsarc_mcp_server"],
    )
    try:
        async with stdio_client(server_params) as (read_stream, write_stream):
            async with ClientSession(read_stream, write_stream) as s:
                await s.initialize()
                yield s
    except RuntimeError as exc:
        # anyio cancel-scope teardown in a different task — harmless with
        # session-scoped pytest-asyncio fixtures
        if "cancel scope" in str(exc):
            pass
        else:
            raise


# ===========================================================================
# 1. Unit tests — pure helpers (7 tests)
# ===========================================================================


class TestBuildParams:
    def test_build_params_drops_none(self):
        assert _build_params(a=1, b=None) == {"a": 1}

    def test_build_params_all_none(self):
        assert _build_params(a=None, b=None) == {}

    def test_build_params_no_args(self):
        assert _build_params() == {}

    def test_build_params_all_present(self):
        result = _build_params(x=1, y="two", z=True)
        assert result == {"x": 1, "y": "two", "z": True}


class TestFormatDatasetSummary:
    def test_format_dataset_summary_full(self):
        ds = {
            "dataset_id": "test-id",
            "metas": {
                "default": {
                    "title": "Test Title",
                    "description": "A" * 300,
                    "theme": ["Energy"],
                    "keyword": ["oil", "gas"],
                    "records_count": 42,
                    "modified": "2024-01-01",
                }
            },
        }
        text = _format_dataset_summary(ds)
        assert "Test Title" in text
        assert "test-id" in text
        assert "42" in text
        assert "Energy" in text
        assert "oil" in text
        assert "\u2026" in text  # truncated description

    def test_format_dataset_summary_nested_wrapper(self):
        ds = {
            "dataset": {
                "dataset_id": "wrapped-id",
                "metas": {
                    "default": {
                        "title": "Wrapped",
                        "records_count": 10,
                    }
                },
            }
        }
        text = _format_dataset_summary(ds)
        assert "wrapped-id" in text
        assert "Wrapped" in text

    def test_format_dataset_summary_minimal(self):
        ds = {"dataset_id": "bare-id", "metas": {}}
        text = _format_dataset_summary(ds)
        assert "bare-id" in text


# ===========================================================================
# 2. Server init + tool registration (4 tests)
# ===========================================================================


class TestServerInit:
    async def test_server_initializes(self, session):
        assert session is not None

    async def test_list_tools_returns_all_thirteen(self, session):
        tools = await session.list_tools()
        names = {t.name for t in tools.tools}
        expected = {
            "search_datasets",
            "get_dataset",
            "get_records",
            "get_record",
            "get_dataset_facets",
            "get_catalog_facets",
            "export_dataset",
            "get_dataset_attachments",
            "export_catalog",
            "list_export_formats",
            "visualize_dataset",
            "visualize_data",
            "visualize_comparison",
        }
        assert names == expected

    async def test_tool_descriptions_nonempty(self, session):
        tools = await session.list_tools()
        for t in tools.tools:
            assert t.description, f"Tool {t.name} has empty description"

    async def test_search_datasets_schema(self, session):
        tools = await session.list_tools()
        search = next(t for t in tools.tools if t.name == "search_datasets")
        props = search.inputSchema.get("properties", {})
        assert "query" in props
        assert "where" in props
        assert "limit" in props
        assert "offset" in props
        required = search.inputSchema.get("required", [])
        assert "query" not in required
        assert "where" not in required


# ===========================================================================
# 3. search_datasets (7 tests)
# ===========================================================================


class TestSearchDatasets:
    async def test_search_no_args(self, session):
        result = await session.call_tool("search_datasets", {})
        text = get_text(result)
        assert "Found" in text
        assert "ID:" in text

    async def test_search_with_query(self, session):
        result = await session.call_tool("search_datasets", {"query": "oil"})
        text = get_text(result)
        assert "Found" in text

    async def test_search_with_limit(self, session):
        result = await session.call_tool("search_datasets", {"limit": 3})
        text = get_text(result)
        assert text.count("ID:") == 3

    async def test_search_pagination(self, session):
        r1 = await session.call_tool("search_datasets", {"limit": 2, "offset": 0})
        r2 = await session.call_tool("search_datasets", {"limit": 2, "offset": 2})
        t1 = get_text(r1)
        t2 = get_text(r2)
        assert "1\u20132" in t1
        assert "3\u20134" in t2
        assert t1 != t2

    async def test_search_with_where(self, session):
        result = await session.call_tool(
            "search_datasets",
            {"where": "default.theme='Economy'"},
        )
        text = get_text(result)
        assert "Found" in text

    async def test_search_no_results(self, session):
        result = await session.call_tool(
            "search_datasets",
            {"query": "xyzzyplughnotarealkeyword9999"},
        )
        text = get_text(result)
        assert "No datasets found" in text

    async def test_search_query_and_where_combined(self, session):
        result = await session.call_tool(
            "search_datasets",
            {"query": "oil", "where": "default.theme='Economy'"},
        )
        text = get_text(result)
        assert "Found" in text or "No datasets found" in text


# ===========================================================================
# 4. get_dataset (2 tests)
# ===========================================================================


class TestGetDataset:
    async def test_get_dataset_known(self, session):
        result = await session.call_tool("get_dataset", {"dataset_id": DATASET})
        text = get_text(result)
        assert DATASET in text
        assert "## Fields" in text
        assert "**" in text

    async def test_get_dataset_nonexistent(self, session):
        result = await session.call_tool("get_dataset", {"dataset_id": NONEXISTENT})
        text = get_text(result)
        assert "404" in text.lower() or "not found" in text.lower()


# ===========================================================================
# 5. get_records (7 tests)
# ===========================================================================


class TestGetRecords:
    async def test_get_records_basic(self, session):
        result = await session.call_tool(
            "get_records", {"dataset_id": DATASET, "limit": 5}
        )
        data = get_json(result)
        assert isinstance(data["total_count"], int)
        assert data["total_count"] > 0
        assert isinstance(data["results"], list)
        assert len(data["results"]) == 5
        assert isinstance(data["results"][0], dict)

    async def test_get_records_with_select(self, session):
        result = await session.call_tool(
            "get_records",
            {"dataset_id": DATASET, "select": "year,value", "limit": 3},
        )
        data = get_json(result)
        for rec in data["results"]:
            assert "year" in rec or "value" in rec

    async def test_get_records_with_where(self, session):
        result = await session.call_tool(
            "get_records",
            {"dataset_id": DATASET, "where": "year>='2020'", "limit": 3},
        )
        data = get_json(result)
        assert isinstance(data["results"], list)

    async def test_get_records_with_order_by(self, session):
        result = await session.call_tool(
            "get_records",
            {"dataset_id": DATASET, "order_by": "year DESC", "limit": 5},
        )
        data = get_json(result)
        years = [r.get("year") for r in data["results"] if r.get("year") is not None]
        if len(years) >= 2:
            assert years == sorted(years, reverse=True)

    async def test_get_records_with_group_by(self, session):
        result = await session.call_tool(
            "get_records",
            {
                "dataset_id": DATASET,
                "select": "year,count(*) as cnt",
                "group_by": "year",
                "limit": 5,
            },
        )
        data = get_json(result)
        for rec in data["results"]:
            assert "cnt" in rec

    async def test_get_records_nonexistent(self, session):
        result = await session.call_tool(
            "get_records", {"dataset_id": NONEXISTENT}
        )
        text = get_text(result)
        assert "404" in text.lower()

    async def test_get_records_bad_odsql(self, session):
        result = await session.call_tool(
            "get_records",
            {"dataset_id": DATASET, "where": "INVALID $$$ ODSQL"},
        )
        text = get_text(result)
        assert "400" in text


# ===========================================================================
# 6. get_record (2 tests)
# ===========================================================================


class TestGetRecord:
    async def test_get_record_valid(self, session):
        result = await session.call_tool(
            "get_records", {"dataset_id": DATASET, "limit": 1}
        )
        data = get_json(result)
        records = data.get("results", [])
        if not records:
            pytest.skip("No records returned")
        rec = records[0]
        record_id = rec.get("record_id") or rec.get("recordid") or rec.get("id")
        if not record_id:
            pytest.skip("No record ID discoverable in flat result")
        result2 = await session.call_tool(
            "get_record", {"dataset_id": DATASET, "record_id": record_id}
        )
        data2 = get_json(result2)
        assert isinstance(data2, dict)

    async def test_get_record_nonexistent(self, session):
        result = await session.call_tool(
            "get_record",
            {"dataset_id": DATASET, "record_id": "not-a-real-record-id"},
        )
        text = get_text(result)
        assert "404" in text.lower() or "not found" in text.lower()


# ===========================================================================
# 7. get_dataset_facets (3 tests)
# ===========================================================================


class TestGetDatasetFacets:
    async def test_facets_all(self, session):
        result = await session.call_tool(
            "get_dataset_facets", {"dataset_id": DATASET}
        )
        text = get_text(result)
        assert "##" in text
        assert " - " in text or "(" in text

    async def test_facets_specific(self, session):
        result = await session.call_tool(
            "get_dataset_facets", {"dataset_id": DATASET, "facet": "year"}
        )
        text = get_text(result)
        assert "year" in text.lower()

    async def test_facets_nonexistent(self, session):
        result = await session.call_tool(
            "get_dataset_facets", {"dataset_id": NONEXISTENT}
        )
        text = get_text(result)
        assert "404" in text.lower()


# ===========================================================================
# 8. get_catalog_facets (2 tests)
# ===========================================================================


class TestGetCatalogFacets:
    async def test_catalog_facets_all(self, session):
        result = await session.call_tool("get_catalog_facets", {})
        text = get_text(result)
        assert "##" in text
        assert "theme" in text.lower() or "keyword" in text.lower()

    async def test_catalog_facets_theme(self, session):
        result = await session.call_tool(
            "get_catalog_facets", {"facet": "theme"}
        )
        text = get_text(result)
        assert "## theme" in text.lower() or "## Theme" in text
        assert "Economy" in text


# ===========================================================================
# 9. export_dataset (6 tests)
# ===========================================================================


class TestExportDataset:
    async def test_export_json(self, session):
        result = await session.call_tool(
            "export_dataset",
            {"dataset_id": DATASET, "format": "json", "limit": 5},
        )
        data = get_json(result)
        # Export endpoint may return a list or a single dict for limit=1
        if isinstance(data, dict):
            data = [data]
        assert isinstance(data, list)
        assert len(data) == 5
        assert isinstance(data[0], dict)

    async def test_export_csv(self, session):
        result = await session.call_tool(
            "export_dataset",
            {"dataset_id": DATASET, "format": "csv", "limit": 5},
        )
        text = get_text(result)
        lines = text.strip().splitlines()
        assert len(lines) >= 2
        assert ";" in lines[0] or "," in lines[0]

    async def test_export_with_where(self, session):
        result = await session.call_tool(
            "export_dataset",
            {
                "dataset_id": DATASET,
                "format": "json",
                "where": "year>='2020'",
                "limit": 3,
            },
        )
        data = get_json(result)
        if isinstance(data, dict):
            data = [data]
        assert isinstance(data, list)

    async def test_export_with_select(self, session):
        result = await session.call_tool(
            "export_dataset",
            {
                "dataset_id": DATASET,
                "format": "json",
                "select": "year,value",
                "limit": 3,
            },
        )
        data = get_json(result)
        if isinstance(data, dict):
            data = [data]
        for rec in data:
            assert "year" in rec or "value" in rec

    async def test_export_unsupported_format(self, session):
        result = await session.call_tool(
            "export_dataset",
            {"dataset_id": DATASET, "format": "xml"},
        )
        text = get_text(result)
        assert "Unsupported format" in text

    async def test_export_nonexistent(self, session):
        result = await session.call_tool(
            "export_dataset",
            {"dataset_id": NONEXISTENT, "format": "json", "limit": 1},
        )
        text = get_text(result)
        assert "404" in text.lower() or "error" in text.lower()


# ===========================================================================
# 10. get_dataset_attachments (2 tests)
# ===========================================================================


class TestGetDatasetAttachments:
    async def test_attachments_known(self, session):
        result = await session.call_tool(
            "get_dataset_attachments", {"dataset_id": DATASET}
        )
        text = get_text(result)
        assert "Attachments for" in text or "No attachments found" in text

    async def test_attachments_nonexistent(self, session):
        result = await session.call_tool(
            "get_dataset_attachments", {"dataset_id": NONEXISTENT}
        )
        text = get_text(result)
        assert "404" in text.lower()


# ===========================================================================
# 11. export_catalog (4 tests)
# ===========================================================================


class TestExportCatalog:
    async def test_catalog_json(self, session):
        result = await session.call_tool(
            "export_catalog", {"format": "json", "limit": 3}
        )
        data = get_json(result)
        if isinstance(data, dict):
            data = [data]
        assert isinstance(data, list)
        assert len(data) == 3
        for ds in data:
            assert "dataset_id" in ds

    async def test_catalog_csv(self, session):
        result = await session.call_tool(
            "export_catalog", {"format": "csv", "limit": 3}
        )
        text = get_text(result)
        lines = text.strip().splitlines()
        assert len(lines) >= 2

    async def test_catalog_with_where(self, session):
        result = await session.call_tool(
            "export_catalog",
            {"format": "json", "where": "default.theme='Economy'", "limit": 2},
        )
        data = get_json(result)
        if isinstance(data, dict):
            data = [data]
        assert isinstance(data, list)

    async def test_catalog_unsupported_format(self, session):
        result = await session.call_tool(
            "export_catalog", {"format": "xml"}
        )
        text = get_text(result)
        assert "Unsupported format" in text


# ===========================================================================
# 12. list_export_formats (2 tests)
# ===========================================================================


class TestListExportFormats:
    async def test_formats_known(self, session):
        result = await session.call_tool(
            "list_export_formats", {"dataset_id": DATASET}
        )
        text = get_text(result)
        assert "Export formats for" in text
        assert "json" in text.lower()
        assert "csv" in text.lower()

    async def test_formats_nonexistent(self, session):
        result = await session.call_tool(
            "list_export_formats", {"dataset_id": NONEXISTENT}
        )
        text = get_text(result)
        assert "404" in text.lower()


# ===========================================================================
# 13. Edge cases (6 tests)
# ===========================================================================


class TestEdgeCases:
    async def test_limit_zero_clamped(self, session):
        result = await session.call_tool(
            "search_datasets", {"limit": 0}
        )
        text = get_text(result)
        assert "ID:" in text
        assert text.count("ID:") >= 1

    async def test_negative_limit_clamped(self, session):
        result = await session.call_tool(
            "search_datasets", {"limit": -5}
        )
        text = get_text(result)
        assert "ID:" in text
        assert text.count("ID:") >= 1

    async def test_limit_over_100_clamped(self, session):
        result = await session.call_tool(
            "get_records", {"dataset_id": DATASET, "limit": 200}
        )
        data = get_json(result)
        assert len(data["results"]) <= 100

    async def test_export_format_case_insensitive(self, session):
        result = await session.call_tool(
            "export_dataset",
            {"dataset_id": DATASET, "format": "JSON", "limit": 2},
        )
        data = get_json(result)
        if isinstance(data, dict):
            data = [data]
        assert isinstance(data, list)
        assert len(data) >= 1

    async def test_catalog_format_case_insensitive(self, session):
        result = await session.call_tool(
            "export_catalog", {"format": "CSV", "limit": 1}
        )
        text = get_text(result)
        assert len(text.strip().splitlines()) >= 1

    async def test_get_records_restricted_dataset(self, session):
        result = await session.call_tool(
            "get_records", {"dataset_id": RESTRICTED_DATASET, "limit": 1}
        )
        text = get_text(result)
        assert "403" in text


# ===========================================================================
# 14. visualize_dataset (4 tests)
# ===========================================================================


class TestVisualizeDataset:
    async def test_basic_chart(self, session):
        result = await session.call_tool(
            "visualize_dataset",
            {"dataset_id": DATASET, "limit": 20},
        )
        # Should return text summary + image
        assert result.content
        assert len(result.content) >= 2
        assert result.content[0].type == "text"
        assert result.content[1].type == "image"

    async def test_explicit_chart_type(self, session):
        result = await session.call_tool(
            "visualize_dataset",
            {"dataset_id": DATASET, "limit": 10, "chart_type": "bar"},
        )
        assert result.content
        assert len(result.content) >= 2
        assert result.content[0].type == "text"
        assert result.content[1].type == "image"

    async def test_invalid_chart_type(self, session):
        result = await session.call_tool(
            "visualize_dataset",
            {"dataset_id": DATASET, "chart_type": "invalid_type"},
        )
        text = get_text(result)
        assert "Invalid chart_type" in text

    async def test_nonexistent_dataset(self, session):
        result = await session.call_tool(
            "visualize_dataset",
            {"dataset_id": NONEXISTENT},
        )
        text = get_text(result)
        assert "404" in text.lower() or "not found" in text.lower()


# ===========================================================================
# 15. visualize_data (2 tests)
# ===========================================================================


class TestVisualizeData:
    async def test_basic(self, session):
        records = [
            {"country": "SA", "production": 10.5},
            {"country": "US", "production": 12.1},
            {"country": "RU", "production": 10.8},
        ]
        result = await session.call_tool(
            "visualize_data",
            {"records": records},
        )
        assert result.content
        assert len(result.content) >= 2
        assert result.content[0].type == "text"
        assert result.content[1].type == "image"

    async def test_empty_records(self, session):
        result = await session.call_tool(
            "visualize_data",
            {"records": []},
        )
        text = get_text(result)
        assert "No records" in text


# ===========================================================================
# 16. visualize_comparison (1 test)
# ===========================================================================


class TestVisualizeComparison:
    async def test_basic_multi_series(self, session):
        result = await session.call_tool(
            "visualize_comparison",
            {
                "dataset_id": DATASET,
                "x_field": "year",
                "y_field": "value",
                "series_field": "division",
                "limit": 50,
            },
        )
        # Should return text summary + image
        assert result.content
        assert len(result.content) >= 2
        assert result.content[0].type == "text"
        assert result.content[1].type == "image"
