//! Base tool definitions for all provider tools.
//!
//! These are the "generic" schemas before enrichment.
//! Provider enrichers modify them based on capabilities and metadata.

use devboy_core::{PropertySchema, ToolCategory, ToolSchema};

/// A tool definition with name, description, category, and input schema.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ToolDefinition {
    pub name: String,
    pub description: String,
    pub category: ToolCategory,
    pub input_schema: ToolSchema,
}

/// Get all base tool definitions (before enrichment).
pub fn base_tool_definitions() -> Vec<ToolDefinition> {
    vec![
        // Issue tools
        ToolDefinition {
            name: "get_issues".into(),
            description: "Get issues from configured provider. Returns a list with filters.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("state", PropertySchema::string_enum(&["open", "closed", "all"], "Filter by issue state (default: open)"));
                s.add_property("search", PropertySchema::string("Search query for title and description"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Filter by label names"));
                s.add_property("assignee", PropertySchema::string("Filter by assignee username"));
                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 20)", Some(1.0), Some(100.0)));
                s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
                s.add_property("sort_by", PropertySchema::string_enum(&["created_at", "updated_at"], "Sort by field (default: updated_at)"));
                s.add_property("sort_order", PropertySchema::string_enum(&["asc", "desc"], "Sort order (default: desc)"));
                s.add_property("projectKey", PropertySchema::string("Project key to filter issues (e.g., \"PROJ\"). Overrides default project. Removed by providers that don't support it."));
                s.add_property("nativeQuery", PropertySchema::string("Native query passed directly to provider (e.g., Jira JQL). Replaces auto-generated filters. If the query omits a project clause, the default project is auto-injected."));
                s
            },
        },
        ToolDefinition {
            name: "get_issue".into(),
            description: "Get a single issue by key with optional comments and relations.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("Issue key (e.g., 'gh#123', 'gitlab#456', 'CU-abc', 'DEV-42', 'jira#PROJ-123')"));
                s.add_property("includeComments", PropertySchema::boolean("Include issue comments (default: true)"));
                s.add_property("includeRelations", PropertySchema::boolean("Include issue relations — parent, subtasks, dependencies (default: true)"));
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "get_issue_comments".into(),
            description: "Get comments for an issue.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("Issue key"));
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "get_issue_relations".into(),
            description: "Get relations for an issue (parent, subtasks, linked issues).".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("Issue key"));
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "create_issue".into(),
            description: "Create a new issue in the configured provider.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("title", PropertySchema::string("Issue title"));
                s.add_property("description", PropertySchema::string("Issue description/body"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to add"));
                s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "Assignee usernames"));
                s.add_property("parentId", PropertySchema::string("Parent issue key to create a subtask (e.g., 'CU-abc123' or 'DEV-42'). Only supported by ClickUp."));
                s.add_property("markdown", PropertySchema::boolean("Whether the description is markdown (default: true). When true, ClickUp renders formatted text."));
                s.add_property("projectId", PropertySchema::string("Jira project key (not numeric ID) for issue creation (e.g., \"PROJ\"). Optional — overrides the default project."));
                s.add_property("issueType", PropertySchema::string("Issue type (e.g., \"Task\", \"Bug\", \"Story\"). Default: \"Task\". Removed by providers that don't support it."));
                // Jira-specific slots (`components`, `fixVersions`,
                // `epicKey`, `sprintId`, `epicName`) are *not* in
                // the base schema — `JiraSchemaEnricher` adds them
                // dynamically so non-Jira providers
                // (GitHub/GitLab/ClickUp) don't see them and can't
                // think they're applicable.
                s.set_required("title", true);
                s
            },
        },
        ToolDefinition {
            name: "update_issue".into(),
            description: "Update an existing issue. Only provided fields will be changed.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("Issue key"));
                s.add_property("title", PropertySchema::string("New title"));
                s.add_property("description", PropertySchema::string("New description"));
                s.add_property("state", PropertySchema::string_enum(&["open", "closed"], "New state (generic open/closed). For ClickUp custom statuses (\"in progress\", \"review\", \"to do\", …) use `status` instead — `get_available_statuses` lists valid names"));
                s.add_property("status", PropertySchema::string("Provider-specific status name. ClickUp: any custom status from `get_available_statuses` (e.g. \"in progress\", \"review\"). Other providers: ignored. Takes precedence over `state` when both are set."));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
                s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "New assignees"));
                s.add_property("parentId", PropertySchema::string("Parent issue key to move task as subtask (e.g., 'CU-abc123' or 'DEV-42'). Only supported by ClickUp."));
                s.add_property("markdown", PropertySchema::boolean("Whether the description is markdown (default: true). When true, ClickUp renders formatted text."));
                // Jira-specific slots are added dynamically by
                // `JiraSchemaEnricher` — see the create_issue
                // schema comment above.
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "add_issue_comment".into(),
            description: "Add a comment to an issue with optional file attachments (ClickUp only).".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("Issue key"));
                s.add_property("body", PropertySchema::string("Comment text"));
                s.add_property("attachments", PropertySchema::array(
                    PropertySchema::string("Attachment object with fileData (base64) and filename"),
                    "File attachments (ClickUp only, max 10MB per file). Each: {fileData: base64, filename: string}",
                ));
                s.set_required("key", true);
                s.set_required("body", true);
                s
            },
        },

        // MR/PR tools
        ToolDefinition {
            name: "get_merge_requests".into(),
            description: "Get merge requests / pull requests from configured provider.".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("state", PropertySchema::string_enum(&["open", "closed", "merged", "all"], "Filter by state (default: open)"));
                s.add_property("author", PropertySchema::string("Filter by author username"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Filter by label names"));
                s.add_property("source_branch", PropertySchema::string("Filter by source branch"));
                s.add_property("target_branch", PropertySchema::string("Filter by target branch"));
                s.add_property("limit", PropertySchema::integer("Maximum results (default: 20)", Some(1.0), Some(100.0)));
                s
            },
        },
        ToolDefinition {
            name: "get_merge_request".into(),
            description: "Get a single merge request by key (e.g., 'pr#123', 'mr#456').".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("MR/PR key"));
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "get_merge_request_discussions".into(),
            description: "Get discussions/review comments for a merge request with code positions.".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("MR/PR key"));
                s.add_property("limit", PropertySchema::integer("Max discussions (default: 20)", Some(1.0), Some(100.0)));
                s.add_property("offset", PropertySchema::integer("Skip N discussions (default: 0)", Some(0.0), None));
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "get_merge_request_diffs".into(),
            description: "Get file diffs for a merge request.".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("MR/PR key"));
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "create_merge_request".into(),
            description: "Create a new merge request (GitLab) or pull request (GitHub).".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("title", PropertySchema::string("MR/PR title"));
                s.add_property("description", PropertySchema::string("MR/PR description"));
                s.add_property("source_branch", PropertySchema::string("Source branch"));
                s.add_property("target_branch", PropertySchema::string("Target branch"));
                s.add_property("draft", PropertySchema::boolean("Create as draft (default: false)"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels"));
                s.add_property("reviewers", PropertySchema::array(PropertySchema::string("reviewer"), "Reviewers"));
                s.set_required("title", true);
                s.set_required("source_branch", true);
                s.set_required("target_branch", true);
                s
            },
        },
        ToolDefinition {
            name: "create_merge_request_comment".into(),
            description: "Add a comment to a merge request. Can be general or inline code review.".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("MR/PR key"));
                s.add_property("body", PropertySchema::string("Comment text"));
                s.add_property("file_path", PropertySchema::string("File path for inline comment"));
                s.add_property("line", PropertySchema::integer("Line number for inline comment", None, None));
                s.add_property("line_type", PropertySchema::string_enum(&["old", "new"], "Line type (default: new)"));
                s.add_property("commit_sha", PropertySchema::string("Commit SHA for inline comment"));
                s.add_property("discussion_id", PropertySchema::string("Reply to existing discussion"));
                s.set_required("key", true);
                s.set_required("body", true);
                s
            },
        },

        // Pipeline tools
        ToolDefinition {
            name: "get_pipeline".into(),
            description: "Get CI/CD pipeline status for branch or MR/PR with job details.".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("branch", PropertySchema::string("Branch name (default: main)"));
                s.add_property("mrKey", PropertySchema::string("MR/PR key (priority over branch)"));
                s.add_property("includeFailedLogs", PropertySchema::boolean("Include error extraction for failed jobs (default: true)"));
                s
            },
        },
        ToolDefinition {
            name: "get_job_logs".into(),
            description: "Get CI/CD job logs. Modes: smart (auto errors), search (pattern), paginated, full.".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("jobId", PropertySchema::string("Job ID from get_pipeline"));
                s.add_property("pattern", PropertySchema::string("Regex/keyword search pattern"));
                s.add_property("context", PropertySchema::integer("Context lines around match (default: 5)", None, None));
                s.add_property("maxMatches", PropertySchema::integer("Max search results (default: 20)", None, None));
                s.add_property("offset", PropertySchema::integer("Start line for paginated mode", None, None));
                s.add_property("limit", PropertySchema::integer("Lines to return (default: 200, max: 1000)", Some(1.0), Some(1000.0)));
                s.add_property("full", PropertySchema::boolean("Return entire log"));
                s.set_required("jobId", true);
                s
            },
        },

        // Status / user / link / epic tools
        ToolDefinition {
            name: "get_available_statuses".into(),
            description: "Get available statuses for the issue tracker.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: ToolSchema::new(),
        },
        ToolDefinition {
            name: "get_users".into(),
            description: "Get users from the issue tracker (Jira). Search by name, project, or ID.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("userId", PropertySchema::string("Get specific user by ID"));
                s.add_property("projectKey", PropertySchema::string("Get assignable users for project"));
                s.add_property("search", PropertySchema::string("Search by name or email"));
                s.add_property("maxResults", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(1000.0)));
                s
            },
        },
        ToolDefinition {
            name: "link_issues".into(),
            description: "Link two issues together (blocks, relates_to, etc.).".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
                s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
                s.add_property("linkType", PropertySchema::string(
                    "Issue link type. Accepts canonical Jira names (`Blocks`, `Relates`, `Causes`, `Implements`, `Created By`, `Duplicate`, `Cloners`) and snake_case aliases (`blocks`, `blocked_by`, `relates_to`, `causes`, `caused_by`, `implements`, `implemented_by`, `created_by`, `creates`, `duplicates`, `duplicated_by`, `clones`, `cloned_by`). The `*_by` variants flip direction. Custom link types configured on the instance also work — pass the exact name. GitHub/GitLab providers ignore this field.",
                ));
                s.set_required("sourceIssueKey", true);
                s.set_required("targetIssueKey", true);
                s.set_required("linkType", true);
                s
            },
        },
        ToolDefinition {
            name: "unlink_issues".into(),
            description: "Remove a link between two issues.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
                s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
                s.add_property("linkType", PropertySchema::string(
                    "Issue link type to remove. Accepts the same canonical names and snake_case aliases as `link_issues` (`Blocks`, `Causes`, `Implements`, `Created By`, `Duplicate`, `Cloners`, plus `*_by` direction flips and `subtask`). Custom link types pass through as-is.",
                ));
                s.set_required("sourceIssueKey", true);
                s.set_required("targetIssueKey", true);
                s.set_required("linkType", true);
                s
            },
        },
        ToolDefinition {
            name: "get_epics".into(),
            description: "Get epics (high-level tasks) from the issue tracker.".into(),
            category: ToolCategory::Epics,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("search", PropertySchema::string("Search in epic title"));
                s.add_property("limit", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(100.0)));
                s.add_property("offset", PropertySchema::integer("Skip N results (default: 0)", Some(0.0), None));
                s
            },
        },
        ToolDefinition {
            name: "create_epic".into(),
            description: "Create a new epic.".into(),
            category: ToolCategory::Epics,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("title", PropertySchema::string("Epic title"));
                s.add_property("description", PropertySchema::string("Epic description"));
                s.set_required("title", true);
                s
            },
        },
        ToolDefinition {
            name: "update_epic".into(),
            description: "Update an existing epic.".into(),
            category: ToolCategory::Epics,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("epicKey", PropertySchema::string("Epic key (e.g., 'CU-abc', 'DEV-123')"));
                s.add_property("title", PropertySchema::string("New title"));
                s.add_property("description", PropertySchema::string("New description"));
                s.add_property("state", PropertySchema::string("New epic state (generic open/closed). For ClickUp custom statuses use `status`."));
                s.add_property("status", PropertySchema::string("Provider-specific status name. Same as `update_issue.status` — for ClickUp, any custom status from `get_available_statuses`. Takes precedence over `state`."));
                s.add_property("goalId", PropertySchema::string("Goal ID (G1-G9) to associate with the epic"));
                s.add_property("priority", PropertySchema::string("New priority (urgent/high/normal/low)"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set"));
                s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "Assignees to set"));
                s.set_required("epicKey", true);
                s
            },
        },
        // Meeting notes tools
        ToolDefinition {
            name: "get_meeting_notes".into(),
            description: "Get meeting notes and transcripts with optional filters (date range, participants, host).".into(),
            category: ToolCategory::MeetingNotes,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601, e.g., '2025-01-01T00:00:00Z')"));
                s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
                s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
                s.add_property("host_email", PropertySchema::string("Filter by host email"));
                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
                s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
                s
            },
        },
        ToolDefinition {
            name: "get_meeting_transcript".into(),
            description: "Get the full transcript for a meeting. Returns speaker-attributed sentences with timestamps.".into(),
            category: ToolCategory::MeetingNotes,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("meeting_id", PropertySchema::string("Meeting ID from get_meeting_notes"));
                s.set_required("meeting_id", true);
                // gh#291: restore pagination + filter + format params dropped
                // during the NestJS→Rust migration. Backend dispatcher
                // (`TranscriptArgs` in consumer monorepo) already parses
                // these — the schema gap left agents unable to paginate
                // through long transcripts (1000+ sentences) or discover
                // grouped output / speaker / text filters.
                s.add_property("offset", PropertySchema::integer("Number of sentences to skip for pagination (default: 0)", Some(0.0), None));
                s.add_property("limit", PropertySchema::integer("Maximum number of sentences (default: 50, max: 500)", Some(1.0), Some(500.0)));
                s.add_property("speaker_filter", PropertySchema::string("Filter sentences by speaker name (case-insensitive substring match)"));
                s.add_property("search_text", PropertySchema::string("Search sentence text (case-insensitive substring match)"));
                s.add_property("format", PropertySchema::string_enum(&["flat", "grouped"], "Output format: 'flat' (default, per-sentence) or 'grouped' (same-speaker runs collapsed)"));
                s
            },
        },
        ToolDefinition {
            name: "search_meeting_notes".into(),
            description: "Search across meetings by keywords, topics, or action items, with optional filters (date range, participants, host).".into(),
            category: ToolCategory::MeetingNotes,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("query", PropertySchema::string("Search query"));
                s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601)"));
                s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
                s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
                s.add_property("host_email", PropertySchema::string("Filter by host email"));
                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
                s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
                s.set_required("query", true);
                s
            },
        },
        // Knowledge base tools
        ToolDefinition {
            name: "get_knowledge_base_spaces".into(),
            description: "List available knowledge base spaces.".into(),
            category: ToolCategory::KnowledgeBase,
            input_schema: ToolSchema::new(),
        },
        ToolDefinition {
            name: "list_knowledge_base_pages".into(),
            description: "List pages in a knowledge base space with pagination.".into(),
            category: ToolCategory::KnowledgeBase,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("spaceKey", PropertySchema::string("Space key to list pages from"));
                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 25)", Some(1.0), Some(100.0)));
                s.add_property("offset", PropertySchema::integer("Number of results to skip when offset pagination is supported", Some(0.0), None));
                s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
                s.add_property("search", PropertySchema::string("Optional free-text title/content filter"));
                s.add_property("parentId", PropertySchema::string("Optional ancestor/parent page ID to scope the listing"));
                s.set_required("spaceKey", true);
                s
            },
        },
        ToolDefinition {
            name: "get_knowledge_base_page".into(),
            description: "Get a knowledge base page with content, labels, and ancestors.".into(),
            category: ToolCategory::KnowledgeBase,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
                s.set_required("pageId", true);
                s
            },
        },
        ToolDefinition {
            name: "create_knowledge_base_page".into(),
            description: "Create a knowledge base page in a space.".into(),
            category: ToolCategory::KnowledgeBase,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("spaceKey", PropertySchema::string("Target space key"));
                s.add_property("title", PropertySchema::string("Page title"));
                s.add_property("content", PropertySchema::string("Page body content"));
                s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
                s.add_property("parentId", PropertySchema::string("Optional parent page ID"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set on the page"));
                s.set_required("spaceKey", true);
                s.set_required("title", true);
                s.set_required("content", true);
                s
            },
        },
        ToolDefinition {
            name: "update_knowledge_base_page".into(),
            description: "Update a knowledge base page title, content, metadata, or labels.".into(),
            category: ToolCategory::KnowledgeBase,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
                s.add_property("title", PropertySchema::string("New page title"));
                s.add_property("content", PropertySchema::string("New page body content"));
                s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
                s.add_property("version", PropertySchema::integer("Expected current version for optimistic locking", Some(1.0), None));
                s.add_property("parentId", PropertySchema::string("Optional new parent page ID"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to replace on the page"));
                s.set_required("pageId", true);
                s
            },
        },
        ToolDefinition {
            name: "search_knowledge_base".into(),
            description: "Search knowledge base pages across spaces using free text or provider-native syntax such as CQL.".into(),
            category: ToolCategory::KnowledgeBase,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("query", PropertySchema::string("Free-text query or provider-native search expression"));
                s.add_property("spaceKey", PropertySchema::string("Restrict search to a specific space key"));
                s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
                s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(100.0)));
                s.add_property("rawQuery", PropertySchema::boolean("Whether `query` should be treated as raw provider-native syntax"));
                s.set_required("query", true);
                s
            },
        },
        ToolDefinition {
            name: "update_merge_request".into(),
            description: "Update a merge request / pull request (title, description, state, labels, draft).".into(),
            category: ToolCategory::GitRepository,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("MR key (e.g. 'mr#1', 'pr#42')"));
                s.add_property("title", PropertySchema::string("New title"));
                s.add_property("description", PropertySchema::string("New description / body (supports markdown)"));
                s.add_property("state", PropertySchema::string_enum(&["close", "reopen"], "Change MR state"));
                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
                s.set_required("key", true);
                s
            },
        },
        // =====================================================================
        // Asset tools
        // =====================================================================
        ToolDefinition {
            name: "get_assets".into(),
            description: "List file attachments for an issue or merge request.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr' (merge request / pull request)"));
                s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123', 'gitlab#42') or MR key (e.g. 'mr#42', 'pr#42')"));
                s.set_required("context_type", true);
                s.set_required("key", true);
                s
            },
        },
        ToolDefinition {
            name: "upload_asset".into(),
            description: "Upload a file attachment to an issue. Returns the download URL.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("context_type", PropertySchema::string_enum(&["issue"], "Context type (currently only 'issue' is supported for uploads)"));
                s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123')"));
                s.add_property("filename", PropertySchema::string("Original filename (e.g. 'screenshot.png')"));
                s.add_property("fileData", PropertySchema::string("Base64-encoded file content"));
                s.set_required("context_type", true);
                s.set_required("key", true);
                s.set_required("filename", true);
                s.set_required("fileData", true);
                s
            },
        },
        ToolDefinition {
            name: "download_asset".into(),
            description: "Download a file attachment to local cache. Returns local file path when cache is available, base64-encoded content as fallback.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr'"));
                s.add_property("key", PropertySchema::string("Issue key or MR key"));
                s.add_property("asset_id", PropertySchema::string("Asset identifier from get_assets response"));
                s.set_required("context_type", true);
                s.set_required("key", true);
                s.set_required("asset_id", true);
                s
            },
        },
        // =====================================================================
        // Messenger tools
        // =====================================================================
        ToolDefinition {
            name: "get_messenger_chats".into(),
            description: "List available messenger chats, channels, groups, or direct messages.".into(),
            category: ToolCategory::Messenger,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("search", PropertySchema::string("Optional chat name search"));
                s.add_property("chat_type", PropertySchema::string_enum(&["direct", "group", "channel"], "Optional chat type filter"));
                s.add_property("limit", PropertySchema::integer("Maximum number of chats to return", Some(1.0), Some(1000.0)));
                s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
                s.add_property("include_inactive", PropertySchema::boolean("Include archived or inactive chats"));
                s
            },
        },
        ToolDefinition {
            name: "get_chat_messages".into(),
            description: "Get message history for a chat or fetch replies for a specific thread.".into(),
            category: ToolCategory::Messenger,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
                s.add_property("limit", PropertySchema::integer("Maximum number of messages to return", Some(1.0), Some(1000.0)));
                s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
                s.add_property("thread_id", PropertySchema::string("Thread identifier to fetch replies for"));
                s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
                s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
                s.set_required("chat_id", true);
                s
            },
        },
        ToolDefinition {
            name: "search_chat_messages".into(),
            description: "Search messages across accessible chats or within a specific chat.".into(),
            category: ToolCategory::Messenger,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("query", PropertySchema::string("Message search query"));
                s.add_property("chat_id", PropertySchema::string("Optional chat ID to scope the search"));
                s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(1000.0)));
                s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
                s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
                s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
                s.set_required("query", true);
                s
            },
        },
        ToolDefinition {
            name: "send_message".into(),
            description: "Send a message to a chat or as a threaded reply.".into(),
            category: ToolCategory::Messenger,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
                s.add_property("text", PropertySchema::string("Message body"));
                s.add_property("thread_id", PropertySchema::string("Thread identifier to post as a threaded reply"));
                s.add_property("reply_to_id", PropertySchema::string("Direct parent message ID when supported"));
                s.set_required("chat_id", true);
                s.set_required("text", true);
                s
            },
        },
        ToolDefinition {
            name: "delete_asset".into(),
            description: "Delete a file attachment from an issue. Not all providers support this — check asset_capabilities first.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("key", PropertySchema::string("Issue key (e.g. 'PROJ-123')"));
                s.add_property("asset_id", PropertySchema::string("Asset identifier to delete"));
                s.set_required("key", true);
                s.set_required("asset_id", true);
                s
            },
        },
        // Jira Structure plugin tools
        ToolDefinition {
            name: "get_structures".into(),
            description: "List all available Jira Structures. Returns structure ID, name, and description. Requires Jira with Structure plugin.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: ToolSchema::new(),
        },
        ToolDefinition {
            name: "get_structure_forest".into(),
            description: "Get the hierarchy tree of a Jira Structure. Returns nested tree with rowId, itemId (Jira issue key), itemType, and children. Supports pagination for large structures.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("structureId", PropertySchema::integer("Structure ID. Use get_structures to find it.", None, None));
                s.add_property("offset", PropertySchema::integer("Offset for pagination (default: 0)", Some(0.0), None));
                s.add_property("limit", PropertySchema::integer("Max rows to return (default: 200)", Some(1.0), Some(10000.0)));
                s.set_required("structureId", true);
                s
            },
        },
        ToolDefinition {
            name: "add_structure_rows".into(),
            description: "Add items (Jira issues or folders) to a Structure. Specify position with under (parent row) and/or after (sibling row). Use forestVersion for optimistic concurrency.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
                s.add_property("items", PropertySchema::array(
                    PropertySchema::string("Item: Jira issue key (e.g. 'PROJ-123') or JSON {\"itemId\":\"PROJ-123\",\"itemType\":\"issue\"}"),
                    "Items to add",
                ));
                s.add_property("under", PropertySchema::integer("Parent row ID — items become children of this row", None, None));
                s.add_property("after", PropertySchema::integer("Sibling row ID — items placed after this row", None, None));
                s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking (from get_structure_forest)", None, None));
                s.set_required("structureId", true);
                s.set_required("items", true);
                s
            },
        },
        ToolDefinition {
            name: "move_structure_rows".into(),
            description: "Move rows within a Jira Structure hierarchy. Specify new position with under (new parent) and/or after (sibling).".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
                s.add_property("rowIds", PropertySchema::array(
                    PropertySchema::integer("Row ID", None, None),
                    "Row IDs to move (from get_structure_forest)",
                ));
                s.add_property("under", PropertySchema::integer("New parent row ID", None, None));
                s.add_property("after", PropertySchema::integer("Sibling row ID to place after", None, None));
                s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking", None, None));
                s.set_required("structureId", true);
                s.set_required("rowIds", true);
                s
            },
        },
        ToolDefinition {
            name: "remove_structure_row".into(),
            description: "Remove a row from a Jira Structure. Only removes from the structure hierarchy — the underlying Jira issue is NOT deleted.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
                s.add_property("rowId", PropertySchema::integer("Row ID to remove (from get_structure_forest)", None, None));
                s.set_required("structureId", true);
                s.set_required("rowId", true);
                s
            },
        },
        ToolDefinition {
            name: "get_structure_values".into(),
            description: "Read column values (including Expr formulas like SUM, PROGRESS, COUNT) for specific rows in a Jira Structure. Values are computed server-side.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
                s.add_property("rows", PropertySchema::array(
                    PropertySchema::integer("Row ID", None, None),
                    "Row IDs to read values for",
                ));
                s.add_property("columns", PropertySchema::array(
                    PropertySchema::string("Column spec: field name (e.g. 'summary'), or JSON {\"field\":\"status\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\"}"),
                    "Columns to read",
                ));
                s.set_required("structureId", true);
                s.set_required("rows", true);
                s.set_required("columns", true);
                s
            },
        },
        ToolDefinition {
            name: "get_structure_views".into(),
            description: "Get views for a Jira Structure. Without viewId: lists all views. With viewId: returns full view configuration (columns, grouping, sorting, filter).".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
                s.add_property("viewId", PropertySchema::integer("View ID for full config (optional — omit to list all views)", None, None));
                s.set_required("structureId", true);
                s
            },
        },
        ToolDefinition {
            name: "save_structure_view".into(),
            description: "Create or update a Jira Structure view. Views define column layout (fields and formulas), grouping, sorting, and filters. Omit id to create new.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("id", PropertySchema::integer("View ID to update (omit to create new)", None, None));
                s.add_property("structureId", PropertySchema::integer("Structure ID this view belongs to", None, None));
                s.add_property("name", PropertySchema::string("View name"));
                s.add_property("columns", PropertySchema::array(
                    PropertySchema::string("Column spec: JSON {\"field\":\"summary\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\",\"width\":100}"),
                    "Column definitions",
                ));
                s.add_property("groupBy", PropertySchema::string("Field name to group by"));
                s.add_property("sortBy", PropertySchema::string("Field name to sort by"));
                s.add_property("filter", PropertySchema::string("JQL filter expression"));
                s.set_required("structureId", true);
                s.set_required("name", true);
                s
            },
        },
        ToolDefinition {
            name: "create_structure".into(),
            description: "Create a new Jira Structure. After creation, use add_structure_rows to populate and save_structure_view to configure columns.".into(),
            category: ToolCategory::JiraStructure,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("name", PropertySchema::string("Structure name"));
                s.add_property("description", PropertySchema::string("Structure description"));
                s.set_required("name", true);
                s
            },
        },

        // Project versions / fixVersion targets (issue #238).
        // Two-tool surface — list returns a rich payload so a per-id GET
        // is unnecessary, upsert is name-keyed so the LLM never deals
        // with numeric ids. See `docs/research/paper-3-context-enrichment.md`.
        ToolDefinition {
            name: "list_project_versions".into(),
            description: "List Jira project versions / fixVersion targets (releases). Returns rich per-version payload (description, dates, released/archived flags, optional issue counts). Default filter hides archived versions and limits to 20 most recent (unreleased first, then released by releaseDate desc). For issue-level details on a release, follow up with `get_issues` and a JQL `nativeQuery` such as `fixVersion = \"<name>\"` — there is no per-id get tool by design.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
                s.add_property("released", PropertySchema::string_enum(&["true", "false", "all"], "Filter by release state: \"true\" → only released, \"false\" → only unreleased, \"all\" → both (default: \"all\")"));
                s.add_property("archived", PropertySchema::string_enum(&["true", "false", "all"], "Filter by archived flag (default: \"false\" — hides archival noise)"));
                s.add_property("limit", PropertySchema::integer("Max versions to return (default: 20). Sorted by releaseDate desc; oldest archival entries trimmed first", Some(1.0), Some(200.0)));
                s.add_property("includeIssueCount", PropertySchema::boolean("Fetch issue counts per version via Cloud `?expand=issuesstatus` (default: false). Adds latency on large projects."));
                s
            },
        },
        ToolDefinition {
            name: "upsert_project_version".into(),
            description: "Create or partially update a Jira project version, keyed by `(project, name)`. If a version with this name exists, fields you supply are updated and unspecified fields are preserved. If not, a new version is created. Useful for writing release notes (`description`) or closing a release (`released: true`, `releaseDate`).".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
                s.add_property("name", PropertySchema::string("Version name — both the lookup key and, on create, the value (e.g., \"3.18.0\")."));
                s.add_property("description", PropertySchema::string("Release notes / version description. Markdown-style text is preserved on Server/DC; Cloud accepts plain text."));
                s.add_property("startDate", PropertySchema::string("Planned start date as ISO 8601 calendar date (`YYYY-MM-DD`)."));
                s.add_property("releaseDate", PropertySchema::string("Planned or actual release date (`YYYY-MM-DD`)."));
                s.add_property("released", PropertySchema::boolean("Mark released (true) / unreleased (false). Pair with `releaseDate` when closing a release."));
                s.add_property("archived", PropertySchema::boolean("Archive (true) / unarchive (false) the version."));
                s.set_required("name", true);
                s
            },
        },
        // Agile / Sprint (issue #198). Pairs with the `sprintId` slot on
        // create_issue / update_issue: `get_board_sprints` is how callers
        // discover available sprint ids on a board.
        ToolDefinition {
            name: "get_board_sprints".into(),
            description: "List sprints visible on a Jira agile board. Use to discover the numeric `sprintId` accepted by `create_issue` / `update_issue` and `assign_to_sprint`. Returns name, state (active/future/closed), planned start/end, and goal — enough for the agent to pick the right sprint without a follow-up call.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "boardId",
                    PropertySchema::integer(
                        "Numeric Jira board id. The Agile / Boards REST endpoint returns sprints scoped to one board — there is no global sprint list",
                        Some(0.0),
                        None,
                    ),
                );
                s.add_property(
                    "state",
                    PropertySchema::string_enum(
                        &["active", "future", "closed", "all"],
                        "Filter by sprint state. Default `all` returns every sprint on the board",
                    ),
                );
                s.set_required("boardId", true);
                s
            },
        },
        ToolDefinition {
            name: "assign_to_sprint".into(),
            description: "Move one or more issues onto a Jira sprint. Pair with `get_board_sprints` to look up the numeric `sprintId`. Issues already on a sprint are silently moved.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "sprintId",
                    PropertySchema::integer(
                        "Numeric sprint id. Use `get_board_sprints` to discover ids on a board",
                        Some(0.0),
                        None,
                    ),
                );
                s.add_property(
                    "issueKeys",
                    PropertySchema::array(
                        PropertySchema::string("issue key (e.g., \"PROJ-1\")"),
                        "Issue keys to move onto the sprint. Must contain at least one key.",
                    ),
                );
                s.set_required("sprintId", true);
                s.set_required("issueKeys", true);
                s
            },
        },
        // Custom-field discovery — pairs with the `epicKey` / `sprintId` /
        // `epicName` slots on create/update_issue and the raw `customFields`
        // escape hatch. Returns a name → id mapping so agents stop guessing
        // `customfield_*` numbers.
        ToolDefinition {
            name: "get_custom_fields".into(),
            description: "List custom fields available on the issue tracker, with their id, name, and field type. Use to discover the `customfield_*` id of a Jira instance — names like `Epic Link`, `Sprint`, `Epic Name` map to different ids on every deployment. Pair with `customFields: { \"<id>\": <value> }` on `create_issue` / `update_issue` for fields not yet exposed as first-class params.".into(),
            category: ToolCategory::IssueTracker,
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "project",
                    PropertySchema::string(
                        "Optional project key. Reserved for providers that scope custom fields per project; ignored on Jira's global `/field` endpoint.",
                    ),
                );
                s.add_property(
                    "issueType",
                    PropertySchema::string(
                        "Optional issue type. Reserved for providers that scope custom fields per create-screen context.",
                    ),
                );
                s.add_property(
                    "search",
                    PropertySchema::string(
                        "Case-insensitive substring filter on the field name (e.g. `\"Epic\"` to find `Epic Link` and `Epic Name`).",
                    ),
                );
                s.add_property(
                    "limit",
                    PropertySchema::integer(
                        "Max fields to return after filtering (default 50). Sorted by name asc",
                        Some(1.0),
                        Some(200.0),
                    ),
                );
                s
            },
        },
    ]
}

