"""List, search, and manage test cases and test folders on Maeris."""

from __future__ import annotations

import json
from typing import Any

from mcp.types import TextContent, Tool

from maeris_mcp.auth.token_store import TokenStore
from maeris_mcp.maeris.client import AuthenticationError, MaerisClient


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

def list_test_folders_tool() -> Tool:
    return Tool(
        name="list_test_folders",
        description=(
            "List all test case folders (groups) for the current application. "
            "Use 'name' to filter by name substring. "
            "Use include_id, include_parent_id to add extra fields to the output."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Filter folders by name (case-insensitive substring match).",
                },
                "include_id": {
                    "type": "boolean",
                    "description": "Include folder id in output.",
                },
                "include_parent_id": {
                    "type": "boolean",
                    "description": "Include parent folder id in output.",
                },
            },
        },
    )


def create_test_folder_tool() -> Tool:
    return Tool(
        name="create_test_folder",
        description=(
            "Create a new testcase folder (group) for the current application. "
            "Optionally nest it under an existing parent folder by name. "
            "Use natural language like 'create a folder called Login Flows' or "
            "'create folder Checkout under E2E'."
        ),
        inputSchema={
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Name for the new folder.",
                },
                "parent_name": {
                    "type": "string",
                    "description": "Name of an existing parent folder to nest under (optional).",
                },
            },
        },
    )


def delete_test_folder_tool() -> Tool:
    return Tool(
        name="delete_test_folder",
        description=(
            "Delete one or more testcase folders (groups) by name, name prefix, parent folder, or ID. "
            "Use natural language like 'delete the Login Flows folder', "
            "'delete all folders starting with Smoke', or "
            "'delete all child folders under the Regression parent'."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "names": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Folder names to delete (case-insensitive exact match).",
                },
                "ids": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Folder IDs to delete directly (overrides name lookup).",
                },
                "name": {
                    "type": "string",
                    "description": "Delete all folders whose name contains this substring (case-insensitive).",
                },
                "name_prefix": {
                    "type": "string",
                    "description": "Delete all folders whose name starts with this prefix (case-insensitive).",
                },
                "parent_name": {
                    "type": "string",
                    "description": "Delete all child folders under the parent folder with this exact name.",
                },
                "parent_name_prefix": {
                    "type": "string",
                    "description": "Delete all child folders under any parent folder whose name starts with this prefix.",
                },
                "delete_all": {
                    "type": "boolean",
                    "description": "Set true to delete ALL folders. Use with caution.",
                },
            },
        },
    )


def list_test_cases_tool() -> Tool:
    return Tool(
        name="list_test_cases",
        description=(
            "List test cases for the current application. "
            "Supports filters and include flags similar to 'maeris list tests'."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "include_id": {
                    "type": "boolean",
                    "description": "Include testcase id in the output.",
                },
                "include_name": {
                    "type": "boolean",
                    "description": "Include testcase name in the output.",
                },
                "include_origin": {
                    "type": "boolean",
                    "description": "Include testcase origin in the output.",
                },
                "include_path": {
                    "type": "boolean",
                    "description": "Include testcase path in the output.",
                },
                "folder": {
                    "type": "string",
                    "description": "Filter by folder name or testcase_group_id.",
                },
                "origin": {
                    "type": "string",
                    "description": "Filter by testcase origin (manual, record, default).",
                },
                "name": {
                    "type": "string",
                    "description": "Filter by testcase name substring (case-insensitive).",
                },
                "name_prefix": {
                    "type": "string",
                    "description": "Filter by testcase name prefix (case-insensitive).",
                },
                "name_match": {
                    "type": "string",
                    "description": "Match mode for name filter: contains | starts_with.",
                },
                "search": {
                    "type": "string",
                    "description": "Alias for name filter (case-insensitive substring).",
                },
                "test_type": {
                    "type": "string",
                    "description": "Filter by testcase type (smoke, regression, negative).",
                },
                "type": {
                    "type": "string",
                    "description": "Filter by test type (e.g. smoke, regression, negative).",
                },
                "smoke": {
                    "type": "boolean",
                    "description": "Filter to smoke testcases.",
                },
                "regression": {
                    "type": "boolean",
                    "description": "Filter to regression testcases.",
                },
            },
        },
    )


def search_test_cases_tool() -> Tool:
    return Tool(
        name="search_test_cases",
        description=(
            "Search test cases by name or description (case-insensitive). "
            "Optionally narrow results to a specific folder."
        ),
        inputSchema={
            "type": "object",
            "required": ["query"],
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search string to match against test case name and description.",
                },
                "folder": {
                    "type": "string",
                    "description": "Optional folder name or testcase_group_id to narrow the search.",
                },
            },
        },
    )


def run_test_cases_tool() -> Tool:
    return Tool(
        name="run_test_cases",
        description=(
            "Run testcases for the current application. "
            "Supports filters similar to 'maeris run tests' and optional execution settings."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "ids": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Explicit testcase IDs to run (overrides filters).",
                },
                "folder": {
                    "type": "string",
                    "description": "Filter by folder name or testcase_group_id.",
                },
                "origin": {
                    "type": "string",
                    "description": "Filter by testcase origin (manual, record, default).",
                },
                "name": {
                    "type": "string",
                    "description": "Filter by testcase name substring (case-insensitive).",
                },
                "name_prefix": {
                    "type": "string",
                    "description": "Filter by testcase name prefix (case-insensitive).",
                },
                "name_match": {
                    "type": "string",
                    "description": "Match mode for name filter: contains | starts_with.",
                },
                "search": {
                    "type": "string",
                    "description": "Alias for name filter (case-insensitive substring).",
                },
                "test_type": {
                    "type": "string",
                    "description": "Filter by testcase type (smoke, regression, negative).",
                },
                "type": {
                    "type": "string",
                    "description": "Alias for test_type.",
                },
                "smoke": {
                    "type": "boolean",
                    "description": "Filter to smoke testcases.",
                },
                "regression": {
                    "type": "boolean",
                    "description": "Filter to regression testcases.",
                },
                "all": {
                    "type": "boolean",
                    "description": "Run all testcases (ignores filters).",
                },
                "execution_id": {
                    "type": "string",
                    "description": "Optional test execution ID (uuid).",
                },
                "run_type": {
                    "type": "string",
                    "description": "Type of test run (default: manual).",
                },
            },
        },
    )


