//! Skill use cases (ARCHITECTURE §14.2; L12).
//!
//! - **CRUD** in either scope: [`CreateSkill`], [`UpdateSkill`], [`DeleteSkill`],
//!   [`ListSkills`].
//! - **Assignment**: [`AssignSkillToAgent`] / [`UnassignSkillFromAgent`] mutate
//!   the project manifest entry's `skills` and announce
//!   [`DomainEvent::SkillAssigned`]. Both are idempotent.

use std::sync::Arc;

use domain::ports::{AgentContextStore, EventBus, IdGenerator, SkillStore, StoreError};
use domain::{
    Agent, AgentId, AgentManifest, AssignedSkillSnapshot, ConversationParty, DomainEvent,
    MarkdownDoc, OrchestrationCapabilitySnapshot, Project, ProjectPath, Skill, SkillId, SkillKind,
    SkillRef, SkillScope,
};

use crate::error::AppError;

// ---------------------------------------------------------------------------
// CreateSkill
// ---------------------------------------------------------------------------

/// Input for [`CreateSkill::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSkillInput {
    /// Display name (also the `.md` stem on disk).
    pub name: String,
    /// Optional one-line affordance description (feature « skills à la MCP »).
    /// `None`/empty ⇒ the skill falls back to the first line of its body when
    /// surfaced (see [`domain::Skill::effective_description`]).
    pub description: Option<String>,
    /// Capability nature to expose for this skill. Defaults to workflow when the
    /// caller does not specify it.
    pub kind: SkillKind,
    /// Initial Markdown body.
    pub content: String,
    /// Scope the skill is created in (selects its backing store).
    pub scope: SkillScope,
    /// Active project root (used only for [`SkillScope::Project`]).
    pub project_root: ProjectPath,
}

/// Output of [`CreateSkill::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSkillOutput {
    /// The created skill.
    pub skill: Skill,
}

/// Creates a skill in the store of its [`SkillScope`].
pub struct CreateSkill {
    skills: Arc<dyn SkillStore>,
    ids: Arc<dyn IdGenerator>,
}

impl CreateSkill {
    /// Builds the use case from its ports.
    #[must_use]
    pub fn new(skills: Arc<dyn SkillStore>, ids: Arc<dyn IdGenerator>) -> Self {
        Self { skills, ids }
    }

    /// Executes creation.
    ///
    /// # Errors
    /// - [`AppError::Invalid`] if `name`/`content` is empty,
    /// - [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: CreateSkillInput) -> Result<CreateSkillOutput, AppError> {
        let id = SkillId::from_uuid(self.ids.new_uuid());
        let skill = Skill::new(id, input.name, MarkdownDoc::new(input.content), input.scope)
            .map_err(|e| AppError::Invalid(e.to_string()))?
            .with_description(input.description)
            .with_kind(input.kind);
        self.skills.save(&skill, &input.project_root).await?;
        Ok(CreateSkillOutput { skill })
    }
}

// ---------------------------------------------------------------------------
// UpdateSkill
// ---------------------------------------------------------------------------

/// Input for [`UpdateSkill::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateSkillInput {
    /// Scope the skill lives in.
    pub scope: SkillScope,
    /// Skill to update.
    pub skill_id: SkillId,
    /// New Markdown body.
    pub content: String,
    /// Active project root (used only for [`SkillScope::Project`]).
    pub project_root: ProjectPath,
}

/// Output of [`UpdateSkill::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateSkillOutput {
    /// The updated skill.
    pub skill: Skill,
}

/// Replaces a skill's content (re-validating the non-empty invariant).
pub struct UpdateSkill {
    skills: Arc<dyn SkillStore>,
}

impl UpdateSkill {
    /// Builds the use case.
    #[must_use]
    pub fn new(skills: Arc<dyn SkillStore>) -> Self {
        Self { skills }
    }

    /// Executes the update.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the skill is unknown in that scope,
    /// - [`AppError::Invalid`] if the new content is empty,
    /// - [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: UpdateSkillInput) -> Result<UpdateSkillOutput, AppError> {
        let current = self
            .skills
            .get(input.scope, &input.project_root, input.skill_id)
            .await?;
        let updated = current
            .with_content(MarkdownDoc::new(input.content))
            .map_err(|e| AppError::Invalid(e.to_string()))?;
        self.skills.save(&updated, &input.project_root).await?;
        Ok(UpdateSkillOutput { skill: updated })
    }
}

