//! S25 Phase 1+2 — verify the macro stack populates the linkme global
//! slices and `#[plugin(...)]` assembles a correct `PluginManifest`.
//!
//! Phase 5a: handlers are now `&self` methods on the plugin struct,
//! so this test exercises that shape. The legacy free-fn shape is
//! gone.

use maat_sdk::events::{EventKind, GameEvent};
use maat_sdk::handler::{Decision, HandlerResponse};
use maat_sdk::plugin::slices::{
    GameContext, REGISTERED_ENTITIES, REGISTERED_HANDLERS,
};
use maat_sdk::plugin::PluginKind;
use maat_sdk::priority::Priority;
use maat_sdk::traits::{FeaturedData, NamedData, PipelineRole};
use maat_sdk_derive::{handlers, plugin, EntityType};

#[derive(EntityType)]
#[entity(type_id = 9001, display = "Sample Pawn")]
#[allow(dead_code)]
pub struct SamplePawn {
    pub featured: FeaturedData,
}

#[derive(EntityType)]
#[entity(type_id = 9002, display = "Sample King")]
#[allow(dead_code)]
pub struct SampleKing {
    pub named: NamedData,
}

#[plugin(
    canonical_id = "github.com/maat/test-sample-plugin",
    display_name = "Sample Plugin",
    authors = ["Test Author"],
    description = "A test plugin for the macro stack.",
    license = "MIT",
    repository = "https://example.com/sample"
)]
#[derive(Default)]
pub struct SamplePlugin;

#[handlers]
impl SamplePlugin {
    #[handler(
        event = EventKind::Moved,
        role = PipelineRole::Filter,
        priority = Priority::BASE,
        name = "sample:on_moved"
    )]
    fn on_moved(&self, _event: &GameEvent, _ctx: &impl GameContext) -> HandlerResponse {
        HandlerResponse::from(Decision::Allow)
    }

    #[handler(
        event = EventKind::TurnEnded,
        role = PipelineRole::React,
        name = "sample:on_turn_end"
    )]
    fn on_turn_end(&self, _event: &GameEvent, _ctx: &impl GameContext) -> HandlerResponse {
        HandlerResponse::from(Decision::Allow)
    }
}

// SamplePlugin needs a ModelPlugin impl for the thunks' downcast to
// type-check. The methods themselves don't need to be reachable
// through dispatch — this test only validates that registration works.
#[maat_sdk_derive::model_plugin]
impl maat_engine::plugin::ModelPlugin for SamplePlugin {
    fn manifest(&self) -> &maat_sdk::plugin::PluginManifest {
        Self::manifest()
    }
    fn run_setup(&mut self, _ctx: &mut dyn maat_sdk::SetupContext) {}
    fn backend(&self) -> maat_sdk::plugin::PluginBackend {
        maat_sdk::plugin::PluginBackend::Static
    }
}

#[test]
fn derive_entity_registers_into_linkme_slice() {
    let names: Vec<&str> = REGISTERED_ENTITIES
        .iter()
        .filter(|r| r.plugin_crate_name == env!("CARGO_PKG_NAME"))
        .map(|r| r.type_name)
        .collect();
    assert!(names.contains(&"SamplePawn"), "got {names:?}");
    assert!(names.contains(&"SampleKing"), "got {names:?}");
}

#[test]
fn handler_attribute_registers_into_linkme_slice() {
    let names: Vec<&str> = REGISTERED_HANDLERS
        .iter()
        .filter(|r| r.plugin_crate_name == env!("CARGO_PKG_NAME"))
        .map(|r| r.name)
        .collect();
    assert!(names.contains(&"sample:on_moved"), "got {names:?}");
    assert!(names.contains(&"sample:on_turn_end"), "got {names:?}");
}

#[test]
fn handler_registration_carries_method_name() {
    let methods: Vec<&str> = REGISTERED_HANDLERS
        .iter()
        .filter(|r| {
            r.plugin_crate_name == env!("CARGO_PKG_NAME") && r.plugin_struct == "SamplePlugin"
        })
        .map(|r| r.method_name)
        .collect();
    assert!(methods.contains(&"on_moved"), "got {methods:?}");
    assert!(methods.contains(&"on_turn_end"), "got {methods:?}");
}

