//! Tests for the Lot E1 auto-memory harvest use case (`HarvestMemoryFromTurn`).
//!
//! Best-effort, port-faked: a `Response` turn carrying valid ` ```idea-memory `
//! blocks is persisted + announced; non-`Response` turns are no-ops; invalid blocks
//! are skipped individually; a store failure is reported, never propagated.

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

use async_trait::async_trait;
use domain::conversation_log::TurnRole;
use domain::events::DomainEvent;
use domain::ports::{EventBus, EventStream, MemoryError, MemoryStore};
use domain::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug, ProjectPath};

use application::HarvestMemoryFromTurn;

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

/// In-memory store keyed by slug, with an optional forced `save` failure.
#[derive(Default)]
struct FakeMemories {
    saved: Mutex<Vec<Memory>>,
    fail_save: bool,
}

impl FakeMemories {
    fn failing() -> Self {
        Self {
            saved: Mutex::new(Vec::new()),
            fail_save: true,
        }
    }
    fn slugs(&self) -> Vec<String> {
        self.saved
            .lock()
            .unwrap()
            .iter()
            .map(|m| m.slug().as_str().to_owned())
            .collect()
    }
}

#[async_trait]
impl MemoryStore for FakeMemories {
    async fn list(&self, _root: &ProjectPath) -> Result<Vec<Memory>, MemoryError> {
        Ok(self.saved.lock().unwrap().clone())
    }
    async fn get(&self, _root: &ProjectPath, slug: &MemorySlug) -> Result<Memory, MemoryError> {
        self.saved
            .lock()
            .unwrap()
            .iter()
            .find(|m| m.slug() == slug)
            .cloned()
            .ok_or(MemoryError::NotFound)
    }
    async fn save(&self, _root: &ProjectPath, memory: &Memory) -> Result<(), MemoryError> {
        if self.fail_save {
            return Err(MemoryError::Frontmatter("forced failure".to_owned()));
        }
        self.saved.lock().unwrap().push(memory.clone());
        Ok(())
    }
    async fn delete(&self, _root: &ProjectPath, _slug: &MemorySlug) -> Result<(), MemoryError> {
        Ok(())
    }
    async fn read_index(&self, _root: &ProjectPath) -> Result<Vec<MemoryIndexEntry>, MemoryError> {
        Ok(Vec::new())
    }
    async fn resolve_links(
        &self,
        _root: &ProjectPath,
        _slug: &MemorySlug,
    ) -> Result<Vec<MemoryLink>, MemoryError> {
        Ok(Vec::new())
    }
}