// ---------------------------------------------------------------------------
// ListSkills / DeleteSkill
// ---------------------------------------------------------------------------

/// Input for [`ListSkills::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListSkillsInput {
    /// Scope to list.
    pub scope: SkillScope,
    /// Active project root (used only for [`SkillScope::Project`]).
    pub project_root: ProjectPath,
}

/// Output of [`ListSkills::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListSkillsOutput {
    /// All skills in the requested scope.
    pub skills: Vec<Skill>,
}

/// Lists the skills in one scope.
pub struct ListSkills {
    skills: Arc<dyn SkillStore>,
}

impl ListSkills {
    /// Builds the use case.
    #[must_use]
    pub fn new(skills: Arc<dyn SkillStore>) -> Self {
        Self { skills }
    }

    /// Lists skills in `input.scope`.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: ListSkillsInput) -> Result<ListSkillsOutput, AppError> {
        Ok(ListSkillsOutput {
            skills: self.skills.list(input.scope, &input.project_root).await?,
        })
    }
}

/// Input for [`DeleteSkill::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteSkillInput {
    /// Scope the skill lives in.
    pub scope: SkillScope,
    /// Skill to delete.
    pub skill_id: SkillId,
    /// Active project root (used only for [`SkillScope::Project`]).
    pub project_root: ProjectPath,
}

/// Deletes a skill from its scope's store.
///
/// Agents that referenced it keep their [`SkillRef`]; the injection step simply
/// finds nothing to resolve for the now-absent skill and skips it.
pub struct DeleteSkill {
    skills: Arc<dyn SkillStore>,
}

impl DeleteSkill {
    /// Builds the use case.
    #[must_use]
    pub fn new(skills: Arc<dyn SkillStore>) -> Self {
        Self { skills }
    }

    /// Deletes the skill.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the skill is unknown in that scope,
    /// - [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: DeleteSkillInput) -> Result<(), AppError> {
        self.skills
            .delete(input.scope, &input.project_root, input.skill_id)
            .await?;
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// ResolveAgentCapabilities / ReadSkill
// ---------------------------------------------------------------------------

/// One discoverable capability carried by an agent.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapability {
    /// Capability display name.
    pub name: String,
    /// One-line affordance description.
    pub description: String,
    /// Capability nature.
    pub kind: SkillKind,
}

impl From<&AssignedSkillSnapshot> for AgentCapability {
    fn from(skill: &AssignedSkillSnapshot) -> Self {
        Self {
            name: skill.name.clone(),
            description: skill.description.clone(),
            kind: skill.kind,
        }
    }
}

/// Input for [`ResolveAgentCapabilities::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolveAgentCapabilitiesInput {
    /// Agent whose assigned skills must be resolved.
    pub agent: Agent,
    /// Active project root used to resolve project-scoped skills.
    pub project_root: ProjectPath,
}

/// Resolves assigned skills into discoverable agent capabilities.
pub struct ResolveAgentCapabilities {
    resolver: Arc<AssignedSkillResolver>,
}

impl ResolveAgentCapabilities {
    /// Builds the use case from a skill store.
    #[must_use]
    pub fn new(skills: Arc<dyn SkillStore>) -> Self {
        Self {
            resolver: Arc::new(AssignedSkillResolver::new(skills)),
        }
    }

    /// Builds the use case from a shared resolver.
    #[must_use]
    pub fn with_resolver(resolver: Arc<AssignedSkillResolver>) -> Self {
        Self { resolver }
    }

    /// Resolves the compact capability list, skipping dangling skill refs.
    ///
    /// # Errors
    /// [`AppError::Store`] on store failures other than a missing skill.
    pub async fn execute(
        &self,
        input: ResolveAgentCapabilitiesInput,
    ) -> Result<Vec<AgentCapability>, AppError> {
        let snapshot = self
            .resolver
            .resolve_for_agent(&input.agent, &input.project_root)
            .await?;
        Ok(snapshot
            .assigned_skills
            .iter()
            .map(AgentCapability::from)
            .collect())
    }
}

/// Resolves assigned skills into the runtime capability snapshot used by both
/// context rendering and `idea_skill_read` authorization.
pub struct AssignedSkillResolver {
    skills: Arc<dyn SkillStore>,
}