#[test]
fn plugin_attribute_filters_slices_by_cargo_pkg_name() {
    let m = SamplePlugin::manifest();
    // Two of our entities are present; nothing from outside this crate
    // bleeds in. (Other tests in this same crate may also register
    // entities via #[derive(EntityType)], so we only verify "at least
    // ours, and only ones from this crate".)
    let our_types: Vec<&str> = m.declared_types.iter().map(|t| t.type_name.as_str()).collect();
    assert!(our_types.contains(&"SamplePawn"));
    assert!(our_types.contains(&"SampleKing"));
}

#[test]
fn plugin_attribute_generates_correct_manifest() {
    let m = SamplePlugin::manifest();
    assert_eq!(m.canonical_id.as_str(), "github.com/maat/test-sample-plugin");
    assert_eq!(m.display_name, "Sample Plugin");
    assert_eq!(m.kind, PluginKind::Model);
    assert_eq!(m.authors, vec!["Test Author".to_string()]);
    assert_eq!(m.license.as_str(), "MIT");
    assert_eq!(m.repository.as_deref(), Some("https://example.com/sample"));
    assert_eq!(m.manifest_format_version, 1);

    // Handlers from this crate, filtered to those tagged with our
    // plugin struct, should include both we declared.
    let handler_names: Vec<&str> = m
        .declared_handlers
        .iter()
        .map(|h| h.name.as_str())
        .collect();
    assert!(handler_names.contains(&"sample:on_moved"));
    assert!(handler_names.contains(&"sample:on_turn_end"));
}

#[test]
fn declared_handlers_sorted_by_method_name() {
    // The dispatch table and the manifest must agree on local_id
    // ordering. Both sort by method_name. SamplePlugin has methods
    // `on_moved` (M) and `on_turn_end` (T), which sort to that order.
    let m = SamplePlugin::manifest();
    // Filter to our plugin's handlers only.
    let names: Vec<&str> = m.declared_handlers.iter().map(|h| h.name.as_str()).collect();
    let our: Vec<&&str> = names
        .iter()
        .filter(|n| n.starts_with("sample:"))
        .collect();
    // Method names sorted: "on_moved" < "on_turn_end", so wire-names
    // map to "sample:on_moved" before "sample:on_turn_end".
    assert_eq!(our.len(), 2);
    assert_eq!(*our[0], "sample:on_moved");
    assert_eq!(*our[1], "sample:on_turn_end");
}

#[test]
fn plugin_manifest_caches_in_oncelock() {
    let m1 = SamplePlugin::manifest() as *const _;
    let m2 = SamplePlugin::manifest() as *const _;
    assert_eq!(m1, m2);
}

// ---------------------------------------------------------------------------
// Phase 5c (S25): the `#[handler(priority = ...)]` arg accepts any
// `Expr`, not just named bands. A plugin can use `priority = Priority::new(150)`
// or `priority = BASE` (with a `use` import in scope) — anything that
// evaluates to a `Priority` value works.
// ---------------------------------------------------------------------------

#[plugin(canonical_id = "github.com/maat/test-arb-priority", license = "MIT")]
#[derive(Default)]
pub struct ArbPriorityPlugin;

#[handlers]
impl ArbPriorityPlugin {
    #[handler(
        event = EventKind::Moved,
        role = PipelineRole::React,
        priority = maat_sdk::priority::Priority::new(150),
        name = "arb:p150"
    )]
    fn handle_p150(&self, _e: &GameEvent, _c: &impl GameContext) -> HandlerResponse {
        HandlerResponse::from(Decision::Allow)
    }
}

#[maat_sdk_derive::model_plugin]
impl maat_engine::plugin::ModelPlugin for ArbPriorityPlugin {
    fn manifest(&self) -> &maat_sdk::plugin::PluginManifest { Self::manifest() }
    fn run_setup(&mut self, _c: &mut dyn maat_sdk::SetupContext) {}
    fn backend(&self) -> maat_sdk::plugin::PluginBackend {
        maat_sdk::plugin::PluginBackend::Static
    }
}

#[test]
fn handler_macro_accepts_arbitrary_priority_expr() {
    let m = ArbPriorityPlugin::manifest();
    let h = m.declared_handlers.iter().find(|h| h.name == "arb:p150").expect("arb:p150 registered");
    assert_eq!(h.priority.value(), 150);
}