#[derive(Default, Clone)]
struct SpyBus(Arc<Mutex<Vec<DomainEvent>>>);
impl SpyBus {
    fn saved_slugs(&self) -> Vec<String> {
        self.0
            .lock()
            .unwrap()
            .iter()
            .filter_map(|e| match e {
                DomainEvent::MemorySaved { slug } => Some(slug.as_str().to_owned()),
                _ => None,
            })
            .collect()
    }
}
impl EventBus for SpyBus {
    fn publish(&self, event: DomainEvent) {
        self.0.lock().unwrap().push(event);
    }
    fn subscribe(&self) -> EventStream {
        Box::new(std::iter::empty())
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

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

fn block(slug: &str, body: &str) -> String {
    format!(
        "```idea-memory\nslug: {slug}\ntitle: Title\ntype: project\ndescription: hook\n---\n{body}\n```"
    )
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[tokio::test]
async fn ignores_non_response_turns() {
    let store = Arc::new(FakeMemories::default());
    let bus = Arc::new(SpyBus::default());
    let harvester = HarvestMemoryFromTurn::new(
        Arc::clone(&store) as Arc<dyn MemoryStore>,
        Arc::clone(&bus) as Arc<dyn EventBus>,
    );
    let text = block("note-a", "Body");

    for role in [TurnRole::Prompt, TurnRole::ToolActivity] {
        let outcome = harvester.harvest(&root(), role, &text).await;
        assert_eq!(outcome.found, 0, "non-response turns are not parsed");
        assert!(outcome.saved.is_empty());
    }
    assert!(store.slugs().is_empty(), "nothing persisted");
    assert!(bus.saved_slugs().is_empty(), "nothing announced");
}

#[tokio::test]
async fn response_with_valid_block_saves_and_announces() {
    let store = Arc::new(FakeMemories::default());
    let bus = Arc::new(SpyBus::default());
    let harvester = HarvestMemoryFromTurn::new(
        Arc::clone(&store) as Arc<dyn MemoryStore>,
        Arc::clone(&bus) as Arc<dyn EventBus>,
    );
    let text = format!(
        "Sure, noting that.\n\n{}",
        block("api-url", "Use the staging URL.")
    );

    let outcome = harvester.harvest(&root(), TurnRole::Response, &text).await;

    assert_eq!(outcome.found, 1);
    assert_eq!(outcome.saved.len(), 1);
    assert_eq!(outcome.saved[0].as_str(), "api-url");
    assert_eq!(outcome.skipped, 0);
    assert!(outcome.errors.is_empty());
    assert_eq!(store.slugs(), vec!["api-url"], "persisted via the store");
    assert_eq!(bus.saved_slugs(), vec!["api-url"], "MemorySaved announced");
}

#[tokio::test]
async fn partial_invalid_blocks_are_skipped_individually() {
    let store = Arc::new(FakeMemories::default());
    let bus = Arc::new(SpyBus::default());
    let harvester = HarvestMemoryFromTurn::new(
        Arc::clone(&store) as Arc<dyn MemoryStore>,
        Arc::clone(&bus) as Arc<dyn EventBus>,
    );
    // One valid, one with an invalid slug.
    let text = format!(
        "{}\n{}",
        block("good-one", "Body"),
        block("Bad Slug", "Body")
    );

    let outcome = harvester.harvest(&root(), TurnRole::Response, &text).await;

    assert_eq!(outcome.found, 2);
    assert_eq!(outcome.saved.len(), 1, "only the valid one persisted");
    assert_eq!(outcome.saved[0].as_str(), "good-one");
    assert_eq!(outcome.skipped, 1);
    assert_eq!(outcome.errors.len(), 1);
    assert_eq!(store.slugs(), vec!["good-one"]);
    assert_eq!(bus.saved_slugs(), vec!["good-one"]);
}

#[tokio::test]
async fn store_failure_is_reported_not_propagated() {
    let store = Arc::new(FakeMemories::failing());
    let bus = Arc::new(SpyBus::default());
    let harvester = HarvestMemoryFromTurn::new(
        Arc::clone(&store) as Arc<dyn MemoryStore>,
        Arc::clone(&bus) as Arc<dyn EventBus>,
    );
    let text = block("doomed", "Body");

    // No panic, no Result — the call simply returns an outcome carrying the error.
    let outcome = harvester.harvest(&root(), TurnRole::Response, &text).await;

    assert_eq!(outcome.found, 1);
    assert!(outcome.saved.is_empty(), "save failed ⇒ nothing saved");
    assert_eq!(outcome.skipped, 1);
    assert_eq!(outcome.errors.len(), 1);
    assert!(
        outcome.errors[0].contains("store failed"),
        "{:?}",
        outcome.errors
    );
    // A failed save must not announce a MemorySaved event.
    assert!(bus.saved_slugs().is_empty());
}

#[tokio::test]
async fn parses_only_current_text_no_full_log() {
    // The use case takes only the turn's `text`; it has no `ConversationLog`
    // dependency, so it structurally cannot read the full thread. We assert it
    // acts purely on the text passed in: a response with no directive saves
    // nothing even though the store already holds (unrelated) notes.
    let store = Arc::new(FakeMemories::default());
    let bus = Arc::new(SpyBus::default());
    let harvester = HarvestMemoryFromTurn::new(
        Arc::clone(&store) as Arc<dyn MemoryStore>,
        Arc::clone(&bus) as Arc<dyn EventBus>,
    );

    let outcome = harvester
        .harvest(
            &root(),
            TurnRole::Response,
            "A plain reply with no directive.",
        )
        .await;

    assert_eq!(outcome.found, 0);
    assert!(outcome.saved.is_empty());
    assert!(store.slugs().is_empty());
}