impl AssignedSkillResolver {
    /// Builds the resolver from the existing skill store port.
    #[must_use]
    pub fn new(skills: Arc<dyn SkillStore>) -> Self {
        Self { skills }
    }

    /// Resolves the assigned skills for `agent` in manifest order.
    ///
    /// Dangling assignments are skipped: a deleted skill must not block a launch,
    /// and a skipped skill is therefore not readable via `idea_skill_read`.
    ///
    /// # Errors
    /// [`AppError::Store`] on store failures other than a missing skill.
    pub async fn resolve_for_agent(
        &self,
        agent: &Agent,
        root: &ProjectPath,
    ) -> Result<OrchestrationCapabilitySnapshot, AppError> {
        let resolved = self.resolve_for_agent_with_content(agent, root).await?;
        Ok(OrchestrationCapabilitySnapshot::new(
            resolved.into_iter().map(|(snapshot, _)| snapshot).collect(),
        ))
    }

    /// Resolves assigned skills and keeps their Markdown bodies for non-MCP
    /// fallback context injection.
    ///
    /// # Errors
    /// [`AppError::Store`] on store failures other than a missing skill.
    pub async fn resolve_for_agent_with_content(
        &self,
        agent: &Agent,
        root: &ProjectPath,
    ) -> Result<Vec<(AssignedSkillSnapshot, MarkdownDoc)>, AppError> {
        let mut assigned = Vec::with_capacity(agent.skills.len());
        for (index, skill_ref) in agent.skills.iter().enumerate() {
            match self
                .skills
                .get(skill_ref.scope, root, skill_ref.skill_id)
                .await
            {
                Ok(skill) => {
                    let description = skill.effective_description();
                    let snapshot = AssignedSkillSnapshot {
                        skill_id: skill.id,
                        scope: skill.scope,
                        name: skill.name,
                        description,
                        kind: skill.kind,
                        assignment_index: index as u32,
                    };
                    assigned.push((snapshot, skill.content_md));
                }
                Err(StoreError::NotFound) => {}
                Err(e) => return Err(e.into()),
            }
        }
        Ok(assigned)
    }

    /// Reads the Markdown body for one skill by name, but only inside the supplied
    /// assigned-skill snapshot.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if `name` is not assigned in `snapshot`,
    /// - [`AppError::Invalid`] if the assigned snapshot has ambiguous names,
    /// - [`AppError::Store`] on a store failure.
    pub async fn read_assigned_by_name(
        &self,
        snapshot: &OrchestrationCapabilitySnapshot,
        root: &ProjectPath,
        name: &str,
    ) -> Result<MarkdownDoc, AppError> {
        let mut matches = snapshot
            .assigned_skills
            .iter()
            .filter(|s| s.name.eq_ignore_ascii_case(name));
        let selected = match (matches.next(), matches.next()) {
            (None, _) => return Err(AppError::NotFound(format!("assigned skill `{name}`"))),
            (Some(skill), None) => skill,
            (Some(_), Some(_)) => {
                return Err(AppError::Invalid(format!(
                    "assigned skill name `{name}` is ambiguous for this agent"
                )))
            }
        };
        let skill = self
            .skills
            .get(selected.scope, root, selected.skill_id)
            .await?;
        Ok(skill.content_md)
    }
}

/// Input for [`ReadSkill::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadSkillInput {
    /// Skill display name to resolve (case-insensitive).
    pub name: String,
    /// Active project.
    pub project: Project,
    /// Current requester identity from the orchestration handshake.
    pub requester: ConversationParty,
}

/// Reads an assigned skill's Markdown body **by name** — the application side of
/// the MCP `idea_skill_read` tool.
///
/// The same [`AssignedSkillResolver`] that builds the runtime capability
/// snapshot authorizes the read. A model can read only skills assigned to the
/// requesting agent in the current manifest.
pub struct ReadSkill {
    contexts: Arc<dyn AgentContextStore>,
    resolver: Arc<AssignedSkillResolver>,
}

impl ReadSkill {
    /// Builds the use case from existing ports.
    #[must_use]
    pub fn new(contexts: Arc<dyn AgentContextStore>, skills: Arc<dyn SkillStore>) -> Self {
        Self {
            contexts,
            resolver: Arc::new(AssignedSkillResolver::new(skills)),
        }
    }

