//! L7 tests for the template & synchronisation use cases, with in-memory port
//! fakes (no real store/FS): `CreateTemplate`, `UpdateTemplate`,
//! `CreateAgentFromTemplate`, `DetectAgentDrift`, `SyncAgentWithTemplate`.

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

use async_trait::async_trait;
use domain::events::DomainEvent;
use domain::ids::{AgentId, ProfileId, ProjectId, TemplateId};
use domain::markdown::MarkdownDoc;
use domain::ports::{
    AgentContextStore, EventBus, EventStream, IdGenerator, StoreError, TemplateStore,
};
use domain::template::{AgentTemplate, TemplateVersion};
use domain::{AgentManifest, ManifestEntry, Project, ProjectPath, RemoteRef};
use uuid::Uuid;

use application::{
    CreateAgentFromTemplate, CreateAgentFromTemplateInput, CreateTemplate, CreateTemplateInput,
    DetectAgentDrift, DetectAgentDriftInput, ReadTemplate, ReadTemplateInput,
    SyncAgentWithTemplate, SyncAgentWithTemplateInput, UpdateTemplate, UpdateTemplateInput,
};

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

#[derive(Clone, Default)]
struct FakeTemplates(Arc<Mutex<Vec<AgentTemplate>>>);
impl FakeTemplates {
    fn with(templates: Vec<AgentTemplate>) -> Self {
        Self(Arc::new(Mutex::new(templates)))
    }
    fn get_sync(&self, id: TemplateId) -> Option<AgentTemplate> {
        self.0.lock().unwrap().iter().find(|t| t.id == id).cloned()
    }
}
#[async_trait]
impl TemplateStore for FakeTemplates {
    async fn list(&self) -> Result<Vec<AgentTemplate>, StoreError> {
        Ok(self.0.lock().unwrap().clone())
    }
    async fn get(&self, id: TemplateId) -> Result<AgentTemplate, StoreError> {
        self.get_sync(id).ok_or(StoreError::NotFound)
    }
    async fn save(&self, template: &AgentTemplate) -> Result<(), StoreError> {
        let mut v = self.0.lock().unwrap();
        if let Some(slot) = v.iter_mut().find(|t| t.id == template.id) {
            *slot = template.clone();
        } else {
            v.push(template.clone());
        }
        Ok(())
    }
    async fn delete(&self, id: TemplateId) -> Result<(), StoreError> {
        let mut v = self.0.lock().unwrap();
        let before = v.len();
        v.retain(|t| t.id != id);
        if v.len() == before {
            return Err(StoreError::NotFound);
        }
        Ok(())
    }
}

