//! L12 tests for the memory use cases (ARCHITECTURE §14.5.1) with in-memory
//! port fakes (no real store/FS): CRUD (`CreateMemory`, `UpdateMemory`,
//! `GetMemory`, `ListMemories`, `DeleteMemory`) asserting the `MemorySaved` /
//! `MemoryDeleted` events, and the read-only navigation helpers
//! (`ReadMemoryIndex`, `ResolveMemoryLinks`) asserting they emit nothing.

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

use async_trait::async_trait;
use domain::events::DomainEvent;
use domain::ports::{EventBus, EventStream, MemoryError, MemoryQuery, MemoryRecall, MemoryStore};
use domain::{
    MarkdownDoc, Memory, MemoryFrontmatter, MemoryIndexEntry, MemoryLink, MemorySlug, MemoryType,
    ProjectPath,
};

use application::{
    CreateMemory, CreateMemoryInput, DeleteMemory, DeleteMemoryInput, GetMemory, GetMemoryInput,
    ListMemories, ListMemoriesInput, ReadMemoryIndex, ReadMemoryIndexInput, RecallMemory,
    RecallMemoryInput, ResolveMemoryLinks, ResolveMemoryLinksInput, UpdateMemory,
    UpdateMemoryInput,
};

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

/// In-memory memory store keyed by slug. Index and links are derived from the
/// stored notes (mirroring the real adapter), so navigation reads stay honest.
#[derive(Clone, Default)]
struct FakeMemories(Arc<Mutex<Vec<Memory>>>);

#[async_trait]
impl MemoryStore for FakeMemories {
    async fn list(&self, _root: &ProjectPath) -> Result<Vec<Memory>, MemoryError> {
        Ok(self.0.lock().unwrap().clone())
    }

    async fn get(&self, _root: &ProjectPath, slug: &MemorySlug) -> Result<Memory, MemoryError> {
        self.0
            .lock()
            .unwrap()
            .iter()
            .find(|m| m.slug() == slug)
            .cloned()
            .ok_or(MemoryError::NotFound)
    }

    async fn save(&self, _root: &ProjectPath, memory: &Memory) -> Result<(), MemoryError> {
        let mut v = self.0.lock().unwrap();
        if let Some(slot) = v.iter_mut().find(|m| m.slug() == memory.slug()) {
            *slot = memory.clone();
        } else {
            v.push(memory.clone());
        }
        Ok(())
    }

    async fn delete(&self, _root: &ProjectPath, slug: &MemorySlug) -> Result<(), MemoryError> {
        let mut v = self.0.lock().unwrap();
        let before = v.len();
        v.retain(|m| m.slug() != slug);
        if v.len() == before {
            return Err(MemoryError::NotFound);
        }
        Ok(())
    }

    async fn read_index(&self, _root: &ProjectPath) -> Result<Vec<MemoryIndexEntry>, MemoryError> {
        Ok(self
            .0
            .lock()
            .unwrap()
            .iter()
            .map(Memory::index_entry)
            .collect())
    }

    async fn resolve_links(
        &self,
        _root: &ProjectPath,
        slug: &MemorySlug,
    ) -> Result<Vec<MemoryLink>, MemoryError> {
        let v = self.0.lock().unwrap();
        let source = v
            .iter()
            .find(|m| m.slug() == slug)
            .ok_or(MemoryError::NotFound)?;
        // Mirror the adapter: drop links whose target note does not exist.
        Ok(source
            .outgoing_links()
            .into_iter()
            .filter(|l| v.iter().any(|m| m.slug() == &l.target))
            .collect())
    }
}

#[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())
    }
}

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

fn root() -> ProjectPath {
    ProjectPath::new("/home/me/demo").unwrap()
}

fn slug(s: &str) -> MemorySlug {
    MemorySlug::new(s).unwrap()
}

/// Builds and persists a note directly through the store (bypassing use cases).
async fn seed(store: &FakeMemories, name: &str, body: &str) {
    let memory = Memory::new(
        MemoryFrontmatter {
            name: slug(name),
            description: format!("{name} hook"),
            r#type: MemoryType::Project,
        },
        MarkdownDoc::new(body),
    )
    .unwrap();
    store.save(&root(), &memory).await.unwrap();
}

fn saved_slugs(events: &[DomainEvent]) -> Vec<MemorySlug> {
    events
        .iter()
        .filter_map(|e| match e {
            DomainEvent::MemorySaved { slug } => Some(slug.clone()),
            _ => None,
        })
        .collect()
}

