//! L12 tests for the skill use cases with in-memory port fakes (no real
//! store/FS): CRUD across scopes (`CreateSkill`, `UpdateSkill`, `DeleteSkill`,
//! `ListSkills`) and the manifest-mutating assignment
//! (`AssignSkillToAgent` / `UnassignSkillFromAgent`), asserting the
//! `SkillAssigned` event and idempotence.

use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use domain::events::DomainEvent;
use domain::ids::{AgentId, ProfileId, ProjectId, SkillId};
use domain::markdown::MarkdownDoc;
use domain::ports::{
    AgentContextStore, EventBus, EventStream, IdGenerator, SkillStore, StoreError,
};
use domain::skill::{Skill, SkillScope};
use domain::{AgentManifest, ManifestEntry, Project, ProjectPath, RemoteRef, SkillRef};
use uuid::Uuid;

use application::{
    AssignSkillToAgent, AssignSkillToAgentInput, CreateSkill, CreateSkillInput, DeleteSkill,
    DeleteSkillInput, ListSkills, ListSkillsInput, UnassignSkillFromAgent,
    UnassignSkillFromAgentInput, UpdateSkill, UpdateSkillInput,
};

// ---------------------------------------------------------------------------
// Fakes
// ---------------------------------------------------------------------------

/// In-memory skill store keyed by `(scope, id)`, honouring scope isolation.
#[derive(Clone, Default)]
struct FakeSkills(Arc<Mutex<Vec<Skill>>>);
#[async_trait]
impl SkillStore for FakeSkills {
    async fn list(&self, scope: SkillScope, _root: &ProjectPath) -> Result<Vec<Skill>, StoreError> {
        Ok(self
            .0
            .lock()
            .unwrap()
            .iter()
            .filter(|s| s.scope == scope)
            .cloned()
            .collect())
    }
    async fn get(
        &self,
        scope: SkillScope,
        _root: &ProjectPath,
        id: SkillId,
    ) -> Result<Skill, StoreError> {
        self.0
            .lock()
            .unwrap()
            .iter()
            .find(|s| s.scope == scope && s.id == id)
            .cloned()
            .ok_or(StoreError::NotFound)
    }
    async fn save(&self, skill: &Skill, _root: &ProjectPath) -> Result<(), StoreError> {
        let mut v = self.0.lock().unwrap();
        if let Some(slot) = v
            .iter_mut()
            .find(|s| s.scope == skill.scope && s.id == skill.id)
        {
            *slot = skill.clone();
        } else {
            v.push(skill.clone());
        }
        Ok(())
    }
    async fn delete(
        &self,
        scope: SkillScope,
        _root: &ProjectPath,
        id: SkillId,
    ) -> Result<(), StoreError> {
        let mut v = self.0.lock().unwrap();
        let before = v.len();
        v.retain(|s| !(s.scope == scope && s.id == id));
        if v.len() == before {
            return Err(StoreError::NotFound);
        }
        Ok(())
    }
}

#[derive(Clone)]
struct FakeContexts(Arc<Mutex<AgentManifest>>);
impl FakeContexts {
    fn new(entries: Vec<ManifestEntry>) -> Self {
        Self(Arc::new(Mutex::new(AgentManifest {
            version: 1,
            entries,
            orchestrator: None,
        })))
    }
    fn manifest(&self) -> AgentManifest {
        self.0.lock().unwrap().clone()
    }
}
#[async_trait]
impl AgentContextStore for FakeContexts {
    async fn read_context(
        &self,
        _p: &Project,
        _agent: &AgentId,
    ) -> Result<MarkdownDoc, StoreError> {
        Err(StoreError::NotFound)
    }
    async fn write_context(
        &self,
        _p: &Project,
        _agent: &AgentId,
        _md: &MarkdownDoc,
    ) -> Result<(), StoreError> {
        Ok(())
    }
    async fn load_manifest(&self, _p: &Project) -> Result<AgentManifest, StoreError> {
        Ok(self.manifest())
    }
    async fn save_manifest(&self, _p: &Project, m: &AgentManifest) -> Result<(), StoreError> {
        *self.0.lock().unwrap() = m.clone();
        Ok(())
    }
}

