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

use async_trait::async_trait;
use domain::ports::{
    AgentContextStore, Clock, EventBus, EventStream, IdGenerator, IssueNumberAllocator, IssueStore,
    IssueStoreError, StoreError,
};
use domain::{
    AgentId, AgentManifest, DomainEvent, Issue, IssueActor, IssueAttachmentContent,
    IssueAttachmentId, IssueCarnet, IssueIndexEntry, IssueListFilter, IssueNumber, IssuePriority,
    IssueRef, IssueStatus, IssueVersion, LocalPath, ManifestEntry, MarkdownDoc, ProfileId, Project,
    ProjectId, ProjectPath, RemoteRef,
};
use uuid::Uuid;

use application::{
    BulkDeleteIssues, BulkDeleteIssuesInput, BulkUpdateIssuePriority, BulkUpdateIssuePriorityInput,
    BulkUpdateIssueStatus, BulkUpdateIssueStatusInput, CreateIssue, CreateIssueInput, DeleteIssue,
    DeleteIssueInput, UpdateIssue, UpdateIssueInput,
};

#[derive(Default)]
struct FakeIssues {
    issues: Mutex<HashMap<IssueRef, Issue>>,
    conflict: Mutex<bool>,
}

#[async_trait]
impl IssueStore for FakeIssues {
    async fn create(&self, _root: &ProjectPath, issue: &Issue) -> Result<(), IssueStoreError> {
        self.issues
            .lock()
            .unwrap()
            .insert(issue.reference(), issue.clone());
        Ok(())
    }

    async fn get_by_ref(
        &self,
        _root: &ProjectPath,
        issue_ref: IssueRef,
    ) -> Result<Issue, IssueStoreError> {
        self.issues
            .lock()
            .unwrap()
            .get(&issue_ref)
            .cloned()
            .ok_or(IssueStoreError::NotFound)
    }

    async fn list(
        &self,
        _root: &ProjectPath,
        _filter: IssueListFilter,
    ) -> Result<Vec<IssueIndexEntry>, IssueStoreError> {
        Ok(self
            .issues
            .lock()
            .unwrap()
            .values()
            .map(IssueIndexEntry::from)
            .collect())
    }

    async fn update(
        &self,
        _root: &ProjectPath,
        issue: &Issue,
        expected_version: IssueVersion,
    ) -> Result<(), IssueStoreError> {
        if *self.conflict.lock().unwrap() {
            return Err(IssueStoreError::VersionConflict {
                expected: expected_version,
                actual: issue.version,
            });
        }
        let current = self.get_by_ref(_root, issue.reference()).await?;
        if current.version != expected_version {
            return Err(IssueStoreError::VersionConflict {
                expected: expected_version,
                actual: current.version,
            });
        }
        self.issues
            .lock()
            .unwrap()
            .insert(issue.reference(), issue.clone());
        Ok(())
    }

    async fn delete(
        &self,
        _root: &ProjectPath,
        issue_ref: IssueRef,
    ) -> Result<(), IssueStoreError> {
        self.issues
            .lock()
            .unwrap()
            .remove(&issue_ref)
            .map(|_| ())
            .ok_or(IssueStoreError::NotFound)
    }

    async fn read_carnet(
        &self,
        _root: &ProjectPath,
        issue_ref: IssueRef,
    ) -> Result<IssueCarnet, IssueStoreError> {
        let issue = self.get_by_ref(_root, issue_ref).await?;
        Ok(IssueCarnet {
            issue_ref,
            carnet: issue.carnet,
            version: issue.version,
            updated_by: issue.updated_by,
            updated_at: issue.updated_at,
        })
    }

    async fn write_carnet(
        &self,
        _root: &ProjectPath,
        issue_ref: IssueRef,
        carnet: MarkdownDoc,
        actor: IssueActor,
        now_ms: u64,
        expected_version: IssueVersion,
    ) -> Result<IssueCarnet, IssueStoreError> {
        let issue = self.get_by_ref(_root, issue_ref).await?;
        if issue.version != expected_version {
            return Err(IssueStoreError::VersionConflict {
                expected: expected_version,
                actual: issue.version,
            });
        }
        let updated = issue
            .mutate(actor, now_ms, |i| i.carnet = carnet)
            .map_err(|err| IssueStoreError::Invalid(err.to_string()))?;
        self.update(_root, &updated, expected_version).await?;
        self.read_carnet(_root, issue_ref).await
    }

    async fn add_attachment_from_path(
        &self,
        _root: &ProjectPath,
        _issue_ref: IssueRef,
        _source: &LocalPath,
        _attachment_id: IssueAttachmentId,
        _filename: String,
        _mime: String,
        _actor: IssueActor,
        _now_ms: u64,
        _expected_version: IssueVersion,
    ) -> Result<Issue, IssueStoreError> {
        unimplemented!("not needed")
    }

