//! S25 Phase 5e — verify the `#[capability_plugin(...)]` proc-macro
//! emits a working `engine::CapabilityPlugin` impl for any struct that
//! already implements `maat_sdk::Capability`.
//!
//! Tests:
//! - `maat_manifest()` returns a `PluginManifest` with the
//!   expected canonical_id, display_name, kind, license.
//! - `CapabilityPlugin::capability_id()` returns
//!   `Capability::LOCAL_NAME` boxed as `CapabilityId`.
//! - `CapabilityPlugin::handle_capability_frame` correctly extracts a
//!   `Generic { id, bytes }` envelope variant matching this
//!   capability's LOCAL_NAME and delegates to
//!   `Capability::handle_incoming`.
//! - Mismatched ids produce an error rather than silently mutating
//!   state.

use maat_sdk::capability::{
    Capability, CapabilityResult, CapabilityScope, FrameContext,
    WireFrameHash,
};
use maat_sdk::plugin::PluginKind;
use maat_sdk::traits::FeaturedData;
use maat_sdk::wire::CapabilityFrame;
use maat_sdk::CapabilityId;
use maat_sdk_derive::{capability_plugin, EntityType};

// M4 — co-located entity. The capability lives in this crate, so the
// linkme-slice walker emitted by `#[capability_plugin]` should pick
// this entity up and report it in `manifest.declared_types`. Before
// M4 the walker was a model-plugin-only emission and capabilities
// always reported `declared_types: vec![]`.
#[derive(EntityType)]
#[entity(type_id = 9101, display = "Capability Sidekick Token")]
#[allow(dead_code)]
pub struct CapabilitySidekickToken {
    pub featured: FeaturedData,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct DummyFrame {
    pub delta: u32,
}
impl WireFrameHash for DummyFrame {
    const SCHEMA_HASH: u64 = 0xCAFE_F00D_BABE_1234;
}

#[derive(Debug, Default)]
#[capability_plugin(
    canonical_id = "github.com/maat/test-dummy-cap",
    display_name = "Test Dummy Capability",
    authors      = ["Test Author"],
    description  = "Macro smoke-test capability",
    license      = "MIT",
)]
pub struct DummyCapability {
    pub counter: u32,
}

#[async_trait::async_trait]
impl Capability for DummyCapability {
    const LOCAL_NAME: &'static str = "test_dummy";
    type Frame = DummyFrame;

    fn scope(&self) -> CapabilityScope {
        CapabilityScope::Session
    }

    async fn handle_incoming(
        &mut self,
        frame: Self::Frame,
        _ctx: &mut FrameContext,
    ) -> CapabilityResult<()> {
        self.counter += frame.delta;
        Ok(())
    }
}

#[test]
fn maat_manifest_has_expected_shape() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;
    let cap = DummyCapability::default();
    let manifest = EngineCap::manifest(&cap);
    assert_eq!(manifest.canonical_id.as_str(), "github.com/maat/test-dummy-cap");
    assert_eq!(manifest.display_name, "Test Dummy Capability");
    assert_eq!(manifest.kind, PluginKind::Capability);
    assert_eq!(manifest.license.0, "MIT");
    // The dev-build path doesn't run a capability-plugin build.rs (the
    // test crate doesn't need one), so the content_hash is zero. That's
    // expected; production capability crates ship a build.rs that emits
    // METAGAMER_PLUGIN_CONTENT_HASH.
    assert_eq!(manifest.content_hash, [0u8; 32]);
}

#[test]
fn capability_id_returns_local_name() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;
    let cap = DummyCapability::default();
    let id = EngineCap::capability_id(&cap);
    assert_eq!(id, CapabilityId::new("test_dummy"));
}

#[tokio::test]
async fn handle_capability_frame_routes_generic_match() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;

    let mut cap = DummyCapability::default();
    let mut ctx = FrameContext::new();

    // Encode a DummyFrame and wrap it in CapabilityFrame::Generic
    // with our LOCAL_NAME as the id.
    let frame = DummyFrame { delta: 7 };
    let bytes = postcard::to_allocvec(&frame).unwrap();
    let envelope = CapabilityFrame {
        capability_id: CapabilityId::new(<DummyCapability as Capability>::LOCAL_NAME),
        payload: bytes,
    };

    EngineCap::handle_capability_frame(&mut cap, envelope, &mut ctx).await.unwrap();
    assert_eq!(cap.counter, 7);

    // Second frame accumulates.
    let frame2 = DummyFrame { delta: 5 };
    let bytes2 = postcard::to_allocvec(&frame2).unwrap();
    let envelope2 = CapabilityFrame {
        capability_id: CapabilityId::new("test_dummy"),
        payload: bytes2,
    };
    EngineCap::handle_capability_frame(&mut cap, envelope2, &mut ctx).await.unwrap();
    assert_eq!(cap.counter, 12);
}