fn deleted_slugs(events: &[DomainEvent]) -> Vec<MemorySlug> {
    events
        .iter()
        .filter_map(|e| match e {
            DomainEvent::MemoryDeleted { slug } => Some(slug.clone()),
            _ => None,
        })
        .collect()
}

// ---------------------------------------------------------------------------
// CreateMemory
// ---------------------------------------------------------------------------

#[tokio::test]
async fn create_persists_note_and_emits_memory_saved_with_slug() {
    let store = FakeMemories::default();
    let bus = SpyBus::default();
    let out = CreateMemory::new(Arc::new(store.clone()), Arc::new(bus.clone()))
        .execute(CreateMemoryInput {
            project_root: root(),
            name: "design-decision".to_owned(),
            description: "why we chose hexagonal".to_owned(),
            r#type: MemoryType::Project,
            content: "# body".to_owned(),
        })
        .await
        .unwrap();

    assert_eq!(out.memory.slug(), &slug("design-decision"));
    // Persisted.
    let listed = store.list(&root()).await.unwrap();
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].slug(), &slug("design-decision"));
    // Emits exactly one MemorySaved with the right slug.
    assert_eq!(saved_slugs(&bus.events()), vec![slug("design-decision")]);
}

#[tokio::test]
async fn create_rejects_non_kebab_name_as_invalid() {
    let store = FakeMemories::default();
    let bus = SpyBus::default();
    let err = CreateMemory::new(Arc::new(store.clone()), Arc::new(bus.clone()))
        .execute(CreateMemoryInput {
            project_root: root(),
            name: "Not Kebab".to_owned(),
            description: "x".to_owned(),
            r#type: MemoryType::User,
            content: "# body".to_owned(),
        })
        .await
        .unwrap_err();

    assert_eq!(err.code(), "INVALID");
    // Nothing persisted, nothing announced.
    assert!(store.list(&root()).await.unwrap().is_empty());
    assert!(bus.events().is_empty());
}

#[tokio::test]
async fn create_rejects_empty_description() {
    let store = FakeMemories::default();
    let bus = SpyBus::default();
    let err = CreateMemory::new(Arc::new(store), Arc::new(bus))
        .execute(CreateMemoryInput {
            project_root: root(),
            name: "ok-slug".to_owned(),
            description: String::new(),
            r#type: MemoryType::User,
            content: "# body".to_owned(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "INVALID");
}

#[tokio::test]
async fn create_rejects_empty_content() {
    let store = FakeMemories::default();
    let bus = SpyBus::default();
    let err = CreateMemory::new(Arc::new(store), Arc::new(bus))
        .execute(CreateMemoryInput {
            project_root: root(),
            name: "ok-slug".to_owned(),
            description: "a hook".to_owned(),
            r#type: MemoryType::User,
            content: String::new(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "INVALID");
}

// ---------------------------------------------------------------------------
// UpdateMemory
// ---------------------------------------------------------------------------

#[tokio::test]
async fn update_replaces_content_and_emits_memory_saved() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "v1").await;
    let bus = SpyBus::default();

    let out = UpdateMemory::new(Arc::new(store.clone()), Arc::new(bus.clone()))
        .execute(UpdateMemoryInput {
            project_root: root(),
            slug: slug("note-a"),
            description: "updated hook".to_owned(),
            r#type: MemoryType::Reference,
            content: "v2".to_owned(),
        })
        .await
        .unwrap();

    assert_eq!(out.memory.body.as_str(), "v2");
    assert_eq!(out.memory.frontmatter.description, "updated hook");
    assert_eq!(out.memory.frontmatter.r#type, MemoryType::Reference);
    // Replace semantics: still a single note.
    let listed = store.list(&root()).await.unwrap();
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].body.as_str(), "v2");
    assert_eq!(saved_slugs(&bus.events()), vec![slug("note-a")]);
}

#[tokio::test]
async fn update_revalidates_invariants_and_rejects_empty_content() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "v1").await;
    let bus = SpyBus::default();

    let err = UpdateMemory::new(Arc::new(store.clone()), Arc::new(bus.clone()))
        .execute(UpdateMemoryInput {
            project_root: root(),
            slug: slug("note-a"),
            description: "still here".to_owned(),
            r#type: MemoryType::Project,
            content: String::new(),
        })
        .await
        .unwrap_err();

    assert_eq!(err.code(), "INVALID");
    // Original note untouched, no event.
    assert_eq!(
        store
            .get(&root(), &slug("note-a"))
            .await
            .unwrap()
            .body
            .as_str(),
        "v1"
    );
    assert!(bus.events().is_empty());
}

// ---------------------------------------------------------------------------
// DeleteMemory
// ---------------------------------------------------------------------------