def import_test_cases_tool() -> Tool:
    return Tool(
        name="import_test_cases",
        description=(
            "Import Playwright, Cypress, or Selenium test files into Maeris as manual testcases. "
            "Accepts one or more file or directory paths, auto-detects the framework, and pushes "
            "the parsed testcases into a Maeris folder. Falls back to AI extraction when regex "
            "parsing finds nothing. "
            "Use natural language like 'import tests from ./playwright_tests', "
            "'import my cypress specs into the Smoke folder', or "
            "'import tests from tests/ using playwright framework'."
        ),
        inputSchema={
            "type": "object",
            "required": ["paths"],
            "properties": {
                "paths": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "One or more file or directory paths containing test files to import.",
                },
                "framework": {
                    "type": "string",
                    "enum": ["auto", "playwright", "cypress", "selenium"],
                    "description": "Framework to parse. Defaults to 'auto' (tries all).",
                },
                "test_folder": {
                    "type": "string",
                    "description": (
                        "REQUIRED. Target folder name or testcase_group_id in Maeris. "
                        "If the name doesn't match an existing folder, a new one is created. "
                        "If omitted, the tool will return the list of existing folders and ask you to choose."
                    ),
                },
                "base_url": {
                    "type": "string",
                    "description": "Base URL used to expand relative navigation paths in steps.",
                },
                "labels": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Labels to attach to every imported testcase.",
                },
                "page_name": {
                    "type": "string",
                    "description": "Optional page name override applied to all imported steps.",
                },
            },
        },
    )


def delete_test_cases_tool() -> Tool:
    return Tool(
        name="delete_test_cases",
        description=(
            "Permanently delete testcases for the current application. "
            "⛔ NEVER use this to 'update' or 'edit' a test — use edit_test_case or edit_test_step instead. "
            "Deleting and recreating a test destroys its history and creates a duplicate. "
            "Only use this for genuine removal requests. "
            "Supports the same filters as 'maeris delete tests': by type (smoke/regression), "
            "by origin, by name, by folder/group, by explicit IDs, or all at once. "
            "Use natural language like 'delete all smoke tests', 'delete tests named login', "
            "'delete all manual testcases', or 'delete test with id <id>'."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "ids": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Explicit testcase IDs to delete (overrides all other filters).",
                },
                "smoke": {
                    "type": "boolean",
                    "description": "Delete only smoke testcases.",
                },
                "regression": {
                    "type": "boolean",
                    "description": "Delete only regression testcases.",
                },
                "origin": {
                    "type": "string",
                    "description": "Delete testcases with this origin (manual / record / default).",
                },
                "name": {
                    "type": "string",
                    "description": "Delete testcases whose name contains this string (case-insensitive).",
                },
                "name_prefix": {
                    "type": "string",
                    "description": "Delete testcases whose name starts with this string (case-insensitive).",
                },
                "name_match": {
                    "type": "string",
                    "description": "Match mode for name filter: contains | starts_with.",
                },
                "search": {
                    "type": "string",
                    "description": "Alias for name filter (case-insensitive substring).",
                },
                "folder": {
                    "type": "string",
                    "description": "Delete testcases belonging to this folder (name or testcase_group_id).",
                },
                "test_type": {
                    "type": "string",
                    "description": "Delete testcases of this type (smoke, regression, negative).",
                },
                "type": {
                    "type": "string",
                    "description": "Alias for test_type.",
                },
                "all": {
                    "type": "boolean",
                    "description": "Delete every testcase for the current application.",
                },
            },
        },
    )


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

def _auth_guard() -> list[TextContent] | None:
    """Return an error response if the user is not authenticated, else None."""
    ts = TokenStore()
    if not ts.load_token():
        return [TextContent(type="text", text="Error: not authenticated. Run 'maeris login' first.")]
    if ts.is_token_expired():
        return [TextContent(type="text", text="Error: your session has expired. Run 'maeris login' to re-authenticate.")]
    creds = ts.get_token_data() or {}
    if not creds.get("app_id"):
        return [TextContent(
            type="text",
            text="Error: missing application_id. Run 'maeris switch-app' to link an application.",
        )]
    return None


def _format_testcase(tc: dict[str, Any]) -> dict[str, Any]:
    """Normalise a raw testcase dict into a compact summary."""
    return {
        "id": tc.get("testcase_id") or tc.get("id"),
        "name": tc.get("name") or tc.get("testcase_name"),
        "description": tc.get("testcase_description") or tc.get("description") or "",
        "type": tc.get("test_type") or tc.get("testcase_type") or "",
        "folder_id": tc.get("testcase_group_id") or "",
        "status": tc.get("status") or "",
        "created_at": tc.get("created_at") or "",
    }


def _format_folder(group: dict[str, Any]) -> dict[str, Any]:
    """Normalise a raw testcase-group dict into a compact summary."""
    return {
        "id": group.get("testcase_group_id") or group.get("id"),
        "name": group.get("testcase_group_name") or group.get("name") or "",
        "parent_id": group.get("testcase_group_parent_id") or group.get("parent_id") or "",
        "created_at": group.get("created_at") or "",
    }


async def _resolve_folder_id(
    client: MaerisClient,
    folder: str,
) -> str | None:
    """Resolve a folder name or ID to a testcase_group_id.

    If *folder* already looks like an existing group ID it is returned as-is.
    Otherwise the groups are fetched and matched by name (case-insensitive).
    """
    groups = await client.get_testcase_groups()

    # Direct ID match
    for g in groups:
        gid = g.get("testcase_group_id") or g.get("id") or ""
        if gid == folder:
            return gid

    # Name match
    folder_lower = folder.lower()
    for g in groups:
        gname = g.get("testcase_group_name") or g.get("name") or ""
        if gname.lower() == folder_lower:
            return g.get("testcase_group_id") or g.get("id") or ""

    return None


# ---------------------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------------------