#[tokio::test]
async fn handle_capability_frame_rejects_mismatched_id() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;

    let mut cap = DummyCapability::default();
    let mut ctx = FrameContext::new();

    let frame = DummyFrame { delta: 99 };
    let bytes = postcard::to_allocvec(&frame).unwrap();
    let envelope = CapabilityFrame {
        capability_id: CapabilityId::new("some_other_cap"),
        payload: bytes,
    };

    let result = EngineCap::handle_capability_frame(&mut cap, envelope, &mut ctx).await;
    assert!(result.is_err(), "mismatched id should error");
    // Counter must not have mutated.
    assert_eq!(cap.counter, 0);
}

#[tokio::test]
async fn handle_capability_frame_accepts_namespaced_id() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;

    let mut cap = DummyCapability::default();
    let mut ctx = FrameContext::new();

    let frame = DummyFrame { delta: 3 };
    let bytes = postcard::to_allocvec(&frame).unwrap();
    // Engine prepends the plugin namespace at registration time, so a
    // Generic envelope may carry "core:test_dummy" rather than the bare
    // LOCAL_NAME. The macro accepts either.
    let envelope = CapabilityFrame {
        capability_id: CapabilityId::new("core:test_dummy"),
        payload: bytes,
    };

    EngineCap::handle_capability_frame(&mut cap, envelope, &mut ctx).await.unwrap();
    assert_eq!(cap.counter, 3);
}

// M4 — `#[capability_plugin]` shares the linkme-slice walker with
// `#[plugin]`, so a capability whose crate contributes an entity
// (or handler / phase / wire-frame) registration should see it
// reflected in the manifest's `declared_*` fields. Pre-M4 these were
// hardcoded to `Vec::new()`.
#[test]
fn capability_manifest_walks_declared_types() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;
    let cap = DummyCapability::default();
    let manifest = EngineCap::manifest(&cap);
    let names: Vec<&str> = manifest
        .declared_types
        .iter()
        .map(|t| t.type_name.as_str())
        .collect();
    assert!(
        names.contains(&"CapabilitySidekickToken"),
        "capability manifest should report sibling entity in declared_types; got {names:?}"
    );
}

#[test]
fn capability_manifest_decls_have_correct_shape() {
    // Even when no producer-side registration exists for a given slice,
    // the walker still emits an empty vec — proving the field is alive
    // (not hardcoded) and ready to populate once registration arrives.
    use maat_engine::plugin::CapabilityPlugin as EngineCap;
    let cap = DummyCapability::default();
    let manifest = EngineCap::manifest(&cap);
    // declared_wire_frames is empty today (no producer-side macro pushes
    // into REGISTERED_WIRE_FRAMES yet), but the walker IS wired. This
    // assertion is a forward-compat anchor: once wire-frame registration
    // lands, this list will populate without further changes here.
    let _: &Vec<_> = &manifest.declared_wire_frames;
    let _: &Vec<_> = &manifest.declared_handlers;
    let _: &Vec<_> = &manifest.declared_capabilities;
    let _: &Vec<_> = &manifest.declared_phases;
}

// ---------------------------------------------------------------------------
// M2 — `#[capability_plugin(dependencies = [...])]` populates
// `PluginManifest::dependencies`.  Mirrors the model-plugin M2 tests
// in `plugin_macros.rs`.
// ---------------------------------------------------------------------------

#[derive(Debug, Default)]
#[capability_plugin(
    canonical_id = "github.com/maat/test-cap-with-deps",
    license      = "MIT",
    dependencies = ["github.com/maat/some-other-cap"],
)]
pub struct DepCapability;

#[async_trait::async_trait]
impl Capability for DepCapability {
    const LOCAL_NAME: &'static str = "test_dep_cap";
    type Frame = DummyFrame;
    fn scope(&self) -> CapabilityScope { CapabilityScope::Session }
    async fn handle_incoming(
        &mut self,
        _frame: Self::Frame,
        _ctx: &mut FrameContext,
    ) -> CapabilityResult<()> { Ok(()) }
}

#[test]
fn capability_plugin_with_dependencies_populates_manifest() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;
    let cap = DepCapability;
    let m = EngineCap::manifest(&cap);
    assert_eq!(m.dependencies.len(), 1);
    assert_eq!(m.dependencies[0].canonical_id.as_str(), "github.com/maat/some-other-cap");
    assert!(m.dependencies[0].required);
}

#[test]
fn capability_plugin_without_dependencies_is_empty() {
    use maat_engine::plugin::CapabilityPlugin as EngineCap;
    let cap = DummyCapability::default();
    let m = EngineCap::manifest(&cap);
    assert!(m.dependencies.is_empty());
}