#[tokio::test]
async fn delete_removes_note_and_emits_memory_deleted() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "body").await;
    let bus = SpyBus::default();

    DeleteMemory::new(Arc::new(store.clone()), Arc::new(bus.clone()))
        .execute(DeleteMemoryInput {
            project_root: root(),
            slug: slug("note-a"),
        })
        .await
        .unwrap();

    assert!(store.list(&root()).await.unwrap().is_empty());
    assert_eq!(deleted_slugs(&bus.events()), vec![slug("note-a")]);
}

#[tokio::test]
async fn delete_unknown_slug_is_not_found_and_emits_nothing() {
    let store = FakeMemories::default();
    let bus = SpyBus::default();

    let err = DeleteMemory::new(Arc::new(store), Arc::new(bus.clone()))
        .execute(DeleteMemoryInput {
            project_root: root(),
            slug: slug("ghost"),
        })
        .await
        .unwrap_err();

    assert_eq!(err.code(), "NOT_FOUND");
    assert!(bus.events().is_empty());
}

// ---------------------------------------------------------------------------
// GetMemory
// ---------------------------------------------------------------------------

#[tokio::test]
async fn get_returns_the_note() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "hello").await;

    let out = GetMemory::new(Arc::new(store))
        .execute(GetMemoryInput {
            project_root: root(),
            slug: slug("note-a"),
        })
        .await
        .unwrap();

    assert_eq!(out.memory.slug(), &slug("note-a"));
    assert_eq!(out.memory.body.as_str(), "hello");
}

#[tokio::test]
async fn get_unknown_slug_is_not_found() {
    let store = FakeMemories::default();
    let err = GetMemory::new(Arc::new(store))
        .execute(GetMemoryInput {
            project_root: root(),
            slug: slug("ghost"),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND");
}

// ---------------------------------------------------------------------------
// ListMemories
// ---------------------------------------------------------------------------

#[tokio::test]
async fn list_returns_all_notes() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "a").await;
    seed(&store, "note-b", "b").await;

    let out = ListMemories::new(Arc::new(store))
        .execute(ListMemoriesInput {
            project_root: root(),
        })
        .await
        .unwrap();

    let mut slugs: Vec<_> = out.memories.iter().map(|m| m.slug().clone()).collect();
    slugs.sort();
    assert_eq!(slugs, vec![slug("note-a"), slug("note-b")]);
}

#[tokio::test]
async fn list_is_empty_when_no_notes() {
    let store = FakeMemories::default();
    let out = ListMemories::new(Arc::new(store))
        .execute(ListMemoriesInput {
            project_root: root(),
        })
        .await
        .unwrap();
    assert!(out.memories.is_empty());
}

// ---------------------------------------------------------------------------
// ReadMemoryIndex
// ---------------------------------------------------------------------------

#[tokio::test]
async fn read_index_returns_one_row_per_note() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "a").await;
    seed(&store, "note-b", "b").await;

    let out = ReadMemoryIndex::new(Arc::new(store))
        .execute(ReadMemoryIndexInput {
            project_root: root(),
        })
        .await
        .unwrap();

    assert_eq!(out.entries.len(), 2);
    let mut rows: Vec<_> = out
        .entries
        .iter()
        .map(|e| (e.slug.clone(), e.hook.clone()))
        .collect();
    rows.sort();
    assert_eq!(
        rows,
        vec![
            (slug("note-a"), "note-a hook".to_owned()),
            (slug("note-b"), "note-b hook".to_owned()),
        ]
    );
}

// ---------------------------------------------------------------------------
// ResolveMemoryLinks
// ---------------------------------------------------------------------------

#[tokio::test]
async fn resolve_links_returns_outgoing_links_dropping_broken_ones() {
    let store = FakeMemories::default();
    // note-a links to note-b (exists) and to ghost (does not).
    seed(&store, "note-a", "see [[note-b]] and [[ghost]]").await;
    seed(&store, "note-b", "target body").await;

    let out = ResolveMemoryLinks::new(Arc::new(store))
        .execute(ResolveMemoryLinksInput {
            project_root: root(),
            slug: slug("note-a"),
        })
        .await
        .unwrap();

    // Only the resolvable link survives; the broken one is dropped.
    assert_eq!(
        out.links,
        vec![MemoryLink {
            target: slug("note-b")
        }]
    );
}