async def handle_list_test_folders(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    name_filter = (arguments.get("name") or "").strip()
    include_id = bool(arguments.get("include_id"))
    include_parent_id = bool(arguments.get("include_parent_id"))

    async with MaerisClient() as client:
        try:
            groups = await client.get_testcase_groups()
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: could not fetch folders: {e}")]

    # Filter by name substring if provided
    if name_filter:
        needle = name_filter.lower()
        groups = [
            g for g in groups
            if needle in (g.get("testcase_group_name") or g.get("name") or "").lower()
        ]
        if not groups:
            return [TextContent(type="text", text=f"No folders found matching '{name_filter}'.")]

    # Name is always included; add extra fields if requested
    output: list[dict[str, Any]] = []
    for g in groups:
        item: dict[str, Any] = {
            "name": g.get("testcase_group_name") or g.get("name"),
        }
        if include_id:
            item["id"] = g.get("testcase_group_id") or g.get("id") or g.get("_id")
        if include_parent_id:
            item["parent_id"] = (
                g.get("testcase_group_parent_id") or g.get("parent_id") or g.get("parentId")
            )
        output.append(item)

    return [TextContent(type="text", text=json.dumps(output, indent=2))]


async def handle_create_test_folder(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    name = (arguments.get("name") or "").strip()
    if not name:
        return [TextContent(type="text", text="Error: 'name' is required.")]

    parent_name = (arguments.get("parent_name") or "").strip()

    async with MaerisClient() as client:
        try:
            resolved_parent_id: str | None = None
            if parent_name:
                groups = await client.get_testcase_groups()
                match = next(
                    (g for g in groups
                     if (g.get("testcase_group_name") or g.get("name") or "").lower() == parent_name.lower()),
                    None,
                )
                if not match:
                    return [TextContent(
                        type="text",
                        text=f"Error: parent folder not found: '{parent_name}'",
                    )]
                resolved_parent_id = (
                    match.get("testcase_group_id") or match.get("id") or match.get("_id") or None
                )

            group_id = await client.create_testcase_group(name, parent_id=resolved_parent_id)
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: failed to create folder: {e}")]

    lines = ["✓ Folder created.", f"  Name: {name}", f"  ID:   {group_id}"]
    if resolved_parent_id:
        lines.append(f"  Parent ID: {resolved_parent_id}")
    return [TextContent(type="text", text="\n".join(lines))]


async def handle_delete_test_folder(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    ids: list[str] = [str(x) for x in (arguments.get("ids") or []) if x]
    names: list[str] = [str(x) for x in (arguments.get("names") or []) if x]
    name_filter: str = (arguments.get("name") or "").strip()
    name_prefix: str = (arguments.get("name_prefix") or "").strip()
    parent_name: str = (arguments.get("parent_name") or "").strip()
    parent_name_prefix: str = (arguments.get("parent_name_prefix") or "").strip()
    delete_all: bool = bool(arguments.get("delete_all"))

    has_filter = ids or names or name_filter or name_prefix or parent_name or parent_name_prefix or delete_all
    if not has_filter:
        return [TextContent(
            type="text",
            text=(
                "Error: provide at least one filter to delete folders. "
                "Options: 'name' (substring match), 'names' (list of exact names), 'ids' (list of IDs), "
                "'name_prefix' (name starts with), 'parent_name' (exact parent name), "
                "'parent_name_prefix' (parent name starts with), or 'delete_all' (delete everything)."
            ),
        )]

    def _gname(g: dict) -> str:
        return g.get("testcase_group_name") or g.get("name") or ""

    def _gid(g: dict) -> str:
        return g.get("testcase_group_id") or g.get("id") or g.get("_id") or ""

    def _gparent(g: dict) -> str:
        return g.get("testcase_group_parent_id") or g.get("parent_id") or g.get("parentId") or ""

    async with MaerisClient() as client:
        try:
            groups: list[dict] = []
            # Fetch all groups once when needed for name/prefix/parent resolution
            needs_groups = names or name_filter or name_prefix or parent_name or parent_name_prefix or delete_all
            if needs_groups:
                groups = await client.get_testcase_groups()

            # Resolve explicit names → IDs
            if names:
                for target in names:
                    match = next(
                        (g for g in groups if _gname(g).lower() == target.lower()),
                        None,
                    )
                    if not match:
                        return [TextContent(type="text", text=f"Error: folder not found: '{target}'")]
                    gid = _gid(match)
                    if gid and gid not in ids:
                        ids.append(gid)

            # name substring filter
            if name_filter:
                needle = name_filter.lower()
                matched = [g for g in groups if needle in _gname(g).lower()]
                if not matched:
                    return [TextContent(type="text", text=f"Error: no folders found with name containing '{name_filter}'.")]
                for g in matched:
                    gid = _gid(g)
                    if gid and gid not in ids:
                        ids.append(gid)

            # name_prefix filter
            if name_prefix:
                needle = name_prefix.lower()
                matched = [g for g in groups if _gname(g).lower().startswith(needle)]
                if not matched:
                    return [TextContent(type="text", text=f"Error: no folders found with name starting with '{name_prefix}'.")]
                for g in matched:
                    gid = _gid(g)
                    if gid and gid not in ids:
                        ids.append(gid)

            # parent_name filter — delete children of the named parent
            if parent_name:
                parents = [g for g in groups if _gname(g).lower() == parent_name.lower()]
                if not parents:
                    return [TextContent(type="text", text=f"Error: parent folder not found: '{parent_name}'.")]
                parent_ids = {_gid(p) for p in parents}
                children = [g for g in groups if _gparent(g) in parent_ids]
                if not children:
                    return [TextContent(type="text", text=f"Error: no child folders found under parent '{parent_name}'.")]
                for g in children:
                    gid = _gid(g)
                    if gid and gid not in ids:
                        ids.append(gid)

            # parent_name_prefix filter — delete children of all matching parents
            if parent_name_prefix:
                needle = parent_name_prefix.lower()
                parents = [g for g in groups if _gname(g).lower().startswith(needle)]
                if not parents:
                    return [TextContent(type="text", text=f"Error: no parent folders found with name starting with '{parent_name_prefix}'.")]
                parent_ids = {_gid(p) for p in parents}
                children = [g for g in groups if _gparent(g) in parent_ids]
                if not children:
                    return [TextContent(type="text", text="Error: no child folders found under matching parents.")]
                for g in children:
                    gid = _gid(g)
                    if gid and gid not in ids:
                        ids.append(gid)

            # delete_all — delete every folder
            if delete_all and not ids:
                for g in groups:
                    gid = _gid(g)
                    if gid:
                        ids.append(gid)

            if not ids:
                return [TextContent(type="text", text="No folders matched the given filters.")]

            results = []
            for gid in ids:
                results.append(await client.delete_testcase_group(gid))
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: failed to delete folder(s): {e}")]

    lines = [f"✓ Deleted {len(ids)} folder(s)."]
    for gid in ids:
        lines.append(f"  - {gid}")
    return [TextContent(type="text", text="\n".join(lines))]


async def handle_list_test_cases(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    folder = arguments.get("folder") or ""
    origin_filter = (arguments.get("origin") or "").strip()
    name_filter = (arguments.get("name") or arguments.get("search") or "").strip()
    name_prefix = (arguments.get("name_prefix") or "").strip()
    name_match = (arguments.get("name_match") or "").strip().lower()
    type_filter = (arguments.get("test_type") or arguments.get("type") or "").strip().lower()
    filter_smoke = bool(arguments.get("smoke"))
    filter_regression = bool(arguments.get("regression"))

    # Map shorthand flags to test type filter when no explicit type provided.
    if (filter_smoke or filter_regression) and not type_filter:
        types = []
        if filter_smoke:
            types.append("smoke")
        if filter_regression:
            types.append("regression")
        type_filter = ",".join(types)

    async with MaerisClient() as client:
        try:
            testcases = await client.get_testcases()

            # Resolve folder filter
            folder_id: str | None = None
            if folder:
                folder_id = await _resolve_folder_id(client, folder)
                if folder_id is None:
                    return [TextContent(
                        type="text",
                        text=f"Error: folder not found: {folder}",
                    )]
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]

    # Apply filters
    filtered = testcases
    if folder_id:
        filtered = [
            tc for tc in filtered
            if (tc.get("testcase_group_id") or tc.get("folder_id") or "") == folder_id
        ]
    if origin_filter:
        needle = origin_filter.lower()
        filtered = [
            tc for tc in filtered
            if (tc.get("test_origin") or "").lower() == needle
        ]
    if type_filter:
        chosen = {t.strip().lower() for t in type_filter.split(",") if t.strip()}
        filtered = [
            tc for tc in filtered
            if (tc.get("test_type") or tc.get("testcase_type") or "").lower() in chosen
        ]
    if name_prefix:
        needle = name_prefix.lower()
        filtered = [
            tc for tc in filtered
            if (tc.get("name") or tc.get("testcase_name") or "").lower().startswith(needle)
        ]
    elif name_filter:
        needle = name_filter.lower()
        if name_match == "starts_with":
            filtered = [
                tc for tc in filtered
                if (tc.get("name") or tc.get("testcase_name") or "").lower().startswith(needle)
            ]
        else:
            filtered = [
                tc for tc in filtered
                if needle in (tc.get("name") or tc.get("testcase_name") or "").lower()
            ]

    include_id = bool(arguments.get("include_id"))
    include_name = bool(arguments.get("include_name"))
    include_origin = bool(arguments.get("include_origin"))
    include_path = bool(arguments.get("include_path"))
    has_include = include_id or include_name or include_origin or include_path

    def _row(tc: dict[str, Any]) -> dict[str, Any]:
        return {
            "id":     tc.get("testcase_id") or tc.get("id") or tc.get("_id") or "",
            "name":   tc.get("name") or tc.get("testcase_name") or "",
            "type":   tc.get("test_type") or tc.get("testcase_type") or "",
            "origin": tc.get("test_origin") or "",
            "path":   tc.get("path") or tc.get("testcase_path") or tc.get("file_path") or "",
        }

    rows = [_row(tc) for tc in filtered]

    if not rows:
        return [TextContent(type="text", text="No testcases found.")]

    # Decide which columns to include in the table
    show_id     = include_id or not has_include
    show_name   = include_name or not has_include
    show_type   = not has_include
    show_origin = include_origin or not has_include
    show_path   = include_path

    COL_DEFS = [
        (show_name,   "Name",   "name"),
        (show_type,   "Type",   "type"),
        (show_origin, "Origin", "origin"),
        (show_id,     "ID",     "id"),
        (show_path,   "Path",   "path"),
    ]
    active = [(hdr, key) for show, hdr, key in COL_DEFS if show]

    # Column widths
    widths = {key: max(len(hdr), *(len(r[key]) for r in rows)) for hdr, key in active}

    def _fmt(r: dict[str, Any]) -> str:
        return "  ".join(r[key].ljust(widths[key]) for _, key in active)

    header    = "  ".join(hdr.ljust(widths[key]) for hdr, key in active)
    separator = "  ".join("-" * widths[key] for _, key in active)

    lines = [
        f"{len(rows)} testcase(s) found",
        "",
        header,
        separator,
        *(_fmt(r) for r in rows),
    ]
    return [TextContent(type="text", text="\n".join(lines))]


async def handle_search_test_cases(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    query = (arguments.get("query") or "").lower()
    folder = arguments.get("folder") or ""

    if not query:
        return [TextContent(type="text", text="Error: query is required.")]

    async with MaerisClient() as client:
        try:
            testcases = await client.get_testcases()

            folder_id: str | None = None
            if folder:
                folder_id = await _resolve_folder_id(client, folder)
                if folder_id is None:
                    return [TextContent(
                        type="text",
                        text=f"Error: folder not found: {folder}",
                    )]
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]

    matches = []
    for tc in testcases:
        if folder_id:
            tc_folder = tc.get("testcase_group_id") or tc.get("folder_id") or ""
            if tc_folder != folder_id:
                continue

        name = (tc.get("name") or tc.get("testcase_name") or "").lower()
        desc = (tc.get("testcase_description") or tc.get("description") or "").lower()
        if query in name or query in desc:
            matches.append(tc)

    if not matches:
        return [TextContent(type="text", text=f"No testcases found matching '{query}'.")]

    def _row(tc: dict[str, Any]) -> dict[str, str]:
        return {
            "name":   tc.get("name") or tc.get("testcase_name") or "",
            "type":   tc.get("test_type") or tc.get("testcase_type") or "",
            "origin": tc.get("test_origin") or "",
            "id":     tc.get("testcase_id") or tc.get("id") or tc.get("_id") or "",
        }

    rows = [_row(tc) for tc in matches]
    COL_DEFS = [("Name", "name"), ("Type", "type"), ("Origin", "origin"), ("ID", "id")]
    widths = {key: max(len(hdr), *(len(r[key]) for r in rows)) for hdr, key in COL_DEFS}

    def _fmt(r: dict[str, str]) -> str:
        return "  ".join(r[key].ljust(widths[key]) for _, key in COL_DEFS)

    header    = "  ".join(hdr.ljust(widths[key]) for hdr, key in COL_DEFS)
    separator = "  ".join("-" * widths[key] for _, key in COL_DEFS)
    lines = [f"{len(rows)} testcase(s) found", "", header, separator, *(_fmt(r) for r in rows)]
    return [TextContent(type="text", text="\n".join(lines))]


async def handle_run_test_cases(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    ids: list[str] = [str(x) for x in (arguments.get("ids") or []) if str(x)]
    run_all = bool(arguments.get("all"))
    folder = arguments.get("folder") or ""
    origin_filter = (arguments.get("origin") or "").strip()
    name_filter = (arguments.get("name") or arguments.get("search") or "").strip()
    name_prefix = (arguments.get("name_prefix") or "").strip()
    name_match = (arguments.get("name_match") or "").strip().lower()
    type_filter = (arguments.get("test_type") or arguments.get("type") or "").strip().lower()
    filter_smoke = bool(arguments.get("smoke"))
    filter_regression = bool(arguments.get("regression"))
    execution_id = (arguments.get("execution_id") or "").strip() or None
    run_type = (arguments.get("run_type") or "manual").strip() or "manual"

    # Map shorthand flags to test type filter when no explicit type provided.
    if (filter_smoke or filter_regression) and not type_filter:
        types = []
        if filter_smoke:
            types.append("smoke")
        if filter_regression:
            types.append("regression")
        type_filter = ",".join(types)

    async with MaerisClient() as client:
        if ids:
            ids_to_run = ids
        else:
            try:
                testcases = await client.get_testcases()

                folder_id: str | None = None
                if folder:
                    folder_id = await _resolve_folder_id(client, folder)
                    if folder_id is None:
                        return [TextContent(
                            type="text",
                            text=f"Error: folder not found: {folder}",
                        )]
            except AuthenticationError as e:
                return [TextContent(type="text", text=f"Error: {e}")]
            except Exception as e:
                return [TextContent(type="text", text=f"Error: could not fetch testcases: {e}")]

            if not run_all:
                # Apply filters
                filtered = testcases
                if folder_id:
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("testcase_group_id") or tc.get("folder_id") or "") == folder_id
                    ]
                if origin_filter:
                    needle = origin_filter.lower()
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("test_origin") or "").lower() == needle
                    ]
                if type_filter:
                    chosen = {t.strip().lower() for t in type_filter.split(",") if t.strip()}
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("test_type") or tc.get("testcase_type") or "").lower() in chosen
                    ]
                if name_prefix:
                    needle = name_prefix.lower()
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("name") or tc.get("testcase_name") or "").lower().startswith(needle)
                    ]
                elif name_filter:
                    needle = name_filter.lower()
                    if name_match == "starts_with":
                        filtered = [
                            tc for tc in filtered
                            if (tc.get("name") or tc.get("testcase_name") or "").lower().startswith(needle)
                        ]
                    else:
                        filtered = [
                            tc for tc in filtered
                            if needle in (tc.get("name") or tc.get("testcase_name") or "").lower()
                        ]
            else:
                filtered = testcases

            ids_to_run = [
                str(tc.get("testcase_id") or tc.get("id") or tc.get("_id"))
                for tc in (filtered or [])
                if (tc.get("testcase_id") or tc.get("id") or tc.get("_id"))
            ]

        if not ids_to_run:
            return [TextContent(
                type="text",
                text="No testcases matched the provided filters.",
            )]

        import time
        started = time.perf_counter()
        try:
            result = await client.run_testcases(
                test_ids=ids_to_run,
                test_execution_id=execution_id,
                type_of_test_run=run_type,
            )
        except Exception as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        duration = round(time.perf_counter() - started, 2)

    lines = [
        f"✓ Run started for {len(ids_to_run)} testcase(s).",
        f"  Run type: {run_type}",
        f"  Duration: {duration}s",
    ]
    if isinstance(result, dict):
        for key in ("test_execution_id", "execution_id", "status", "message", "tests_count"):
            val = result.get(key)
            if val is not None:
                lines.append(f"  {key}: {val}")
    return [TextContent(type="text", text="\n".join(lines))]