    async fn read_attachment(
        &self,
        _root: &ProjectPath,
        _issue_ref: IssueRef,
        _attachment_id: &IssueAttachmentId,
    ) -> Result<IssueAttachmentContent, IssueStoreError> {
        unimplemented!("not needed")
    }

    async fn mark_attachment_summarized(
        &self,
        _root: &ProjectPath,
        _issue_ref: IssueRef,
        _attachment_id: &IssueAttachmentId,
        _actor: IssueActor,
        _now_ms: u64,
        _expected_version: IssueVersion,
    ) -> Result<Issue, IssueStoreError> {
        unimplemented!("not needed")
    }
}

struct SeqAllocator(Mutex<u64>);

#[async_trait]
impl IssueNumberAllocator for SeqAllocator {
    async fn allocate_next(&self, _root: &ProjectPath) -> Result<IssueNumber, IssueStoreError> {
        let mut next = self.0.lock().unwrap();
        let number = IssueNumber::new(*next).unwrap();
        *next += 1;
        Ok(number)
    }
}

#[derive(Clone)]
struct FakeContexts(AgentManifest);

#[async_trait]
impl AgentContextStore for FakeContexts {
    async fn read_context(
        &self,
        _project: &Project,
        _agent: &AgentId,
    ) -> Result<MarkdownDoc, StoreError> {
        Err(StoreError::NotFound)
    }

    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.0.clone())
    }

    async fn save_manifest(
        &self,
        _project: &Project,
        _manifest: &AgentManifest,
    ) -> Result<(), StoreError> {
        Ok(())
    }
}