#[derive(Clone)]
struct FakeContexts(Arc<Mutex<(AgentManifest, HashMap<String, String>)>>);
impl FakeContexts {
    fn new(entries: Vec<ManifestEntry>) -> Self {
        Self(Arc::new(Mutex::new((
            AgentManifest {
                version: 1,
                entries,
                orchestrator: None,
            },
            HashMap::new(),
        ))))
    }
    fn manifest(&self) -> AgentManifest {
        self.0.lock().unwrap().0.clone()
    }
    fn content(&self, md_path: &str) -> Option<String> {
        self.0.lock().unwrap().1.get(md_path).cloned()
    }
    fn md_path_of(&self, agent: &AgentId) -> Option<String> {
        self.0
            .lock()
            .unwrap()
            .0
            .entries
            .iter()
            .find(|e| &e.agent_id == agent)
            .map(|e| e.md_path.clone())
    }
}
#[async_trait]
impl AgentContextStore for FakeContexts {
    async fn read_context(&self, _p: &Project, agent: &AgentId) -> Result<MarkdownDoc, StoreError> {
        let md = self.md_path_of(agent).ok_or(StoreError::NotFound)?;
        self.content(&md)
            .map(MarkdownDoc::new)
            .ok_or(StoreError::NotFound)
    }
    async fn write_context(
        &self,
        _p: &Project,
        agent: &AgentId,
        md: &MarkdownDoc,
    ) -> Result<(), StoreError> {
        let path = self.md_path_of(agent).ok_or(StoreError::NotFound)?;
        self.0
            .lock()
            .unwrap()
            .1
            .insert(path, md.as_str().to_owned());
        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().0 = 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 tid(n: u128) -> TemplateId {
    TemplateId::from_uuid(Uuid::from_u128(n))
}
fn aid(n: u128) -> AgentId {
    AgentId::from_uuid(Uuid::from_u128(n))
}
fn v(n: u64) -> TemplateVersion {
    TemplateVersion(n)
}
fn project() -> Project {
    Project::new(
        ProjectId::from_uuid(Uuid::from_u128(1000)),
        "demo",
        ProjectPath::new("/home/me/demo").unwrap(),
        RemoteRef::local(),
        1_700_000_000_000,
    )
    .unwrap()
}
fn template(id: TemplateId, name: &str, content: &str, version: u64) -> AgentTemplate {
    let mut t = AgentTemplate::new(id, name, MarkdownDoc::new(content), pid(1)).unwrap();
    // Bump to the requested version by re-applying content updates.
    while t.version.get() < version {
        t = t.with_updated_content(MarkdownDoc::new(content));
    }
    t
}
/// A synchronized, template-backed manifest entry synced at `synced`.
fn synced_entry(agent: AgentId, md: &str, template: TemplateId, synced: u64) -> ManifestEntry {
    ManifestEntry::new(
        agent,
        "A",
        md,
        pid(1),
        Some(template),
        true,
        Some(v(synced)),
    )
    .unwrap()
}

// ---------------------------------------------------------------------------
// CreateTemplate / UpdateTemplate
// ---------------------------------------------------------------------------

#[tokio::test]
async fn create_template_starts_at_initial_version() {
    let store = FakeTemplates::default();
    let out = CreateTemplate::new(Arc::new(store.clone()), Arc::new(SeqIds::new()))
        .execute(CreateTemplateInput {
            name: "Backend".to_owned(),
            content: "# ctx".to_owned(),
            default_profile_id: pid(7),
        })
        .await
        .unwrap();
    assert_eq!(out.template.version, TemplateVersion::INITIAL);
    assert_eq!(out.template.default_profile_id, pid(7));
    assert_eq!(store.list().await.unwrap().len(), 1);
}

#[tokio::test]
async fn read_template_returns_one_template_or_not_found() {
    let existing = template(tid(7), "Base", "body", 3);
    let store = FakeTemplates::with(vec![existing.clone()]);
    let read = ReadTemplate::new(Arc::new(store));

    let out = read
        .execute(ReadTemplateInput {
            template_id: existing.id,
        })
        .await
        .unwrap();
    assert_eq!(out.template, existing);

    let err = read
        .execute(ReadTemplateInput {
            template_id: tid(8),
        })
        .await
        .expect_err("unknown template should be not found");
    assert_eq!(err.code(), "NOT_FOUND");
}

#[tokio::test]
async fn update_template_bumps_version_and_publishes_event() {
    let store = FakeTemplates::with(vec![template(tid(1), "T", "v1", 1)]);
    let bus = SpyBus::default();
    let out = UpdateTemplate::new(Arc::new(store.clone()), Arc::new(bus.clone()))
        .execute(UpdateTemplateInput {
            template_id: tid(1),
            content: "v2".to_owned(),
        })
        .await
        .unwrap();

    assert_eq!(out.template.version.get(), 2);
    assert_eq!(store.get_sync(tid(1)).unwrap().content_md.as_str(), "v2");
    assert_eq!(
        bus.events(),
        vec![DomainEvent::TemplateUpdated {
            template_id: tid(1),
            version: v(2),
        }]
    );
}

#[tokio::test]
async fn update_unknown_template_is_not_found() {
    let err = UpdateTemplate::new(
        Arc::new(FakeTemplates::default()),
        Arc::new(SpyBus::default()),
    )
    .execute(UpdateTemplateInput {
        template_id: tid(404),
        content: "x".to_owned(),
    })
    .await
    .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
}

// ---------------------------------------------------------------------------
// CreateAgentFromTemplate
// ---------------------------------------------------------------------------

#[tokio::test]
async fn create_agent_from_template_links_origin_and_seeds_context() {
    let store = FakeTemplates::with(vec![template(tid(1), "Backend", "# body", 4)]);
    let contexts = FakeContexts::new(vec![]);
    let out = CreateAgentFromTemplate::new(
        Arc::new(store),
        Arc::new(contexts.clone()),
        Arc::new(SeqIds::new()),
        Arc::new(SpyBus::default()),
    )
    .execute(CreateAgentFromTemplateInput {
        project: project(),
        template_id: tid(1),
        name: None,
        synchronized: true,
    })
    .await
    .unwrap();

    // Name defaults to the template name; profile = template default.
    assert_eq!(out.agent.name, "Backend");
    assert_eq!(out.agent.profile_id, pid(1));
    assert!(out.agent.synchronized);
    assert_eq!(
        out.agent.origin,
        domain::AgentOrigin::FromTemplate {
            template_id: tid(1),
            synced_template_version: v(4),
        }
    );
    // Context seeded with the template content under the agent's md path.
    assert_eq!(
        contexts.content(&out.agent.context_path).as_deref(),
        Some("# body")
    );
    assert_eq!(contexts.manifest().entries.len(), 1);
}

// ---------------------------------------------------------------------------
// DetectAgentDrift
// ---------------------------------------------------------------------------

#[tokio::test]
async fn detect_drift_flags_only_synchronized_agents_behind() {
    // Template at v3.
    let store = FakeTemplates::with(vec![template(tid(1), "T", "v3", 3)]);
    // a1: synchronized, synced at v1 → drift (1→3).
    // a2: synchronized, synced at v3 → up to date, no drift.
    // a3: from template but NOT synchronized → ignored.
    // a4: scratch (no template) → ignored.
    let a3 = ManifestEntry::new(
        aid(3),
        "A3",
        "agents/a3.md",
        pid(1),
        Some(tid(1)),
        false,
        Some(v(1)),
    )
    .unwrap();
    let a4 = ManifestEntry::new(aid(4), "A4", "agents/a4.md", pid(1), None, false, None).unwrap();
    let contexts = FakeContexts::new(vec![
        synced_entry(aid(1), "agents/a1.md", tid(1), 1),
        synced_entry(aid(2), "agents/a2.md", tid(1), 3),
        a3,
        a4,
    ]);
    let bus = SpyBus::default();

    let out = DetectAgentDrift::new(Arc::new(store), Arc::new(contexts), Arc::new(bus.clone()))
        .execute(DetectAgentDriftInput { project: project() })
        .await
        .unwrap();

    assert_eq!(out.drifts.len(), 1, "only a1 drifts");
    assert_eq!(out.drifts[0].agent_id, aid(1));
    assert_eq!(out.drifts[0].from, v(1));
    assert_eq!(out.drifts[0].to, v(3));
    assert_eq!(
        bus.events(),
        vec![DomainEvent::AgentDriftDetected {
            agent_id: aid(1),
            from: v(1),
            to: v(3),
        }]
    );
}

#[tokio::test]
async fn detect_drift_ignores_deleted_template() {
    // No templates in the store, but an agent references tid(1): not an error.
    let store = FakeTemplates::default();
    let contexts = FakeContexts::new(vec![synced_entry(aid(1), "agents/a1.md", tid(1), 1)]);
    let out = DetectAgentDrift::new(
        Arc::new(store),
        Arc::new(contexts),
        Arc::new(SpyBus::default()),
    )
    .execute(DetectAgentDriftInput { project: project() })
    .await
    .unwrap();
    assert!(out.drifts.is_empty());
}

// ---------------------------------------------------------------------------
// SyncAgentWithTemplate
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sync_applies_to_synchronized_and_updates_version_and_context() {
    let store = FakeTemplates::with(vec![template(tid(1), "T", "newest body", 3)]);
    let contexts = FakeContexts::new(vec![synced_entry(aid(1), "agents/a1.md", tid(1), 1)]);
    // Seed an old context so we can see the replacement.
    contexts
        .write_context(&project(), &aid(1), &MarkdownDoc::new("old"))
        .await
        .unwrap();
    let bus = SpyBus::default();

    let out = SyncAgentWithTemplate::new(
        Arc::new(store),
        Arc::new(contexts.clone()),
        Arc::new(bus.clone()),
    )
    .execute(SyncAgentWithTemplateInput {
        project: project(),
        agent_id: aid(1),
    })
    .await
    .unwrap();

    assert!(out.synced);
    assert_eq!(out.version, Some(v(3)));
    // Context replaced by the template content.
    assert_eq!(
        contexts.content("agents/a1.md").as_deref(),
        Some("newest body")
    );
    // Manifest synced version advanced to 3.
    let entry = &contexts.manifest().entries[0];
    assert_eq!(entry.synced_template_version, Some(v(3)));
    assert_eq!(
        bus.events(),
        vec![DomainEvent::AgentSynced {
            agent_id: aid(1),
            to: v(3),
        }]
    );
}

#[tokio::test]
async fn sync_ignores_non_synchronized_agent() {
    let store = FakeTemplates::with(vec![template(tid(1), "T", "body", 3)]);
    // Non-synchronized agent from a template.
    let entry = ManifestEntry::new(
        aid(1),
        "A",
        "agents/a1.md",
        pid(1),
        Some(tid(1)),
        false,
        Some(v(1)),
    )
    .unwrap();
    let contexts = FakeContexts::new(vec![entry]);
    contexts
        .write_context(&project(), &aid(1), &MarkdownDoc::new("keep me"))
        .await
        .unwrap();
    let bus = SpyBus::default();

    let out = SyncAgentWithTemplate::new(
        Arc::new(store),
        Arc::new(contexts.clone()),
        Arc::new(bus.clone()),
    )
    .execute(SyncAgentWithTemplateInput {
        project: project(),
        agent_id: aid(1),
    })
    .await
    .unwrap();

    assert!(!out.synced, "non-synchronized agent is left untouched");
    assert_eq!(out.version, None);
    assert_eq!(contexts.content("agents/a1.md").as_deref(), Some("keep me"));
    assert!(
        bus.events().is_empty(),
        "no sync event for an ignored agent"
    );
}