async def handle_delete_test_cases(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    ids: list[str] = [str(x) for x in (arguments.get("ids") or []) if str(x)]
    delete_all = bool(arguments.get("all"))
    filter_smoke = bool(arguments.get("smoke"))
    filter_regression = bool(arguments.get("regression"))
    filter_origin = (arguments.get("origin") or "").strip().lower()
    name_filter = (arguments.get("name") or arguments.get("search") or "").strip()
    name_prefix = (arguments.get("name_prefix") or "").strip()
    name_match = (arguments.get("name_match") or "").strip().lower()
    type_filter = (arguments.get("test_type") or arguments.get("type") or "").strip().lower()
    folder = arguments.get("folder") or ""

    if (filter_smoke or filter_regression) and not type_filter:
        types = []
        if filter_smoke:
            types.append("smoke")
        if filter_regression:
            types.append("regression")
        type_filter = ",".join(types)

    async with MaerisClient() as client:
        if ids:
            ids_to_delete = ids
        else:
            try:
                testcases = await client.get_testcases()

                folder_id: str | None = None
                if folder:
                    folder_id = await _resolve_folder_id(client, folder)
                    if folder_id is None:
                        return [TextContent(
                            type="text",
                            text=f"Error: folder not found: {folder}",
                        )]
            except AuthenticationError as e:
                return [TextContent(type="text", text=f"Error: {e}")]
            except Exception as e:
                return [TextContent(type="text", text=f"Error: could not fetch testcases: {e}")]

            if delete_all:
                filtered = testcases
            else:
                filtered = testcases
                if folder_id:
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("testcase_group_id") or tc.get("folder_id") or "") == folder_id
                    ]
                if filter_origin:
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("test_origin") or "").lower() == filter_origin
                    ]
                if type_filter:
                    chosen = {t.strip().lower() for t in type_filter.split(",") if t.strip()}
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("test_type") or tc.get("testcase_type") or "").lower() in chosen
                    ]
                if name_prefix:
                    needle = name_prefix.lower()
                    filtered = [
                        tc for tc in filtered
                        if (tc.get("name") or tc.get("testcase_name") or "").lower().startswith(needle)
                    ]
                elif name_filter:
                    needle = name_filter.lower()
                    if name_match == "starts_with":
                        filtered = [
                            tc for tc in filtered
                            if (tc.get("name") or tc.get("testcase_name") or "").lower().startswith(needle)
                        ]
                    else:
                        filtered = [
                            tc for tc in filtered
                            if needle in (tc.get("name") or tc.get("testcase_name") or "").lower()
                        ]

            ids_to_delete = [
                str(tc.get("testcase_id") or tc.get("id") or tc.get("_id"))
                for tc in filtered
                if (tc.get("testcase_id") or tc.get("id") or tc.get("_id"))
            ]

        if not ids_to_delete:
            return [TextContent(
                type="text",
                text="No testcases matched the provided filters.",
            )]

        try:
            result = await client.delete_testcases(ids_to_delete)
        except Exception as e:
            return [TextContent(type="text", text=f"Error: {e}")]

    return [TextContent(type="text", text=f"✓ Deleted {len(ids_to_delete)} testcase(s).")]


