"""Dynamic tests that auto-discover all registered MCP tools.

When a new tool is added to any module, these tests cover it automatically:
- Tool is registered and has a schema
- Tool has a description
- Tool is callable with minimal valid arguments
- Tool returns valid JSON
"""

import json

import pytest

from ironcalc_mcp.server import server
from ironcalc_mcp.state import workbook

# ---------------------------------------------------------------------------
# Discovery: collect all registered tools from the FastMCP server
# ---------------------------------------------------------------------------

def _get_all_tools() -> dict:
    """Return {name: tool} for every registered tool."""
    return {name: tool for name, tool in server._tool_manager._tools.items()}


ALL_TOOL_NAMES = list(_get_all_tools().keys())

# Minimal valid arguments per tool. Tools not listed here are called with
# no arguments (relying on defaults) or need a workbook loaded (handled by
# the autouse fixture in conftest).
MINIMAL_ARGS: dict[str, dict] = {
    "create_workbook": {},
    "load_workbook": {"file_path": "__SKIP__"},  # needs a real file
    "save_workbook": {"file_path": "__TMPFILE__"},
    "get_workbook_info": {},
    "get_cell": {"sheet": 0, "row": 1, "column": 1},
    "get_cells": {"sheet": 0, "row_start": 1, "col_start": 1, "row_end": 2, "col_end": 2},
    "set_cell": {"sheet": 0, "row": 1, "column": 1, "value": "test"},
    "set_cells": {"cells": [{"sheet": 0, "row": 1, "column": 1, "value": "batch"}]},
    "add_sheet": {},
    "delete_sheet": {"sheet": 1},  # delete the sheet add_sheet will create
    "rename_sheet": {"sheet": 0, "name": "Renamed"},
    "set_sheet_color": {"sheet": 0, "color": "#FF0000"},
    "insert_rows": {"sheet": 0, "row": 1},
    "delete_rows": {"sheet": 0, "row": 1},
    "insert_columns": {"sheet": 0, "column": 1},
    "delete_columns": {"sheet": 0, "column": 1},
    "set_column_width": {"sheet": 0, "column": 1, "width": 100.0},
    "set_row_height": {"sheet": 0, "row": 1, "height": 30.0},
    "set_frozen_rows": {"sheet": 0, "count": 1},
    "set_frozen_columns": {"sheet": 0, "count": 1},
    "get_cell_style": {"sheet": 0, "row": 1, "column": 1},
}

# Tools that need special setup before calling
NEEDS_EXTRA_SHEET = {"delete_sheet"}
SKIP_CALL = {"load_workbook"}  # requires a real xlsx file on disk


# ---------------------------------------------------------------------------
# Parametrized tests
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("tool_name", ALL_TOOL_NAMES)
def test_tool_is_registered(tool_name):
    """Every tool we expect exists in the server registry."""
    tools = _get_all_tools()
    assert tool_name in tools


@pytest.mark.parametrize("tool_name", ALL_TOOL_NAMES)
def test_tool_has_description(tool_name):
    """Every tool has a non-empty description."""
    tool = _get_all_tools()[tool_name]
    assert tool.description and len(tool.description.strip()) > 0


@pytest.mark.parametrize("tool_name", ALL_TOOL_NAMES)
def test_tool_has_schema(tool_name):
    """Every tool has an input schema with at least a 'type' field."""
    tool = _get_all_tools()[tool_name]
    schema = tool.parameters
    assert "properties" in schema


@pytest.mark.parametrize("tool_name", ALL_TOOL_NAMES)
@pytest.mark.asyncio
async def test_tool_callable(tool_name, tmp_path):
    """Every tool can be called with minimal args and returns valid JSON."""
    if tool_name in SKIP_CALL:
        pytest.skip(f"{tool_name} requires external file")

    # Setup: some tools need an extra sheet
    if tool_name in NEEDS_EXTRA_SHEET:
        workbook.model.add_sheet("ToDelete")
        workbook.model.evaluate()

    args = MINIMAL_ARGS.get(tool_name, {})

    # Replace __TMPFILE__ placeholder with real temp path
    resolved = {}
    for k, v in args.items():
        if v == "__TMPFILE__":
            resolved[k] = str(tmp_path / "test_output.xlsx")
        elif v == "__SKIP__":
            pytest.skip(f"{tool_name} requires special args")
        else:
            resolved[k] = v

    tool = _get_all_tools()[tool_name]
    result = await tool.run(resolved)
    assert isinstance(result, str)
    parsed = json.loads(result)
    assert parsed is not None


# ---------------------------------------------------------------------------
# Coverage guard: fail if a tool exists but has no MINIMAL_ARGS entry
# ---------------------------------------------------------------------------

def test_all_tools_have_minimal_args():
    """Every registered tool must have an entry in MINIMAL_ARGS.

    This test fails when someone adds a new tool but forgets to add
    test arguments — forcing them to keep the test suite in sync.
    """
    tools = _get_all_tools()
    missing = set(tools.keys()) - set(MINIMAL_ARGS.keys())
    assert not missing, f"New tools need MINIMAL_ARGS entries: {missing}"