// ---------------------------------------------------------------------------
// M2 — `#[plugin(dependencies = [...])]` populates
// `PluginManifest::dependencies`.  The shorthand string-list form
// expands each entry into a `PluginDependency { canonical_id,
// required: true, linked_against: ([0u8;32], [0u8;32]), needs:
// StructuralNeeds::default() }`.  The real `linked_against` hashes
// are deferred to install-time resolution.
//
// Without `dependencies = [...]`, the field stays an empty Vec — the
// pre-M2 behaviour preserved for plugins that don't declare deps.
//
// Multiple entries must preserve declared order so install-time
// topo-sort / interface_hash derivation is reproducible.
// ---------------------------------------------------------------------------

#[plugin(
    canonical_id = "github.com/maat/test-deps-plugin",
    license = "MIT",
    dependencies = ["github.com/maat/base-game"]
)]
#[derive(Default)]
pub struct DepsPlugin;

#[maat_sdk_derive::model_plugin]
impl maat_engine::plugin::ModelPlugin for DepsPlugin {
    fn manifest(&self) -> &maat_sdk::plugin::PluginManifest { Self::manifest() }
    fn run_setup(&mut self, _c: &mut dyn maat_sdk::SetupContext) {}
    fn backend(&self) -> maat_sdk::plugin::PluginBackend {
        maat_sdk::plugin::PluginBackend::Static
    }
}

#[plugin(
    canonical_id = "github.com/maat/test-multidep-plugin",
    license = "MIT",
    dependencies = [
        "github.com/maat/a-dep",
        "github.com/maat/b-dep",
        "github.com/maat/c-dep",
    ]
)]
#[derive(Default)]
pub struct MultiDepPlugin;

#[maat_sdk_derive::model_plugin]
impl maat_engine::plugin::ModelPlugin for MultiDepPlugin {
    fn manifest(&self) -> &maat_sdk::plugin::PluginManifest { Self::manifest() }
    fn run_setup(&mut self, _c: &mut dyn maat_sdk::SetupContext) {}
    fn backend(&self) -> maat_sdk::plugin::PluginBackend {
        maat_sdk::plugin::PluginBackend::Static
    }
}

#[test]
fn plugin_with_dependencies_populates_manifest() {
    let m = DepsPlugin::manifest();
    assert_eq!(m.dependencies.len(), 1, "got {:?}", m.dependencies);
    let dep = &m.dependencies[0];
    assert_eq!(dep.canonical_id.as_str(), "github.com/maat/base-game");
    assert!(dep.required, "shorthand form must mark deps as required");
    assert_eq!(
        dep.linked_against,
        ([0u8; 32], [0u8; 32]),
        "linked_against hashes are deferred to install-time"
    );
    assert!(
        dep.needs.is_empty(),
        "shorthand form emits empty StructuralNeeds"
    );
}

#[test]
fn plugin_without_dependencies_is_empty() {
    // SamplePlugin (declared above) omits `dependencies = [...]` —
    // regression check that the field stays an empty Vec.
    let m = SamplePlugin::manifest();
    assert!(
        m.dependencies.is_empty(),
        "missing dependencies arg should yield empty vec, got {:?}",
        m.dependencies
    );
}

#[test]
fn plugin_with_multiple_dependencies_preserves_order() {
    let m = MultiDepPlugin::manifest();
    let ids: Vec<&str> = m
        .dependencies
        .iter()
        .map(|d| d.canonical_id.as_str())
        .collect();
    assert_eq!(
        ids,
        vec![
            "github.com/maat/a-dep",
            "github.com/maat/b-dep",
            "github.com/maat/c-dep",
        ],
        "declared order must be preserved for deterministic interface_hash"
    );
}

#[test]
fn plugin_dependencies_contribute_to_interface_hash() {
    // M1 + M2 — `compute_interface_hash` mixes each dep's
    // canonical_id + linked_against into the hash.  Spot-check that
    // declaring a dep produces a different hash than not declaring
    // one (even though the linked_against hashes are still zeros).
    let with_dep = DepsPlugin::manifest().interface_hash;
    let without = SamplePlugin::manifest().interface_hash;
    // The two plugins also differ in canonical_id / handlers, so the
    // hashes will differ for other reasons too.  The stricter check
    // is "DepsPlugin's hash is NOT what it would be if its
    // dependencies vec were empty" — but we can't easily synthesize
    // the alternative without rebuilding the manifest.  Instead we
    // assert presence + non-equality with the empty-deps baseline as
    // a smoke test.
    assert_ne!(with_dep, without);
    assert_ne!(with_dep, [0u8; 32], "interface_hash should not be zero");
}