async def handle_import_test_cases(arguments: dict) -> list[TextContent]:
    from pathlib import Path as _Path

    from maeris_mcp.importers.ai_importer import extract_with_ai
    from maeris_mcp.importers.test_importer import (
        build_manual_steps_with_verifications,
        diagnose_no_tests,
        discover_test_files,
        parse_test_file,
    )

    err = _auth_guard()
    if err:
        return err

    raw_paths: list[str] = [str(p) for p in (arguments.get("paths") or []) if p]
    if not raw_paths:
        return [TextContent(type="text", text="Error: at least one path is required.")]

    # Validate paths
    invalid = [p for p in raw_paths if not _Path(p).exists()]
    if invalid:
        return [TextContent(
            type="text",
            text=f"Error: path(s) not found: {', '.join(invalid)}",
        )]

    framework: str = (arguments.get("framework") or "auto").strip().lower()
    test_folder: str = (arguments.get("test_folder") or "").strip()
    base_url: str | None = (arguments.get("base_url") or "").strip() or None

    # test_folder is required by the API. If not provided, fetch existing folders
    # and ask the user to choose one (or provide a new name to create).
    if not test_folder:
        async with MaerisClient() as client:
            try:
                groups = await client.get_testcase_groups()
            except Exception as e:
                return [TextContent(type="text", text=f"Error: could not fetch folders: {e}")]
        folder_list = [
            {
                "id": g.get("testcase_group_id") or g.get("id") or "",
                "name": g.get("testcase_group_name") or g.get("name") or "",
            }
            for g in groups
        ]
        msg = (
            "A 'test_folder' is required to import testcases.\n"
            "Please call import_test_cases again with test_folder set to one of the existing "
            "folder names below, or provide a new folder name to create it automatically.\n\n"
            "Existing folders:\n"
            + json.dumps(folder_list, indent=2)
        )
        return [TextContent(type="text", text=msg)]
    labels: list[str] = [str(l) for l in (arguments.get("labels") or []) if l]
    page_name: str | None = (arguments.get("page_name") or "").strip() or None

    # Discover and parse test files
    files = discover_test_files(tuple(raw_paths))
    if not files:
        return [TextContent(type="text", text="Error: no test files found in the provided paths.")]

    imported = []
    ai_fallback_files: list[str] = []
    ai_failed_files: list[str] = []
    for file in files:
        cases = parse_test_file(file, framework=framework)
        if not cases:
            try:
                cases = extract_with_ai(file, base_url=base_url or "")
                if cases:
                    ai_fallback_files.append(file.name)
            except Exception:
                ai_failed_files.append(file.name)
        imported.extend(cases)

    if not imported:
        detail = diagnose_no_tests(files)
        return [TextContent(
            type="text",
            text=f"Error: no testcases could be parsed from the provided files. {detail}",
        )]

    # Build step payloads
    payloads: list[dict] = []
    skipped = 0
    for tc in imported:
        steps = build_manual_steps_with_verifications(tc, base_url, page_name or tc.name)
        if not steps:
            skipped += 1
            continue
        payloads.append({
            "name": tc.name,
            "description": tc.name,
            "testcase_type": "regression",
            "test_folder": test_folder,
            "labels": labels,
            "browser": ["chrome"],
            "mark_as": "pass",
            "steps": steps,
        })

    if not payloads:
        return [TextContent(
            type="text",
            text="Error: no importable steps were found in the parsed testcases.",
        )]

    # Push to Maeris
    async with MaerisClient() as client:
        try:
            # Resolve or create folder
            resolved_folder_id = test_folder
            if test_folder:
                groups = await client.get_testcase_groups()
                matched = next(
                    (g for g in groups if
                     (g.get("testcase_group_id") or g.get("id") or "") == test_folder
                     or (g.get("testcase_group_name") or g.get("name") or "").lower() == test_folder.lower()),
                    None,
                )
                if matched:
                    resolved_folder_id = matched.get("testcase_group_id") or matched.get("id") or test_folder
                else:
                    resolved_folder_id = await client.create_testcase_group(test_folder)

            for p in payloads:
                p["test_folder"] = resolved_folder_id

            result = await client.create_manual_tests(payloads)
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: failed to import testcases: {e}")]

    lines = [
        f"✓ Import complete.",
        f"  Files scanned:      {len(files)}",
        f"  Testcases parsed:   {len(imported)}",
        f"  Testcases imported: {len(payloads)}",
        f"  Skipped:            {skipped}",
        f"  Folder:             {test_folder} ({resolved_folder_id})",
    ]
    if isinstance(result, dict):
        if result.get("message"):
            lines.append(f"  Message:            {result['message']}")
        if result.get("total_count") is not None:
            lines.append(f"  Total count:        {result['total_count']}")
    if ai_fallback_files:
        lines.append(f"  AI-extracted files: {', '.join(ai_fallback_files)}")
    if ai_failed_files:
        lines.append(f"  AI extract failed:  {', '.join(ai_failed_files)}")
    return [TextContent(type="text", text="\n".join(lines))]