/// Always-available MCP tools that don't belong to a provider category.
///
/// These are surfaced by `devboy-mcp` on every `tools/list` regardless of
/// which providers are configured. They live here (not in the MCP crate)
/// so the published reference doc can render them from a single source.
#[derive(Debug, Clone)]
pub struct McpOnlyTool {
    pub name: String,
    pub description: String,
    pub input_schema: ToolSchema,
}

/// MCP-only tools (context management). Always advertised by the server.
pub fn mcp_only_tools() -> Vec<McpOnlyTool> {
    vec![
        McpOnlyTool {
            name: "list_contexts".into(),
            description: "List configured contexts and indicate the active context.".into(),
            input_schema: ToolSchema::new(),
        },
        McpOnlyTool {
            name: "use_context".into(),
            description: "Switch active context at runtime.".into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("name", PropertySchema::string("Context name to activate"));
                s.set_required("name", true);
                s
            },
        },
        McpOnlyTool {
            name: "get_current_context".into(),
            description: "Get current active context name.".into(),
            input_schema: ToolSchema::new(),
        },
        McpOnlyTool {
            name: "secrets_list".into(),
            description: "List secrets the active context's manifest declares. \
                Returns metadata only — values are never included. \
                Optional filter narrows by path substring, scope, status, or \
                whether to include framework-internal paths."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "path_contains",
                    PropertySchema::string("Substring to match against the full ADR-020 path."),
                );
                s.add_property(
                    "scope",
                    PropertySchema::string(
                        "Exact match against the first path segment (e.g. team / personal).",
                    ),
                );
                s.add_property(
                    "status",
                    PropertySchema::string_enum(
                        &["registered", "expiring", "expired"],
                        "Lifecycle status filter computed from expires_at.",
                    ),
                );
                s.add_property(
                    "include_internal",
                    PropertySchema::boolean("Include framework-internal paths (default: false)."),
                );
                s
            },
        },
        McpOnlyTool {
            name: "secrets_describe".into(),
            description: "Describe one secret by ADR-020 path. Returns the same \
                metadata fields as `secrets_list` plus description, retrieval URL, \
                rotation method, last rotated date, rotation cadence, and pattern \
                ID. The value is never returned."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "path",
                    PropertySchema::string("Full ADR-020 path to describe."),
                );
                s.set_required("path", true);
                s
            },
        },
        McpOnlyTool {
            name: "secrets_request_provision".into(),
            description: "Open the provisioning UI dialog for the given ADR-020 \
                path. The dialog hands the user-entered value directly to the \
                local daemon — the agent never sees it. Returns a `request_id` \
                that can be polled with `secrets_poll_status`. Mode defaults to \
                `provision`; pass `rotation` to surface the destructive-confirm \
                checkbox. Pending requests expire 5 minutes after issuance."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "path",
                    PropertySchema::string("Full ADR-020 path to provision."),
                );
                s.add_property(
                    "mode",
                    PropertySchema::string_enum(
                        &["provision", "rotation"],
                        "Dialog mode (default: provision).",
                    ),
                );
                s.set_required("path", true);
                s
            },
        },
        McpOnlyTool {
            name: "secrets_poll_status".into(),
            description: "Poll a provisioning or rotation request issued by \
                `secrets_request_provision` / `secrets_request_rotation`. Returns \
                one of pending / ok / cancelled / expired / failed plus the \
                request's age in seconds and the path it was opened for."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "request_id",
                    PropertySchema::string(
                        "Opaque id returned by request_provision / request_rotation.",
                    ),
                );
                s.set_required("request_id", true);
                s
            },
        },
        McpOnlyTool {
            name: "secrets_request_rotation".into(),
            description: "Open the rotation UI dialog for the given ADR-020 \
                path. Same lifecycle as `secrets_request_provision` but the \
                dialog surfaces the destructive-confirm checkbox so the user \
                explicitly acknowledges that the existing value is being \
                overwritten. Reuses `secrets_poll_status` for status. Pending \
                requests expire 5 minutes after issuance."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "path",
                    PropertySchema::string("Full ADR-020 path to rotate."),
                );
                s.set_required("path", true);
                s
            },
        },
        McpOnlyTool {
            name: "secrets_request_use_approval".into(),
            description: "Open the use-approval dialog for an ADR-020 path \
                whose `approve_on_use` is set to `session` or `per-call`. \
                The agent supplies a short human-facing `reason` that the \
                dialog renders verbatim alongside the path; the user picks \
                `once`, `session`, or `denied`. Returns a `request_id` to \
                poll via `secrets_poll_status`. Pending requests expire 5 \
                minutes after issuance; `ttl_seconds` may shorten the \
                window but never extend it. The agent never sees the \
                secret — only whether the user approved its use."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "path",
                    PropertySchema::string("Full ADR-020 path the agent intends to resolve."),
                );
                s.add_property(
                    "reason",
                    PropertySchema::string(
                        "Short human-facing reason rendered in the dialog \
                         (e.g. 'pushing image to staging registry').",
                    ),
                );
                s.add_property(
                    "ttl_seconds",
                    PropertySchema {
                        schema_type: "integer".into(),
                        description: Some(
                            "Optional lifetime in seconds; capped at the \
                             registry-wide TTL (5 minutes)."
                                .into(),
                        ),
                        ..Default::default()
                    },
                );
                s.set_required("path", true);
                s.set_required("reason", true);
                s
            },
        },
        McpOnlyTool {
            name: "secrets_propose_metadata".into(),
            description: "Suggest metadata edits for an existing ADR-020 \
                path. The dialog renders the manifest's current values as the \
                diff baseline (read straight from the index — agent strings \
                never replace trusted fields, mitigating prompt-injection). \
                The user picks which proposed fields to accept. Reuses \
                `secrets_poll_status` for status. Pending requests expire 5 \
                minutes after issuance."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property("path", PropertySchema::string("Full ADR-020 path to edit."));
                s.add_property(
                    "fields",
                    PropertySchema {
                        schema_type: "object".into(),
                        description: Some(
                            "Proposed field overrides (description, retrieval_url, \
                             rotate_every_days, expires_at, pattern_id). Omitted \
                             fields are not proposed for change."
                                .into(),
                        ),
                        ..Default::default()
                    },
                );
                s.set_required("path", true);
                s.set_required("fields", true);
                s
            },
        },
        McpOnlyTool {
            name: "secrets_propose_new_path".into(),
            description: "Suggest registering a new secret at the given path. \
                The dialog opens with the suggested path editable and the \
                proposed metadata visible in a diff column for review. The \
                user has the final say on the path and the metadata that \
                lands in the manifest. Reuses `secrets_poll_status` for status. \
                Pending requests expire 5 minutes after issuance."
                .into(),
            input_schema: {
                let mut s = ToolSchema::new();
                s.add_property(
                    "suggested_path",
                    PropertySchema::string("Suggested ADR-020 path; user may edit."),
                );
                s.add_property(
                    "metadata",
                    PropertySchema {
                        schema_type: "object".into(),
                        description: Some("Proposed metadata fields for the new entry.".into()),
                        ..Default::default()
                    },
                );
                s.set_required("suggested_path", true);
                s.set_required("metadata", true);
                s
            },
        },
    ]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_base_definitions_count() {
        let tools = base_tool_definitions();
        assert_eq!(tools.len(), 54);
    }

    #[test]
    fn test_all_tools_have_names() {
        for tool in base_tool_definitions() {
            assert!(!tool.name.is_empty());
            assert!(!tool.description.is_empty());
        }
    }

    #[test]
    fn test_tool_categories() {
        let tools = base_tool_definitions();

        let issue_tracker_tools = [
            "get_issues",
            "get_issue",
            "get_issue_comments",
            "get_issue_relations",
            "create_issue",
            "update_issue",
            "add_issue_comment",
            "get_available_statuses",
            "get_users",
            "link_issues",
            "unlink_issues",
            "get_assets",
            "upload_asset",
            "download_asset",
            "delete_asset",
            "list_project_versions",
            "upsert_project_version",
            "get_board_sprints",
            "assign_to_sprint",
            "get_custom_fields",
        ];
        let git_repository_tools = [
            "get_merge_requests",
            "get_merge_request",
            "get_merge_request_discussions",
            "get_merge_request_diffs",
            "create_merge_request",
            "create_merge_request_comment",
            "update_merge_request",
            "get_pipeline",
            "get_job_logs",
        ];
        let epics_tools = ["get_epics", "create_epic", "update_epic"];
        let meeting_notes_tools = [
            "get_meeting_notes",
            "get_meeting_transcript",
            "search_meeting_notes",
        ];
        let knowledge_base_tools = [
            "get_knowledge_base_spaces",
            "list_knowledge_base_pages",
            "get_knowledge_base_page",
            "create_knowledge_base_page",
            "update_knowledge_base_page",
            "search_knowledge_base",
        ];
        let messenger_tools = [
            "get_messenger_chats",
            "get_chat_messages",
            "search_chat_messages",
            "send_message",
        ];
        let jira_structure_tools = [
            "get_structures",
            "get_structure_forest",
            "add_structure_rows",
            "move_structure_rows",
            "remove_structure_row",
            "get_structure_values",
            "get_structure_views",
            "save_structure_view",
            "create_structure",
        ];

        for tool in &tools {
            if issue_tracker_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::IssueTracker,
                    "tool {} should be IssueTracker",
                    tool.name
                );
            } else if git_repository_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::GitRepository,
                    "tool {} should be GitRepository",
                    tool.name
                );
            } else if epics_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::Epics,
                    "tool {} should be Epics",
                    tool.name
                );
            } else if meeting_notes_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::MeetingNotes,
                    "tool {} should be MeetingNotes",
                    tool.name
                );
            } else if knowledge_base_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::KnowledgeBase,
                    "tool {} should be KnowledgeBase",
                    tool.name
                );
            } else if messenger_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::Messenger,
                    "tool {} should be Messenger",
                    tool.name
                );
            } else if jira_structure_tools.contains(&tool.name.as_str()) {
                assert_eq!(
                    tool.category,
                    ToolCategory::JiraStructure,
                    "tool {} should be JiraStructure",
                    tool.name
                );
            } else {
                panic!("tool {} has no expected category mapping", tool.name);
            }
        }
    }

    #[test]
    fn test_required_params() {
        let tools = base_tool_definitions();
        let get_issue = tools.iter().find(|t| t.name == "get_issue").unwrap();
        assert!(get_issue.input_schema.required.contains(&"key".to_string()));

        let create_mr = tools
            .iter()
            .find(|t| t.name == "create_merge_request")
            .unwrap();
        assert!(
            create_mr
                .input_schema
                .required
                .contains(&"title".to_string())
        );
        assert!(
            create_mr
                .input_schema
                .required
                .contains(&"source_branch".to_string())
        );
    }

    // --- ToolDefinition serialization ---

    #[test]
    fn test_tool_definition_serializes_to_json() {
        let tools = base_tool_definitions();
        let tool = &tools[0]; // get_issues
        let json = serde_json::to_string(tool).unwrap();

        assert!(json.contains("\"name\":\"get_issues\""));
        assert!(json.contains("\"description\""));
        assert!(json.contains("\"category\""));
        assert!(json.contains("\"input_schema\""));

        // Should be valid JSON
        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["name"], "get_issues");
    }

    #[test]
    fn test_all_tool_definitions_serialize() {
        let tools = base_tool_definitions();
        for tool in &tools {
            let json = serde_json::to_string(tool);
            assert!(
                json.is_ok(),
                "tool '{}' failed to serialize: {:?}",
                tool.name,
                json.err()
            );
        }
    }

    #[test]
    fn test_tool_definition_json_contains_properties() {
        let tools = base_tool_definitions();
        let get_issues = tools.iter().find(|t| t.name == "get_issues").unwrap();
        let json = serde_json::to_string_pretty(get_issues).unwrap();

        // get_issues should have state, search, labels, assignee, limit, offset, sort_by, sort_order, projectKey, nativeQuery
        assert!(json.contains("state"));
        assert!(json.contains("search"));
        assert!(json.contains("labels"));
        assert!(json.contains("assignee"));
        assert!(json.contains("limit"));
        assert!(json.contains("projectKey"));
        assert!(json.contains("nativeQuery"));
    }

    #[test]
    fn test_tool_definition_required_fields_in_json() {
        let tools = base_tool_definitions();
        let add_comment = tools
            .iter()
            .find(|t| t.name == "add_issue_comment")
            .unwrap();
        let json_val: serde_json::Value = serde_json::to_value(add_comment).unwrap();

        let required = json_val["input_schema"]["required"]
            .as_array()
            .expect("required should be an array");
        let required_strs: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
        assert!(required_strs.contains(&"key"));
        assert!(required_strs.contains(&"body"));
    }

    #[test]
    fn test_tool_names_are_unique() {
        let tools = base_tool_definitions();
        let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
        let original_len = names.len();
        names.sort();
        names.dedup();
        assert_eq!(names.len(), original_len, "tool names should all be unique");
    }

    #[test]
    fn test_link_issues_required_params() {
        let tools = base_tool_definitions();
        let link = tools.iter().find(|t| t.name == "link_issues").unwrap();
        assert!(
            link.input_schema
                .required
                .contains(&"sourceIssueKey".to_string())
        );
        assert!(
            link.input_schema
                .required
                .contains(&"targetIssueKey".to_string())
        );
        assert!(link.input_schema.required.contains(&"linkType".to_string()));
    }

    #[test]
    fn test_get_available_statuses_has_empty_schema() {
        let tools = base_tool_definitions();
        let statuses = tools
            .iter()
            .find(|t| t.name == "get_available_statuses")
            .unwrap();
        assert!(statuses.input_schema.required.is_empty());
        assert!(statuses.input_schema.properties.is_empty());
    }

    /// gh#291: `get_meeting_transcript` schema must advertise pagination
    /// and filter params, otherwise MCP agents cannot paginate through
    /// long transcripts (default limit=50, max=500 in dispatcher) or
    /// discover speaker/text filters or the grouped output format.
    #[test]
    fn test_get_meeting_transcript_exposes_pagination_and_filters() {
        let tools = base_tool_definitions();
        let t = tools
            .iter()
            .find(|t| t.name == "get_meeting_transcript")
            .expect("get_meeting_transcript tool must exist");
        // `meeting_id` is the only required param.
        assert_eq!(t.input_schema.required, vec!["meeting_id".to_string()]);
        // Optional params for pagination, filtering, output format —
        // dropped during NestJS→Rust migration, restored in gh#291.
        for prop in [
            "meeting_id",
            "offset",
            "limit",
            "speaker_filter",
            "search_text",
            "format",
        ] {
            assert!(
                t.input_schema.properties.contains_key(prop),
                "schema must advertise `{prop}` so MCP agents can use it"
            );
        }
    }

    #[test]
    fn test_epic_tools_exist() {
        let tools = base_tool_definitions();
        let epic_names: Vec<&str> = tools
            .iter()
            .filter(|t| t.category == ToolCategory::Epics)
            .map(|t| t.name.as_str())
            .collect();
        assert!(epic_names.contains(&"get_epics"));
        assert!(epic_names.contains(&"create_epic"));
        assert!(epic_names.contains(&"update_epic"));
    }
}