#[derive(Default, Clone)]
struct SpyBus(Arc<Mutex<Vec<DomainEvent>>>);
impl SpyBus {
    fn events(&self) -> Vec<DomainEvent> {
        self.0.lock().unwrap().clone()
    }
}
impl EventBus for SpyBus {
    fn publish(&self, event: DomainEvent) {
        self.0.lock().unwrap().push(event);
    }
    fn subscribe(&self) -> EventStream {
        Box::new(std::iter::empty())
    }
}

struct SeqIds(Mutex<u128>);
impl SeqIds {
    fn new() -> Self {
        Self(Mutex::new(1))
    }
}
impl IdGenerator for SeqIds {
    fn new_uuid(&self) -> Uuid {
        let mut n = self.0.lock().unwrap();
        let id = Uuid::from_u128(*n);
        *n += 1;
        id
    }
}

// ---------------------------------------------------------------------------
// Builders
// ---------------------------------------------------------------------------

fn pid(n: u128) -> ProfileId {
    ProfileId::from_uuid(Uuid::from_u128(n))
}
fn aid(n: u128) -> AgentId {
    AgentId::from_uuid(Uuid::from_u128(n))
}
fn sid(n: u128) -> SkillId {
    SkillId::from_uuid(Uuid::from_u128(n))
}
fn root() -> ProjectPath {
    ProjectPath::new("/home/me/demo").unwrap()
}
fn project() -> Project {
    Project::new(
        ProjectId::from_uuid(Uuid::from_u128(1000)),
        "demo",
        root(),
        RemoteRef::local(),
        1_700_000_000_000,
    )
    .unwrap()
}
fn scratch_entry(agent: AgentId) -> ManifestEntry {
    ManifestEntry::new(agent, "A", "agents/a.md", pid(1), None, false, None).unwrap()
}

// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------

#[tokio::test]
async fn create_skill_persists_in_its_scope() {
    let store = FakeSkills::default();
    let out = CreateSkill::new(Arc::new(store.clone()), Arc::new(SeqIds::new()))
        .execute(CreateSkillInput {
            name: "refactor".to_owned(),
            description: Some("Refactors code".to_owned()),
            content: "# body".to_owned(),
            scope: SkillScope::Project,
            project_root: root(),
        })
        .await
        .unwrap();

    assert_eq!(out.skill.scope, SkillScope::Project);
    // The one-line affordance description flows through the use case onto the skill.
    assert_eq!(out.skill.description.as_deref(), Some("Refactors code"));
    assert_eq!(
        store
            .list(SkillScope::Project, &root())
            .await
            .unwrap()
            .len(),
        1
    );
    assert!(store
        .list(SkillScope::Global, &root())
        .await
        .unwrap()
        .is_empty());
}