# ---------------------------------------------------------------------------
# get_test_case — read a single testcase with its steps
# ---------------------------------------------------------------------------

def get_test_case_tool() -> Tool:
    return Tool(
        name="get_test_case",
        description=(
            "Fetch a single testcase by name and return its metadata and full step list. "
            "Use this before editing a test to see the current step order, URLs, selectors, "
            "and input values. "
            "Example: 'show me the steps in the Login with Credentials test'."
        ),
        inputSchema={
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Testcase name (case-insensitive exact or substring match).",
                },
            },
        },
    )


async def handle_get_test_case(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    name = (arguments.get("name") or "").strip()
    if not name:
        return [TextContent(type="text", text="Error: 'name' is required.")]

    async with MaerisClient() as client:
        try:
            testcases = await client.get_testcases()
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: could not fetch testcases: {e}")]

        # Find by exact name first, then substring
        needle = name.lower()
        tc = next((t for t in testcases if (t.get("testcase_name") or t.get("name") or "").lower() == needle), None)
        if tc is None:
            tc = next((t for t in testcases if needle in (t.get("testcase_name") or t.get("name") or "").lower()), None)
        if tc is None:
            return [TextContent(type="text", text=f"Error: testcase not found: '{name}'")]

        tc_id = tc.get("testcase_id") or tc.get("id") or ""
        tc_name = tc.get("testcase_name") or tc.get("name") or ""

        # Fetch step components via path field
        path = tc.get("path") or tc.get("testcase_path") or ""
        flu_ids = [fid.strip() for fid in path.split("->") if fid.strip()]

        steps: list[dict[str, Any]] = []
        if flu_ids:
            try:
                components = await client.get_components_by_flu_ids(flu_ids)
                flus = await client.get_flus()
            except Exception as e:
                return [TextContent(type="text", text=f"Error: could not fetch step details: {e}")]

            flu_name_map: dict[str, str] = {
                (f.get("flu_id") or f.get("id") or ""): (f.get("flu_name") or f.get("name") or "")
                for f in flus
                if (f.get("flu_id") or f.get("id"))
            }
            comp_map: dict[str, dict] = {
                (c.get("flu_id") or ""): c
                for c in components
                if c.get("flu_id")
            }

            for i, flu_id in enumerate(flu_ids):
                comp = comp_map.get(flu_id, {})
                step: dict[str, Any] = {
                    "step_index": i,
                    "name": flu_name_map.get(flu_id, flu_id),
                    "action": comp.get("action_taken") or "click",
                    "css_selector": comp.get("css_selector") or "",
                    "page_url": comp.get("page_url") or "",
                }
                if comp.get("mock_input"):
                    step["mock_input"] = comp["mock_input"]
                if comp.get("xpath"):
                    step["xpath"] = comp["xpath"]
                steps.append(step)

    out: dict[str, Any] = {
        "id": tc_id,
        "name": tc_name,
        "type": tc.get("test_type") or tc.get("testcase_type") or "",
        "folder_id": tc.get("testcase_group_id") or "",
        "step_count": len(steps),
        "steps": steps,
    }
    return [TextContent(type="text", text=json.dumps(out, indent=2))]


# ---------------------------------------------------------------------------
# edit_test_case — update testcase-level metadata (name, type, description)
# ---------------------------------------------------------------------------

def edit_test_case_tool() -> Tool:
    return Tool(
        name="edit_test_case",
        description=(
            "Update testcase-level metadata: rename a test, change its type "
            "(smoke / regression / negative), or update its description. "
            "⛔ NEVER delete and recreate a test to make changes — always use this tool or edit_test_step. "
            "For editing step fields (URL, selector, input value) use edit_test_step instead. "
            "Example: 'rename Login with Credentials to Login Flow' or "
            "'change the Login test type to smoke'."
        ),
        inputSchema={
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Current testcase name (case-insensitive match).",
                },
                "new_name": {
                    "type": "string",
                    "description": "New name for the testcase.",
                },
                "test_type": {
                    "type": "string",
                    "enum": ["smoke", "regression", "negative"],
                    "description": "New test type.",
                },
                "description": {
                    "type": "string",
                    "description": "New description.",
                },
            },
        },
    )