#[derive(Default)]
struct SpyBus(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 FixedClock;

impl Clock for FixedClock {
    fn now_millis(&self) -> i64 {
        1_234
    }
}

struct FixedIds;

impl IdGenerator for FixedIds {
    fn new_uuid(&self) -> Uuid {
        Uuid::from_u128(42)
    }
}

fn project() -> Project {
    Project::new(
        ProjectId::new_random(),
        "IdeA",
        ProjectPath::new("/tmp/idea").unwrap(),
        RemoteRef::Local,
        0,
    )
    .unwrap()
}

fn manifest(agent_id: AgentId) -> AgentManifest {
    AgentManifest::new(
        1,
        vec![ManifestEntry::new(
            agent_id,
            "Dev",
            "agents/dev.md",
            ProfileId::new_random(),
            None,
            false,
            None,
        )
        .unwrap()],
    )
    .unwrap()
}

#[tokio::test]
async fn create_issue_allocates_number_validates_agent_and_emits_event() {
    let agent_id = AgentId::new_random();
    let issues = Arc::new(FakeIssues::default());
    let bus = Arc::new(SpyBus::default());
    let uc = CreateIssue::new(
        issues.clone(),
        Arc::new(SeqAllocator(Mutex::new(1))),
        Arc::new(FakeContexts(manifest(agent_id))),
        Arc::new(FixedIds),
        Arc::new(FixedClock),
        bus.clone(),
    );

    let out = uc
        .execute(CreateIssueInput {
            project: project(),
            title: "Ship issues".into(),
            description: "Body".into(),
            priority: IssuePriority::Critical,
            status: IssueStatus::Open,
            links: Vec::new(),
            assigned_agent_ids: vec![agent_id],
            actor: IssueActor::User,
        })
        .await
        .unwrap();

    assert_eq!(out.issue.reference().to_string(), "#1");
    assert_eq!(
        out.issue.id,
        domain::IssueId::from_uuid(Uuid::from_u128(42))
    );
    assert_eq!(out.issue.created_at, 1_234);
    assert!(matches!(
        bus.events().as_slice(),
        [DomainEvent::IssueCreated { issue_ref, .. }] if issue_ref.to_string() == "#1"
    ));
}

#[tokio::test]
async fn create_issue_rejects_unknown_assignee() {
    let known = AgentId::new_random();
    let unknown = AgentId::new_random();
    let uc = CreateIssue::new(
        Arc::new(FakeIssues::default()),
        Arc::new(SeqAllocator(Mutex::new(1))),
        Arc::new(FakeContexts(manifest(known))),
        Arc::new(FixedIds),
        Arc::new(FixedClock),
        Arc::new(SpyBus::default()),
    );

    let err = uc
        .execute(CreateIssueInput {
            project: project(),
            title: "Bad assignee".into(),
            description: "Body".into(),
            priority: IssuePriority::Medium,
            status: IssueStatus::Open,
            links: Vec::new(),
            assigned_agent_ids: vec![unknown],
            actor: IssueActor::User,
        })
        .await
        .unwrap_err();

    assert_eq!(err.code(), "NOT_FOUND");
}

#[tokio::test]
async fn update_issue_propagates_expected_version_and_emits_status_event() {
    let agent_id = AgentId::new_random();
    let issues = Arc::new(FakeIssues::default());
    let initial = Issue::new(
        domain::IssueId::new_random(),
        IssueNumber::new(1).unwrap(),
        "Initial",
        MarkdownDoc::new("Body"),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1,
    )
    .unwrap();
    issues.create(&project().root, &initial).await.unwrap();
    let bus = Arc::new(SpyBus::default());
    let uc = UpdateIssue::new(
        issues,
        Arc::new(FakeContexts(manifest(agent_id))),
        Arc::new(FixedClock),
        bus.clone(),
    );

    let out = uc
        .execute(UpdateIssueInput {
            project: project(),
            issue_ref: initial.reference(),
            expected_version: initial.version,
            title: Some("Updated".into()),
            description: None,
            status: Some(IssueStatus::InProgress),
            priority: None,
            assigned_agent_ids: None,
            actor: IssueActor::System,
        })
        .await
        .unwrap();

    assert_eq!(out.issue.version.get(), 2);
    assert!(bus.events().iter().any(|event| matches!(
        event,
        DomainEvent::IssueStatusChanged {
            status: IssueStatus::InProgress,
            ..
        }
    )));
}

#[tokio::test]
async fn update_issue_maps_version_conflict() {
    let agent_id = AgentId::new_random();
    let issues = Arc::new(FakeIssues::default());
    let initial = Issue::new(
        domain::IssueId::new_random(),
        IssueNumber::new(1).unwrap(),
        "Initial",
        MarkdownDoc::new("Body"),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1,
    )
    .unwrap();
    issues.create(&project().root, &initial).await.unwrap();
    *issues.conflict.lock().unwrap() = true;
    let uc = UpdateIssue::new(
        issues,
        Arc::new(FakeContexts(manifest(agent_id))),
        Arc::new(FixedClock),
        Arc::new(SpyBus::default()),
    );

    let err = uc
        .execute(UpdateIssueInput {
            project: project(),
            issue_ref: initial.reference(),
            expected_version: initial.version,
            title: Some("Updated".into()),
            description: None,
            status: None,
            priority: None,
            assigned_agent_ids: None,
            actor: IssueActor::System,
        })
        .await
        .unwrap_err();

    assert_eq!(err.code(), "INVALID");
    assert!(err.to_string().contains("version conflict"));
}

#[tokio::test]
async fn delete_issue_emits_deleted_event_with_freed_sprint() {
    let issues = Arc::new(FakeIssues::default());
    let sprint_id = domain::SprintId::from_uuid(Uuid::from_u128(7));
    let initial = Issue::new(
        domain::IssueId::new_random(),
        IssueNumber::new(1).unwrap(),
        "In sprint",
        MarkdownDoc::new("Body"),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1,
    )
    .unwrap()
    .mutate(IssueActor::System, 2, |issue| {
        issue.sprint = Some(sprint_id);
    })
    .unwrap();
    issues.create(&project().root, &initial).await.unwrap();
    let bus = Arc::new(SpyBus::default());
    let uc = DeleteIssue::new(issues.clone(), bus.clone());

    let out = uc
        .execute(DeleteIssueInput {
            project: project(),
            issue_ref: initial.reference(),
        })
        .await
        .unwrap();

    assert_eq!(out.issue_ref, initial.reference());
    assert!(matches!(
        issues
            .get_by_ref(&project().root, initial.reference())
            .await,
        Err(IssueStoreError::NotFound)
    ));
    assert!(matches!(
        bus.events().as_slice(),
        [DomainEvent::IssueDeleted {
            issue_ref,
            freed_sprint: Some(id),
            ..
        }] if *issue_ref == initial.reference() && *id == sprint_id
    ));
}

#[tokio::test]
async fn delete_issue_emits_deleted_event_without_freed_sprint() {
    let issues = Arc::new(FakeIssues::default());
    let initial = Issue::new(
        domain::IssueId::new_random(),
        IssueNumber::new(2).unwrap(),
        "Backlog",
        MarkdownDoc::new("Body"),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1,
    )
    .unwrap();
    issues.create(&project().root, &initial).await.unwrap();
    let bus = Arc::new(SpyBus::default());
    let uc = DeleteIssue::new(issues, bus.clone());

    uc.execute(DeleteIssueInput {
        project: project(),
        issue_ref: initial.reference(),
    })
    .await
    .unwrap();

    assert!(matches!(
        bus.events().as_slice(),
        [DomainEvent::IssueDeleted {
            issue_ref,
            freed_sprint: None,
            ..
        }] if *issue_ref == initial.reference()
    ));
}

#[tokio::test]
async fn bulk_update_status_preserves_order_and_allows_partial_success() {
    let project = project();
    let issues = Arc::new(FakeIssues::default());
    for number in [1, 3] {
        let issue = Issue::new(
            domain::IssueId::new_random(),
            IssueNumber::new(number).unwrap(),
            format!("Ticket {number}"),
            MarkdownDoc::new("Body"),
            IssueStatus::Open,
            IssuePriority::Medium,
            MarkdownDoc::default(),
            Vec::new(),
            Vec::new(),
            IssueActor::User,
            1,
        )
        .unwrap();
        issues.create(&project.root, &issue).await.unwrap();
    }
    let bus = Arc::new(SpyBus::default());
    let uc = BulkUpdateIssueStatus::new(issues.clone(), Arc::new(FixedClock), bus.clone());

    let out = uc
        .execute(BulkUpdateIssueStatusInput {
            project: project.clone(),
            issue_refs: vec![
                IssueRef::from_str("#1").unwrap(),
                IssueRef::from_str("#2").unwrap(),
                IssueRef::from_str("#3").unwrap(),
            ],
            status: IssueStatus::Qa,
            actor: IssueActor::System,
        })
        .await;

    let refs: Vec<String> = out
        .items
        .iter()
        .map(|item| item.issue_ref.to_string())
        .collect();
    assert_eq!(refs, ["#1", "#2", "#3"]);
    assert_eq!(out.items[0].issue.as_ref().unwrap().status, IssueStatus::Qa);
    assert_eq!(out.items[1].error.as_ref().unwrap().code(), "NOT_FOUND");
    assert_eq!(out.items[2].issue.as_ref().unwrap().status, IssueStatus::Qa);
    assert_eq!(
        bus.events()
            .iter()
            .filter(|event| matches!(event, DomainEvent::IssueStatusChanged { .. }))
            .count(),
        2
    );
}

#[tokio::test]
async fn bulk_update_priority_updates_each_existing_ticket() {
    let project = project();
    let issues = Arc::new(FakeIssues::default());
    let initial = Issue::new(
        domain::IssueId::new_random(),
        IssueNumber::new(1).unwrap(),
        "Initial",
        MarkdownDoc::new("Body"),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1,
    )
    .unwrap();
    issues.create(&project.root, &initial).await.unwrap();
    let bus = Arc::new(SpyBus::default());
    let uc = BulkUpdateIssuePriority::new(issues.clone(), Arc::new(FixedClock), bus.clone());

    let out = uc
        .execute(BulkUpdateIssuePriorityInput {
            project,
            issue_refs: vec![initial.reference()],
            priority: IssuePriority::Critical,
            actor: IssueActor::System,
        })
        .await;

    assert_eq!(out.items.len(), 1);
    assert!(out.items[0].error.is_none());
    assert_eq!(
        out.items[0].issue.as_ref().unwrap().priority,
        IssuePriority::Critical
    );
    assert!(bus.events().iter().any(|event| matches!(
        event,
        DomainEvent::IssuePriorityChanged {
            priority: IssuePriority::Critical,
            ..
        }
    )));
}

#[tokio::test]
async fn bulk_delete_preserves_order_and_allows_partial_success() {
    let project = project();
    let issues = Arc::new(FakeIssues::default());
    let initial = Issue::new(
        domain::IssueId::new_random(),
        IssueNumber::new(1).unwrap(),
        "Initial",
        MarkdownDoc::new("Body"),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1,
    )
    .unwrap();
    issues.create(&project.root, &initial).await.unwrap();
    let bus = Arc::new(SpyBus::default());
    let uc = BulkDeleteIssues::new(issues.clone(), bus.clone());

    let out = uc
        .execute(BulkDeleteIssuesInput {
            project: project.clone(),
            issue_refs: vec![IssueRef::from_str("#2").unwrap(), initial.reference()],
        })
        .await;

    let refs: Vec<String> = out
        .items
        .iter()
        .map(|item| item.issue_ref.to_string())
        .collect();
    assert_eq!(refs, ["#2", "#1"]);
    assert_eq!(out.items[0].error.as_ref().unwrap().code(), "NOT_FOUND");
    assert!(out.items[1].error.is_none());
    assert!(matches!(
        issues.get_by_ref(&project.root, initial.reference()).await,
        Err(IssueStoreError::NotFound)
    ));
    assert!(matches!(
        bus.events().as_slice(),
        [DomainEvent::IssueDeleted {
            issue_ref,
            freed_sprint: None,
            ..
        }] if *issue_ref == initial.reference()
    ));
}