    /// Builds the use case from a shared resolver.
    #[must_use]
    pub fn with_resolver(
        contexts: Arc<dyn AgentContextStore>,
        resolver: Arc<AssignedSkillResolver>,
    ) -> Self {
        Self { contexts, resolver }
    }

    /// Resolves the requester's assigned snapshot and returns the skill body.
    ///
    /// # Errors
    /// - [`AppError::Invalid`] if requester is not an agent or the assigned name is ambiguous,
    /// - [`AppError::NotFound`] if requester/skill is absent,
    /// - [`AppError::Store`] on store failures.
    pub async fn execute(&self, input: ReadSkillInput) -> Result<MarkdownDoc, AppError> {
        let requester = input.requester.as_agent().ok_or_else(|| {
            AppError::Invalid("idea_skill_read requires an agent requester".to_owned())
        })?;
        let manifest = self.contexts.load_manifest(&input.project).await?;
        let entry = manifest
            .entries
            .iter()
            .find(|e| e.agent_id == requester)
            .ok_or_else(|| AppError::NotFound(format!("agent {requester}")))?;
        let agent = entry
            .to_agent()
            .map_err(|e| AppError::Invalid(e.to_string()))?;
        let snapshot = self
            .resolver
            .resolve_for_agent(&agent, &input.project.root)
            .await?;
        self.resolver
            .read_assigned_by_name(&snapshot, &input.project.root, &input.name)
            .await
    }
}

// ---------------------------------------------------------------------------
// AssignSkillToAgent / UnassignSkillFromAgent
// ---------------------------------------------------------------------------

/// Input for [`AssignSkillToAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssignSkillToAgentInput {
    /// The owning project.
    pub project: Project,
    /// The agent receiving the skill.
    pub agent_id: AgentId,
    /// The skill to assign.
    pub skill: SkillRef,
}

/// Assigns a skill to an agent by recording a [`SkillRef`] in its manifest entry.
/// Idempotent: re-assigning the same skill is a no-op (no duplicate).
pub struct AssignSkillToAgent {
    contexts: Arc<dyn AgentContextStore>,
    events: Arc<dyn EventBus>,
}

impl AssignSkillToAgent {
    /// Builds the use case from its ports.
    #[must_use]
    pub fn new(contexts: Arc<dyn AgentContextStore>, events: Arc<dyn EventBus>) -> Self {
        Self { contexts, events }
    }

    /// Executes the assignment.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the agent is unknown to the project,
    /// - [`AppError::Invalid`] if the resulting manifest is invalid,
    /// - [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: AssignSkillToAgentInput) -> Result<(), AppError> {
        let mut manifest = self.contexts.load_manifest(&input.project).await?;
        let entry = manifest
            .entries
            .iter_mut()
            .find(|e| e.agent_id == input.agent_id)
            .ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;

        // Mutate through the domain entity so the dedup invariant is enforced in
        // one place, then fold the result back into the manifest entry.
        let mut agent = entry
            .to_agent()
            .map_err(|e| AppError::Invalid(e.to_string()))?;
        let changed = agent.assign_skill(input.skill);
        *entry = domain::ManifestEntry::from_agent(&agent);

        if changed {
            self.persist_and_announce(
                &input.project,
                manifest,
                input.agent_id,
                input.skill.skill_id,
                true,
            )
            .await?;
        }
        Ok(())
    }

    /// Saves the manifest and announces the assignment change.
    async fn persist_and_announce(
        &self,
        project: &Project,
        manifest: AgentManifest,
        agent_id: AgentId,
        skill_id: SkillId,
        assigned: bool,
    ) -> Result<(), AppError> {
        let manifest = AgentManifest::new(manifest.version, manifest.entries)
            .map_err(|e| AppError::Invalid(e.to_string()))?;
        self.contexts.save_manifest(project, &manifest).await?;
        self.events.publish(DomainEvent::SkillAssigned {
            agent_id,
            skill_id,
            assigned,
        });
        Ok(())
    }
}

/// Input for [`UnassignSkillFromAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnassignSkillFromAgentInput {
    /// The owning project.
    pub project: Project,
    /// The agent losing the skill.
    pub agent_id: AgentId,
    /// The skill to unassign.
    pub skill_id: SkillId,
}