async def handle_edit_test_case(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    name = (arguments.get("name") or "").strip()
    if not name:
        return [TextContent(type="text", text="Error: 'name' is required.")]

    new_name = (arguments.get("new_name") or "").strip() or None
    test_type = (arguments.get("test_type") or "").strip() or None
    description = (arguments.get("description") or "").strip() or None

    if not any([new_name, test_type, description]):
        return [TextContent(type="text", text="Error: provide at least one of new_name, test_type, or description.")]

    async with MaerisClient() as client:
        try:
            testcases = await client.get_testcases()
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: could not fetch testcases: {e}")]

        needle = name.lower()
        tc = next((t for t in testcases if (t.get("testcase_name") or t.get("name") or "").lower() == needle), None)
        if tc is None:
            tc = next((t for t in testcases if needle in (t.get("testcase_name") or t.get("name") or "").lower()), None)
        if tc is None:
            return [TextContent(type="text", text=f"Error: testcase not found: '{name}'")]

        tc_id = tc.get("testcase_id") or tc.get("id") or ""
        if not tc_id:
            return [TextContent(type="text", text="Error: testcase has no ID — cannot edit.")]

        payload: dict[str, Any] = {}
        if new_name:
            payload["name"] = new_name
        if test_type:
            payload["testcase_type"] = test_type
        if description:
            payload["description"] = description

        try:
            await client.edit_manual_testcase(tc_id, payload)
        except Exception as e:
            return [TextContent(type="text", text=f"Error: could not update testcase: {e}")]

    tc_name = tc.get("testcase_name") or tc.get("name") or name
    lines = [f"✓ Updated testcase '{tc_name}'."]
    if new_name:
        lines.append(f"  name → {new_name}")
    if test_type:
        lines.append(f"  type → {test_type}")
    if description:
        lines.append(f"  description → {description}")
    return [TextContent(type="text", text="\n".join(lines))]


# ---------------------------------------------------------------------------
# edit_test_step — update step-level fields (URL, selector, input, action)
# ---------------------------------------------------------------------------

def edit_test_step_tool() -> Tool:
    return Tool(
        name="edit_test_step",
        description=(
            "Edit one or more steps inside an existing testcase in-place. "
            "⛔ NEVER delete and recreate a test to change step values — always use this tool. "
            "Targets steps by index (0-based) and updates their page_url, css_selector, "
            "mock_input, action, or display name. "
            "Use get_test_case first to see the step list and indices. "
            "Examples: "
            "'change the URL in step 0 of Login with Credentials to https://myapp.com/login', "
            "'update the CSS selector in step 2 of the Checkout test'."
        ),
        inputSchema={
            "type": "object",
            "required": ["name", "steps"],
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Testcase name (case-insensitive match).",
                },
                "steps": {
                    "type": "array",
                    "description": "List of step edits to apply.",
                    "items": {
                        "type": "object",
                        "required": ["step_index"],
                        "properties": {
                            "step_index": {
                                "type": "integer",
                                "description": "0-based index of the step to edit.",
                            },
                            "page_url": {
                                "type": "string",
                                "description": "New URL for this step.",
                            },
                            "css_selector": {
                                "type": "string",
                                "description": "New CSS selector for this step.",
                            },
                            "mock_input": {
                                "type": "string",
                                "description": "New input value for fill/type steps.",
                            },
                            "action": {
                                "type": "string",
                                "description": "New action type (click, fill, navigate, etc.).",
                            },
                            "step_name": {
                                "type": "string",
                                "description": "New display name for this step.",
                            },
                        },
                    },
                },
            },
        },
    )


