//! Tasks extension pack.
//!
//! Tasks graduated from experimental core to an explicit extension in MCP
//! 2026-07-28 (SEP-2133). v1 of this pack ships the negotiation surface
//! and version capture so the OSS engine recognizes a Tasks-extension
//! server end to end. The lifecycle conformance checks (`tools/call`
//! returning a task handle, `tasks/get` / `tasks/update` / `tasks/cancel`
//! polling) defer to v2 when a Tasks-aware target server is reachable.
//!
//! Until v2, `run` is intentionally a no-op that records the advertised
//! version and returns `Ok`. The negotiation pairing is exercised by the
//! framework tests in the parent module.

use super::{ExtensionContext, ExtensionId, ExtensionPack};

/// Canonical wire id for the Tasks extension. Matches the form the
/// 2026-07-28 spec shows in examples; the `KnownExtension::classify`
/// suffix matcher in the parent module already accepts the alternate
/// forms servers may emit.
pub const TASKS_EXTENSION_ID: &str = "io.modelcontextprotocol.tasks";

/// Tasks extension test pack.
///
/// Held as a zero-sized type because the pack version is tied to the
/// mcptest binary version: a single mcptest release ships exactly one
/// Tasks pack version. When a future release ships a v2 with the
/// lifecycle checks, the same type carries the new behavior and the
/// version string surfaced via [`ExtensionPack::version`] bumps too.
#[derive(Debug, Default, Clone, Copy)]
pub struct TasksPack;

impl TasksPack {
    /// Build the pack handle.
    pub fn new() -> Self {
        TasksPack
    }

    /// Borrow the extension id this pack handles. Returned by value so
    /// the global builtin-registry can hand back a static slice.
    fn extension_id() -> ExtensionId {
        ExtensionId::new(TASKS_EXTENSION_ID)
    }
}

impl ExtensionPack for TasksPack {
    fn id(&self) -> &ExtensionId {
        // Tied to a `static` so the borrow lives long enough; the id is
        // a constant, so the `OnceLock` initializes once and stays.
        static ID: std::sync::OnceLock<ExtensionId> = std::sync::OnceLock::new();
        ID.get_or_init(TasksPack::extension_id)
    }

    fn version(&self) -> &str {
        // v1: bound to the mcptest crate version so consumers know which
        // build of the pack ran. v2 will introduce a pack-specific
        // version (and the change will land alongside the lifecycle
        // checks).
        env!("CARGO_PKG_VERSION")
    }

    fn run(&self, _context: &ExtensionContext) -> Result<(), String> {
        // v1: negotiation only. The lifecycle conformance checks land in
        // v2 of this pack once a Tasks-aware target server is reachable;
        // until then the pack records its presence in the report and
        // returns Ok.
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::extensions::{advertised_extensions, KnownExtension};
    use serde_json::json;

    #[test]
    fn tasks_pack_handles_the_canonical_id() {
        let pack = TasksPack::new();
        assert_eq!(pack.id().as_str(), TASKS_EXTENSION_ID);
        assert!(!pack.version().is_empty(), "pack must carry a version");
    }

    #[test]
    fn tasks_pack_negotiates_against_an_advertising_server() {
        // The classification + advertisement walk from the framework, paired
        // with the pack, is what the runner calls negotiate() with.
        let caps = json!({
            "extensions": {
                TASKS_EXTENSION_ID: { "version": "1.0" }
            }
        });
        let advertised = advertised_extensions(&caps);
        let pack = TasksPack::new();
        let matched: Vec<_> = advertised
            .iter()
            .filter(|adv| adv.id == *pack.id())
            .collect();
        assert_eq!(matched.len(), 1);
        assert_eq!(matched[0].kind, KnownExtension::Tasks);
        // v1 run is a no-op success.
        let ctx = ExtensionContext {
            id: matched[0].id.clone(),
            server_advertisement: json!({ "version": "1.0" }),
        };
        assert!(pack.run(&ctx).is_ok());
    }

    #[test]
    fn tasks_pack_skips_cleanly_when_not_advertised() {
        let caps = json!({
            "extensions": {
                "com.example.unrelated": {}
            }
        });
        let advertised = advertised_extensions(&caps);
        let pack = TasksPack::new();
        let matched: Vec<_> = advertised
            .iter()
            .filter(|adv| adv.id == *pack.id())
            .collect();
        assert!(matched.is_empty(), "pack must not run when not advertised");
    }
}