#[tokio::test]
async fn create_skill_rejects_empty_content() {
    let store = FakeSkills::default();
    let err = CreateSkill::new(Arc::new(store), Arc::new(SeqIds::new()))
        .execute(CreateSkillInput {
            name: "k".to_owned(),
            description: None,
            content: String::new(),
            scope: SkillScope::Global,
            project_root: root(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "INVALID");
}

#[tokio::test]
async fn update_skill_replaces_content() {
    let store = FakeSkills::default();
    store
        .save(
            &Skill::new(sid(1), "k", MarkdownDoc::new("v1"), SkillScope::Global).unwrap(),
            &root(),
        )
        .await
        .unwrap();

    let out = UpdateSkill::new(Arc::new(store.clone()))
        .execute(UpdateSkillInput {
            scope: SkillScope::Global,
            skill_id: sid(1),
            content: "v2".to_owned(),
            project_root: root(),
        })
        .await
        .unwrap();
    assert_eq!(out.skill.content_md.as_str(), "v2");
}

#[tokio::test]
async fn update_unknown_skill_is_not_found() {
    let store = FakeSkills::default();
    let err = UpdateSkill::new(Arc::new(store))
        .execute(UpdateSkillInput {
            scope: SkillScope::Global,
            skill_id: sid(404),
            content: "x".to_owned(),
            project_root: root(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND");
}

#[tokio::test]
async fn list_is_scope_filtered() {
    let store = FakeSkills::default();
    store
        .save(
            &Skill::new(sid(1), "g", MarkdownDoc::new("x"), SkillScope::Global).unwrap(),
            &root(),
        )
        .await
        .unwrap();
    store
        .save(
            &Skill::new(sid(2), "p", MarkdownDoc::new("x"), SkillScope::Project).unwrap(),
            &root(),
        )
        .await
        .unwrap();

    let listed = ListSkills::new(Arc::new(store))
        .execute(ListSkillsInput {
            scope: SkillScope::Global,
            project_root: root(),
        })
        .await
        .unwrap();
    assert_eq!(listed.skills.len(), 1);
    assert_eq!(listed.skills[0].id, sid(1));
}

#[tokio::test]
async fn delete_then_delete_is_not_found() {
    let store = FakeSkills::default();
    store
        .save(
            &Skill::new(sid(1), "k", MarkdownDoc::new("x"), SkillScope::Project).unwrap(),
            &root(),
        )
        .await
        .unwrap();
    let uc = DeleteSkill::new(Arc::new(store));
    uc.execute(DeleteSkillInput {
        scope: SkillScope::Project,
        skill_id: sid(1),
        project_root: root(),
    })
    .await
    .unwrap();
    let err = uc
        .execute(DeleteSkillInput {
            scope: SkillScope::Project,
            skill_id: sid(1),
            project_root: root(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND");
}

// ---------------------------------------------------------------------------
// Assignment
// ---------------------------------------------------------------------------

fn assigned_event(events: &[DomainEvent]) -> Vec<(SkillId, bool)> {
    events
        .iter()
        .filter_map(|e| match e {
            DomainEvent::SkillAssigned {
                skill_id, assigned, ..
            } => Some((*skill_id, *assigned)),
            _ => None,
        })
        .collect()
}

#[tokio::test]
async fn assign_mutates_manifest_emits_event_and_is_idempotent() {
    let contexts = FakeContexts::new(vec![scratch_entry(aid(1))]);
    let bus = SpyBus::default();
    let uc = AssignSkillToAgent::new(Arc::new(contexts.clone()), Arc::new(bus.clone()));
    let input = AssignSkillToAgentInput {
        project: project(),
        agent_id: aid(1),
        skill: SkillRef::new(sid(9), SkillScope::Global),
    };

    uc.execute(input.clone()).await.unwrap();
    // Re-assigning the same skill is a no-op (dedup).
    uc.execute(input).await.unwrap();

    let entry = &contexts.manifest().entries[0];
    assert_eq!(entry.skills.len(), 1, "no duplicate assignment");
    assert_eq!(entry.skills[0].skill_id, sid(9));
    assert_eq!(
        assigned_event(&bus.events()),
        vec![(sid(9), true)],
        "exactly one assign event despite double execute"
    );
}

#[tokio::test]
async fn unassign_removes_and_emits_false_then_is_noop() {
    let mut entry = scratch_entry(aid(1));
    entry.skills.push(SkillRef::new(sid(9), SkillScope::Global));
    let contexts = FakeContexts::new(vec![entry]);
    let bus = SpyBus::default();
    let uc = UnassignSkillFromAgent::new(Arc::new(contexts.clone()), Arc::new(bus.clone()));
    let input = UnassignSkillFromAgentInput {
        project: project(),
        agent_id: aid(1),
        skill_id: sid(9),
    };

    uc.execute(input.clone()).await.unwrap();
    uc.execute(input).await.unwrap(); // already gone → no-op

    assert!(contexts.manifest().entries[0].skills.is_empty());
    assert_eq!(assigned_event(&bus.events()), vec![(sid(9), false)]);
}

#[tokio::test]
async fn assign_to_unknown_agent_is_not_found() {
    let contexts = FakeContexts::new(vec![]);
    let bus = SpyBus::default();
    let err = AssignSkillToAgent::new(Arc::new(contexts), Arc::new(bus))
        .execute(AssignSkillToAgentInput {
            project: project(),
            agent_id: aid(404),
            skill: SkillRef::new(sid(9), SkillScope::Global),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND");
}