/// Removes a skill assignment from an agent. Idempotent: unassigning a skill the
/// agent does not carry is a no-op.
pub struct UnassignSkillFromAgent {
    contexts: Arc<dyn AgentContextStore>,
    events: Arc<dyn EventBus>,
}

impl UnassignSkillFromAgent {
    /// Builds the use case from its ports.
    #[must_use]
    pub fn new(contexts: Arc<dyn AgentContextStore>, events: Arc<dyn EventBus>) -> Self {
        Self { contexts, events }
    }

    /// Executes the un-assignment.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the agent is unknown to the project,
    /// - [`AppError::Invalid`] if the resulting manifest is invalid,
    /// - [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: UnassignSkillFromAgentInput) -> Result<(), AppError> {
        let mut manifest = self.contexts.load_manifest(&input.project).await?;
        let entry = manifest
            .entries
            .iter_mut()
            .find(|e| e.agent_id == input.agent_id)
            .ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;

        let mut agent = entry
            .to_agent()
            .map_err(|e| AppError::Invalid(e.to_string()))?;
        let changed = agent.unassign_skill(input.skill_id);
        *entry = domain::ManifestEntry::from_agent(&agent);

        if changed {
            let manifest = AgentManifest::new(manifest.version, manifest.entries)
                .map_err(|e| AppError::Invalid(e.to_string()))?;
            self.contexts
                .save_manifest(&input.project, &manifest)
                .await?;
            self.events.publish(DomainEvent::SkillAssigned {
                agent_id: input.agent_id,
                skill_id: input.skill_id,
                assigned: false,
            });
        }
        Ok(())
    }
}

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

    use async_trait::async_trait;
    use domain::ports::{AgentContextStore, SkillStore, StoreError};
    use domain::remote::RemoteRef;

    /// In-memory [`SkillStore`] fake: skills are bucketed by scope, ignoring the
    /// project root (the [`ReadSkill`] resolution logic is root-agnostic — it only
    /// switches scope). Enough to exercise project-first resolution, shadowing,
    /// ambiguity and absence without touching disk.
    #[derive(Default)]
    struct FakeSkillStore {
        global: Mutex<Vec<Skill>>,
        project: Mutex<Vec<Skill>>,
    }

    impl FakeSkillStore {
        fn bucket(&self, scope: SkillScope) -> &Mutex<Vec<Skill>> {
            match scope {
                SkillScope::Global => &self.global,
                SkillScope::Project => &self.project,
            }
        }

        fn push(&self, skill: Skill) {
            self.bucket(skill.scope).lock().unwrap().push(skill);
        }
    }

    #[async_trait]
    impl SkillStore for FakeSkillStore {
        async fn list(
            &self,
            scope: SkillScope,
            _root: &ProjectPath,
        ) -> Result<Vec<Skill>, StoreError> {
            Ok(self.bucket(scope).lock().unwrap().clone())
        }
        async fn get(
            &self,
            scope: SkillScope,
            _root: &ProjectPath,
            id: SkillId,
        ) -> Result<Skill, StoreError> {
            self.bucket(scope)
                .lock()
                .unwrap()
                .iter()
                .find(|s| s.id == id)
                .cloned()
                .ok_or(StoreError::NotFound)
        }
        async fn save(&self, skill: &Skill, _root: &ProjectPath) -> Result<(), StoreError> {
            self.push(skill.clone());
            Ok(())
        }
        async fn delete(
            &self,
            _scope: SkillScope,
            _root: &ProjectPath,
            _id: SkillId,
        ) -> Result<(), StoreError> {
            Ok(())
        }
    }

    struct FakeAgentContextStore {
        manifest: Mutex<AgentManifest>,
    }

    impl FakeAgentContextStore {
        fn new(manifest: AgentManifest) -> Self {
            Self {
                manifest: Mutex::new(manifest),
            }
        }
    }

    #[async_trait]
    impl AgentContextStore for FakeAgentContextStore {
        async fn read_context(
            &self,
            _project: &Project,
            _agent: &AgentId,
        ) -> Result<MarkdownDoc, StoreError> {
            Ok(MarkdownDoc::new("# Persona"))
        }

        async fn write_context(
            &self,
            _project: &Project,
            _agent: &AgentId,
            _md: &MarkdownDoc,
        ) -> Result<(), StoreError> {
            Ok(())
        }

        async fn load_manifest(&self, _project: &Project) -> Result<AgentManifest, StoreError> {
            Ok(self.manifest.lock().unwrap().clone())
        }

        async fn save_manifest(
            &self,
            _project: &Project,
            manifest: &AgentManifest,
        ) -> Result<(), StoreError> {
            *self.manifest.lock().unwrap() = manifest.clone();
            Ok(())
        }
    }

    fn root() -> ProjectPath {
        ProjectPath::new("/proj").unwrap()
    }

    fn project() -> Project {
        Project::new(
            domain::ProjectId::from_uuid(uuid::Uuid::from_u128(100)),
            "Project",
            root(),
            RemoteRef::Local,
            0,
        )
        .unwrap()
    }

    fn agent_id(n: u128) -> AgentId {
        AgentId::from_uuid(uuid::Uuid::from_u128(n))
    }

    fn profile_id(n: u128) -> domain::ProfileId {
        domain::ProfileId::from_uuid(uuid::Uuid::from_u128(n))
    }

    fn manifest_for(agent_id: AgentId, skills: Vec<SkillRef>) -> AgentManifest {
        let agent = Agent::new(
            agent_id,
            "Dev",
            "agents/dev.md",
            profile_id(900),
            domain::AgentOrigin::Scratch,
            false,
        )
        .unwrap()
        .with_skills(skills);
        AgentManifest::new(1, vec![domain::ManifestEntry::from_agent(&agent)]).unwrap()
    }

    fn skill(id: u128, name: &str, body: &str, scope: SkillScope) -> Skill {
        Skill::new(
            SkillId::from_uuid(uuid::Uuid::from_u128(id)),
            name,
            MarkdownDoc::new(body),
            scope,
        )
        .unwrap()
    }

    #[tokio::test]
    async fn resolve_agent_capabilities_returns_compact_kind_aware_affordances() {
        let store = Arc::new(FakeSkillStore::default());
        store.push(
            skill(1, "deploy", "# Deploy\n\nbody", SkillScope::Project)
                .with_description(Some("Deploys safely".to_owned())),
        );
        store.push(
            skill(2, "runbook", "# Runbook", SkillScope::Global).with_kind(SkillKind::Reference),
        );
        let requester = agent_id(10);
        let agent = manifest_for(
            requester,
            vec![
                SkillRef::new(
                    SkillId::from_uuid(uuid::Uuid::from_u128(1)),
                    SkillScope::Project,
                ),
                SkillRef::new(
                    SkillId::from_uuid(uuid::Uuid::from_u128(99)),
                    SkillScope::Project,
                ),
                SkillRef::new(
                    SkillId::from_uuid(uuid::Uuid::from_u128(2)),
                    SkillScope::Global,
                ),
            ],
        )
        .entries[0]
            .to_agent()
            .unwrap();
        let uc = ResolveAgentCapabilities::new(store);

        let capabilities = uc
            .execute(ResolveAgentCapabilitiesInput {
                agent,
                project_root: root(),
            })
            .await
            .unwrap();

        assert_eq!(capabilities.len(), 2);
        assert_eq!(capabilities[0].name, "deploy");
        assert_eq!(capabilities[0].description, "Deploys safely");
        assert_eq!(capabilities[0].kind, SkillKind::Workflow);
        assert_eq!(capabilities[1].name, "runbook");
        assert_eq!(capabilities[1].description, "Runbook");
        assert_eq!(capabilities[1].kind, SkillKind::Reference);
    }

    fn read_skill_uc(
        store: Arc<FakeSkillStore>,
        manifest: AgentManifest,
    ) -> (ReadSkill, Project, AgentId) {
        let requester = manifest.entries[0].agent_id;
        let contexts = Arc::new(FakeAgentContextStore::new(manifest));
        (ReadSkill::new(contexts, store), project(), requester)
    }

    #[tokio::test]
    async fn read_skill_returns_assigned_project_skill() {
        let store = Arc::new(FakeSkillStore::default());
        store.push(skill(1, "deploy", "PROJECT_BODY", SkillScope::Project));

        let requester = agent_id(10);
        let manifest = manifest_for(
            requester,
            vec![SkillRef {
                skill_id: SkillId::from_uuid(uuid::Uuid::from_u128(1)),
                scope: SkillScope::Project,
            }],
        );
        let (uc, project, requester) = read_skill_uc(store, manifest);

        let body = uc
            .execute(ReadSkillInput {
                name: "deploy".to_owned(),
                project,
                requester: ConversationParty::agent(requester),
            })
            .await
            .unwrap();
        assert_eq!(body.as_str(), "PROJECT_BODY");
    }

    #[tokio::test]
    async fn read_skill_returns_assigned_global_skill() {
        let store = Arc::new(FakeSkillStore::default());
        store.push(skill(1, "deploy", "GLOBAL_BODY", SkillScope::Global));

        let requester = agent_id(10);
        let manifest = manifest_for(
            requester,
            vec![SkillRef {
                skill_id: SkillId::from_uuid(uuid::Uuid::from_u128(1)),
                scope: SkillScope::Global,
            }],
        );
        let (uc, project, requester) = read_skill_uc(store, manifest);

        let body = uc
            .execute(ReadSkillInput {
                name: "deploy".to_owned(),
                project,
                requester: ConversationParty::agent(requester),
            })
            .await
            .unwrap();
        assert_eq!(body.as_str(), "GLOBAL_BODY");
    }

    #[tokio::test]
    async fn read_skill_refuses_unassigned_name_even_if_skill_exists() {
        let store = Arc::new(FakeSkillStore::default());
        store.push(skill(1, "deploy", "PROJECT_BODY", SkillScope::Project));

        let requester = agent_id(10);
        let manifest = manifest_for(requester, Vec::new());
        let (uc, project, requester) = read_skill_uc(store, manifest);

        let err = uc
            .execute(ReadSkillInput {
                name: "deploy".to_owned(),
                project,
                requester: ConversationParty::agent(requester),
            })
            .await
            .unwrap_err();
        assert!(matches!(err, AppError::NotFound(_)), "got {err:?}");
    }

    #[tokio::test]
    async fn read_skill_is_case_insensitive_within_assigned_snapshot() {
        let store = Arc::new(FakeSkillStore::default());
        store.push(skill(1, "Deploy", "BODY", SkillScope::Project));

        let requester = agent_id(10);
        let manifest = manifest_for(
            requester,
            vec![SkillRef {
                skill_id: SkillId::from_uuid(uuid::Uuid::from_u128(1)),
                scope: SkillScope::Project,
            }],
        );
        let (uc, project, requester) = read_skill_uc(store, manifest);

        let body = uc
            .execute(ReadSkillInput {
                name: "deploy".to_owned(),
                project,
                requester: ConversationParty::agent(requester),
            })
            .await
            .unwrap();
        assert_eq!(body.as_str(), "BODY");
    }

    #[tokio::test]
    async fn read_skill_requires_agent_requester() {
        let store = Arc::new(FakeSkillStore::default());
        let requester = agent_id(10);
        let manifest = manifest_for(requester, Vec::new());
        let (uc, project, _) = read_skill_uc(store, manifest);

        let err = uc
            .execute(ReadSkillInput {
                name: "ghost".to_owned(),
                project,
                requester: ConversationParty::User,
            })
            .await
            .unwrap_err();
        assert!(matches!(err, AppError::Invalid(_)), "got {err:?}");
    }

    #[tokio::test]
    async fn read_skill_ambiguous_assigned_name_is_invalid() {
        let store = Arc::new(FakeSkillStore::default());
        store.push(skill(1, "deploy", "ONE", SkillScope::Project));
        store.push(skill(2, "Deploy", "TWO", SkillScope::Global));

        let requester = agent_id(10);
        let manifest = manifest_for(
            requester,
            vec![
                SkillRef {
                    skill_id: SkillId::from_uuid(uuid::Uuid::from_u128(1)),
                    scope: SkillScope::Project,
                },
                SkillRef {
                    skill_id: SkillId::from_uuid(uuid::Uuid::from_u128(2)),
                    scope: SkillScope::Global,
                },
            ],
        );
        let (uc, project, requester) = read_skill_uc(store, manifest);

        let err = uc
            .execute(ReadSkillInput {
                name: "deploy".to_owned(),
                project,
                requester: ConversationParty::agent(requester),
            })
            .await
            .unwrap_err();
        assert!(matches!(err, AppError::Invalid(_)), "got {err:?}");
    }
}