async def handle_edit_test_step(arguments: dict) -> list[TextContent]:
    err = _auth_guard()
    if err:
        return err

    name = (arguments.get("name") or "").strip()
    step_edits: list[dict] = arguments.get("steps") or []
    if not name:
        return [TextContent(type="text", text="Error: 'name' is required.")]
    if not step_edits:
        return [TextContent(type="text", text="Error: 'steps' must be a non-empty list.")]

    async with MaerisClient() as client:
        try:
            testcases = await client.get_testcases()
        except AuthenticationError as e:
            return [TextContent(type="text", text=f"Error: {e}")]
        except Exception as e:
            return [TextContent(type="text", text=f"Error: could not fetch testcases: {e}")]

        needle = name.lower()
        tc = next((t for t in testcases if (t.get("testcase_name") or t.get("name") or "").lower() == needle), None)
        if tc is None:
            tc = next((t for t in testcases if needle in (t.get("testcase_name") or t.get("name") or "").lower()), None)
        if tc is None:
            return [TextContent(type="text", text=f"Error: testcase not found: '{name}'")]

        tc_id = tc.get("testcase_id") or tc.get("id") or ""
        if not tc_id:
            return [TextContent(type="text", text="Error: testcase has no ID — cannot edit.")]

        path = tc.get("path") or tc.get("testcase_path") or ""
        flu_ids = [fid.strip() for fid in path.split("->") if fid.strip()]
        if not flu_ids:
            return [TextContent(type="text", text="Error: testcase has no steps (empty path).")]

        # Validate all requested indices before fetching components
        for edit in step_edits:
            idx = int(edit.get("step_index", -1))
            if idx < 0 or idx >= len(flu_ids):
                return [TextContent(type="text", text=f"Error: step_index {idx} out of range (test has {len(flu_ids)} steps).")]

        try:
            components = await client.get_components_by_flu_ids(flu_ids)
        except Exception as e:
            return [TextContent(type="text", text=f"Error: could not fetch step components: {e}")]

        comp_map: dict[str, dict] = {
            (c.get("flu_id") or ""): c
            for c in components
            if c.get("flu_id")
        }

        # Apply each step edit via edit_test_step
        applied: list[str] = []
        for edit in step_edits:
            idx = int(edit["step_index"])
            flu_id = flu_ids[idx]
            comp = comp_map.get(flu_id, {})
            component_id = (
                comp.get("component_id") or comp.get("componentId")
                or comp.get("id") or comp.get("_id")
                or comp.get("testcase_component_id") or ""
            )

            payload: dict[str, Any] = {"testcase_id": tc_id, "flu_id": flu_id}
            if component_id:
                payload["component_id"] = component_id
            if "page_url" in edit:
                payload["page_url"] = edit["page_url"]
            if "css_selector" in edit:
                payload["css_selector"] = edit["css_selector"]
            if "mock_input" in edit:
                payload["mock_input"] = edit["mock_input"]
            if "action" in edit:
                payload["action_taken"] = edit["action"]
            if "step_name" in edit:
                payload["name"] = edit["step_name"]

            field_changes = {k: v for k, v in payload.items() if k not in ("testcase_id", "flu_id", "component_id")}
            if not field_changes:
                continue

            try:
                await client.edit_test_step(payload)
                applied.append(f"  Step {idx}: {field_changes}")
            except Exception as e:
                return [TextContent(type="text", text=f"Error on step {idx}: {e}")]

    tc_name = tc.get("testcase_name") or tc.get("name") or name
    if not applied:
        return [TextContent(type="text", text="No changes were applied (no fields provided).")]
    lines = [f"✓ Updated {len(applied)} step(s) in '{tc_name}'."] + applied
    return [TextContent(type="text", text="\n".join(lines))]