#[tokio::test]
async fn resolve_links_for_unknown_source_is_not_found() {
    let store = FakeMemories::default();
    let err = ResolveMemoryLinks::new(Arc::new(store))
        .execute(ResolveMemoryLinksInput {
            project_root: root(),
            slug: slug("ghost"),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND");
}

// ---------------------------------------------------------------------------
// RecallMemory
// ---------------------------------------------------------------------------

/// In-memory [`MemoryRecall`] fake: records the queries it received and returns a
/// canned set of entries. Lets us assert the use case delegates faithfully (same
/// `root`, the input `text`/`token_budget` forwarded) and surfaces the result.
#[derive(Clone, Default)]
struct FakeRecall {
    entries: Arc<Vec<MemoryIndexEntry>>,
    seen: Arc<Mutex<Vec<MemoryQuery>>>,
}

impl FakeRecall {
    fn returning(entries: Vec<MemoryIndexEntry>) -> Self {
        Self {
            entries: Arc::new(entries),
            seen: Arc::default(),
        }
    }
    fn queries(&self) -> Vec<MemoryQuery> {
        self.seen.lock().unwrap().clone()
    }
}

#[async_trait]
impl MemoryRecall for FakeRecall {
    async fn recall(
        &self,
        _root: &ProjectPath,
        query: &MemoryQuery,
    ) -> Result<Vec<MemoryIndexEntry>, MemoryError> {
        self.seen.lock().unwrap().push(query.clone());
        Ok((*self.entries).clone())
    }
}

fn entry(slug_str: &str, hook: &str) -> MemoryIndexEntry {
    MemoryIndexEntry {
        slug: slug(slug_str),
        title: slug_str.to_owned(),
        hook: hook.to_owned(),
        r#type: MemoryType::Reference,
    }
}

#[tokio::test]
async fn recall_delegates_to_port_and_returns_entries() {
    let recalled = vec![entry("alpha", "first"), entry("beta", "second")];
    let recall = FakeRecall::returning(recalled.clone());

    let out = RecallMemory::new(Arc::new(recall.clone()))
        .execute(RecallMemoryInput {
            project_root: root(),
            text: "current context".to_owned(),
            token_budget: 42,
        })
        .await
        .unwrap();

    // Returns exactly what the port produced, in order.
    assert_eq!(out.entries, recalled);
    // The query was forwarded verbatim (text + budget).
    assert_eq!(
        recall.queries(),
        vec![MemoryQuery {
            text: "current context".to_owned(),
            token_budget: 42,
        }]
    );
}

#[tokio::test]
async fn recall_empty_result_is_empty_not_error() {
    let recall = FakeRecall::returning(Vec::new());
    let out = RecallMemory::new(Arc::new(recall))
        .execute(RecallMemoryInput {
            project_root: root(),
            text: String::new(),
            token_budget: 0,
        })
        .await
        .unwrap();
    assert!(out.entries.is_empty());
}

#[tokio::test]
async fn recall_publishes_no_events() {
    // RecallMemory takes no EventBus; assert a shared spy stays empty when other
    // (mutating) use cases on the same bus are NOT exercised — i.e. recall is
    // read-only by construction.
    let recall = FakeRecall::returning(vec![entry("alpha", "first")]);
    let bus = SpyBus::default();

    RecallMemory::new(Arc::new(recall))
        .execute(RecallMemoryInput {
            project_root: root(),
            text: "ctx".to_owned(),
            token_budget: 10,
        })
        .await
        .unwrap();

    assert!(
        bus.events().is_empty(),
        "recall must not publish any DomainEvent"
    );
}

// ---------------------------------------------------------------------------
// Read use cases emit no events
// ---------------------------------------------------------------------------

#[tokio::test]
async fn read_use_cases_publish_no_events() {
    let store = FakeMemories::default();
    seed(&store, "note-a", "see [[note-a]]").await;
    let bus = SpyBus::default();

    // None of the read use cases take an EventBus — assert the shared spy stays
    // empty after exercising every read path on the same store.
    GetMemory::new(Arc::new(store.clone()))
        .execute(GetMemoryInput {
            project_root: root(),
            slug: slug("note-a"),
        })
        .await
        .unwrap();
    ListMemories::new(Arc::new(store.clone()))
        .execute(ListMemoriesInput {
            project_root: root(),
        })
        .await
        .unwrap();
    ReadMemoryIndex::new(Arc::new(store.clone()))
        .execute(ReadMemoryIndexInput {
            project_root: root(),
        })
        .await
        .unwrap();
    ResolveMemoryLinks::new(Arc::new(store))
        .execute(ResolveMemoryLinksInput {
            project_root: root(),
            slug: slug("note-a"),
        })
        .await
        .unwrap();

    assert!(
        bus.events().is_empty(),
        "read use cases must not publish any DomainEvent"
    );
}
