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

use async_trait::async_trait;
use domain::ports::{Clock, EventBus, EventStream, IdGenerator, IssueStore, IssueStoreError};
use domain::{
    DomainEvent, Issue, IssueActor, IssueAttachmentContent, IssueAttachmentId, IssueCarnet,
    IssueId, IssueIndexEntry, IssueListFilter, IssueNumber, IssuePriority, IssueRef, IssueStatus,
    IssueVersion, LocalPath, MarkdownDoc, Project, ProjectId, ProjectPath, RemoteRef, Sprint,
    SprintId, SprintIndexEntry, SprintOrder, SprintStatus, SprintStore, SprintStoreError,
    SprintVersion,
};
use uuid::Uuid;

use application::{
    normalized_reorder, AssignTicketToSprint, AssignTicketToSprintInput, CreateSprint,
    CreateSprintInput, DeleteSprint, DeleteSprintInput, ListSprints, ListSprintsInput,
    RenameSprint, RenameSprintInput, ReorderSprints, ReorderSprintsInput, UnassignTicketFromSprint,
    UnassignTicketFromSprintInput,
};

#[derive(Default)]
struct FakeSprints {
    sprints: Mutex<HashMap<SprintId, Sprint>>,
}

#[async_trait]
impl SprintStore for FakeSprints {
    async fn create(&self, _root: &ProjectPath, sprint: &Sprint) -> Result<(), SprintStoreError> {
        self.sprints
            .lock()
            .unwrap()
            .insert(sprint.id, sprint.clone());
        Ok(())
    }

    async fn get(
        &self,
        _root: &ProjectPath,
        sprint_id: SprintId,
    ) -> Result<Sprint, SprintStoreError> {
        self.sprints
            .lock()
            .unwrap()
            .get(&sprint_id)
            .cloned()
            .ok_or(SprintStoreError::NotFound)
    }

    async fn list(&self, _root: &ProjectPath) -> Result<Vec<SprintIndexEntry>, SprintStoreError> {
        let mut rows: Vec<_> = self
            .sprints
            .lock()
            .unwrap()
            .values()
            .map(SprintIndexEntry::from)
            .collect();
        rows.sort_by_key(|row| row.order.get());
        Ok(rows)
    }

    async fn update(
        &self,
        _root: &ProjectPath,
        sprint: &Sprint,
        expected_version: SprintVersion,
    ) -> Result<(), SprintStoreError> {
        let mut sprints = self.sprints.lock().unwrap();
        let current = sprints.get(&sprint.id).ok_or(SprintStoreError::NotFound)?;
        if current.version != expected_version {
            return Err(SprintStoreError::VersionConflict {
                expected: expected_version,
                actual: current.version,
            });
        }
        sprints.insert(sprint.id, sprint.clone());
        Ok(())
    }

    async fn delete(
        &self,
        _root: &ProjectPath,
        sprint_id: SprintId,
    ) -> Result<(), SprintStoreError> {
        self.sprints
            .lock()
            .unwrap()
            .remove(&sprint_id)
            .map(|_| ())
            .ok_or(SprintStoreError::NotFound)
    }
}

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

#[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> {
        let mut rows: Vec<IssueIndexEntry> = self
            .issues
            .lock()
            .unwrap()
            .values()
            .filter(|issue| {
                filter
                    .sprint
                    .map_or(true, |sprint| issue.sprint == Some(sprint))
            })
            .map(IssueIndexEntry::from)
            .collect();
        rows.sort_by_key(|row| row.issue_ref.number().get());
        Ok(rows)
    }

    async fn update(
        &self,
        _root: &ProjectPath,
        issue: &Issue,
        expected_version: IssueVersion,
    ) -> Result<(), IssueStoreError> {
        let mut issues = self.issues.lock().unwrap();
        let current = issues
            .get(&issue.reference())
            .ok_or(IssueStoreError::NotFound)?;
        if current.version != expected_version {
            return Err(IssueStoreError::VersionConflict {
                expected: expected_version,
                actual: current.version,
            });
        }
        issues.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> {
        unimplemented!("not needed")
    }

    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 SeqIds(Mutex<u128>);

impl IdGenerator for SeqIds {
    fn new_uuid(&self) -> Uuid {
        let mut next = self.0.lock().unwrap();
        let id = Uuid::from_u128(*next);
        *next += 1;
        id
    }
}

struct FixedClock(i64);

impl Clock for FixedClock {
    fn now_millis(&self) -> i64 {
        self.0
    }
}

#[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(Vec::new().into_iter())
    }
}

fn project() -> Project {
    Project {
        id: ProjectId::from_uuid(Uuid::from_u128(900)),
        name: "P".to_owned(),
        root: ProjectPath::new("/tmp/idea-sprint-usecases").unwrap(),
        remote: RemoteRef::Local,
        created_at: 1_000,
    }
}

fn sid(n: u128) -> SprintId {
    SprintId::from_uuid(Uuid::from_u128(n))
}

fn sprint(id: SprintId, order: u32, name: &str) -> Sprint {
    Sprint::new(
        id,
        SprintOrder::new(order).unwrap(),
        name,
        None,
        IssueActor::User,
        1_000,
    )
    .unwrap()
}

fn issue(number: u64, title: &str, sprint_id: Option<SprintId>) -> Issue {
    Issue::new(
        IssueId::from_uuid(Uuid::from_u128(number as u128)),
        IssueNumber::new(number).unwrap(),
        title,
        MarkdownDoc::default(),
        IssueStatus::Open,
        IssuePriority::Medium,
        MarkdownDoc::default(),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1_000,
    )
    .unwrap()
    .mutate(IssueActor::System, 1_000, |issue| issue.sprint = sprint_id)
    .unwrap()
}

#[tokio::test]
async fn create_sprint_uses_max_order_plus_one_and_publishes_event() {
    let sprints = Arc::new(FakeSprints::default());
    sprints
        .create(&project().root, &sprint(sid(1), 2, "Existing"))
        .await
        .unwrap();
    let bus = Arc::new(SpyBus::default());
    let usecase = CreateSprint::new(
        sprints,
        Arc::new(SeqIds(Mutex::new(10))),
        Arc::new(FixedClock(2_000)),
        bus.clone(),
    );

    let out = usecase
        .execute(CreateSprintInput {
            project: project(),
            name: "Next".to_owned(),
            status: Some(SprintStatus::Active),
            actor: IssueActor::User,
        })
        .await
        .unwrap();

    assert_eq!(out.sprint.id, sid(10));
    assert_eq!(out.sprint.order.get(), 3);
    assert_eq!(out.sprint.status, SprintStatus::Active);
    assert!(bus.events().contains(&DomainEvent::SprintCreated {
        sprint_id: sid(10),
        order: SprintOrder::new(3).unwrap(),
    }));
}

#[tokio::test]
async fn rename_and_reorder_publish_sprint_events() {
    let sprints = Arc::new(FakeSprints::default());
    let first = sprint(sid(1), 1, "One");
    let second = sprint(sid(2), 2, "Two");
    sprints.create(&project().root, &first).await.unwrap();
    sprints.create(&project().root, &second).await.unwrap();
    let bus = Arc::new(SpyBus::default());

    let renamed = RenameSprint::new(sprints.clone(), Arc::new(FixedClock(2_000)), bus.clone())
        .execute(RenameSprintInput {
            project: project(),
            sprint_id: sid(1),
            expected_version: first.version,
            name: "Renamed".to_owned(),
            actor: IssueActor::User,
        })
        .await
        .unwrap()
        .sprint;
    assert_eq!(renamed.name, "Renamed");

    let reordered = ReorderSprints::new(sprints.clone(), Arc::new(FixedClock(3_000)), bus.clone())
        .execute(ReorderSprintsInput {
            project: project(),
            ordered_ids: vec![sid(2), sid(1)],
            actor: IssueActor::System,
        })
        .await
        .unwrap();

    assert_eq!(
        reordered
            .sprints
            .iter()
            .map(|sprint| (sprint.id, sprint.order.get()))
            .collect::<Vec<_>>(),
        vec![(sid(2), 1), (sid(1), 2)]
    );
    assert!(bus.events().iter().any(|event| {
        matches!(
            event,
            DomainEvent::SprintRenamed {
                sprint_id,
                name,
                ..
            } if *sprint_id == sid(1) && name == "Renamed"
        )
    }));
    assert!(bus.events().iter().any(|event| {
        matches!(
            event,
            DomainEvent::SprintReordered { sprint_id, order, .. }
                if *sprint_id == sid(2) && order.get() == 1
        )
    }));
}

#[test]
fn normalized_reorder_rejects_incomplete_or_duplicate_ids() {
    let sprints = vec![sprint(sid(1), 1, "One"), sprint(sid(2), 2, "Two")];
    assert!(normalized_reorder(&sprints, &[sid(1)], IssueActor::User, 2_000).is_err());
    assert!(normalized_reorder(&sprints, &[sid(1), sid(1)], IssueActor::User, 2_000).is_err());
}

#[tokio::test]
async fn assign_and_unassign_ticket_to_sprint_use_issue_expected_version() {
    let sprints = Arc::new(FakeSprints::default());
    sprints
        .create(&project().root, &sprint(sid(1), 1, "One"))
        .await
        .unwrap();
    let issues = Arc::new(FakeIssues::default());
    let original = issue(1, "Ticket", None);
    issues.create(&project().root, &original).await.unwrap();
    let bus = Arc::new(SpyBus::default());
    let assign = AssignTicketToSprint::new(
        sprints,
        issues.clone(),
        Arc::new(FixedClock(2_000)),
        bus.clone(),
    );

    let conflict = assign
        .execute(AssignTicketToSprintInput {
            project: project(),
            issue_ref: original.reference(),
            sprint_id: sid(1),
            expected_version: original.version.next(),
            actor: IssueActor::User,
        })
        .await
        .unwrap_err();
    assert_eq!(conflict.code(), "INVALID");

    let assigned = assign
        .execute(AssignTicketToSprintInput {
            project: project(),
            issue_ref: original.reference(),
            sprint_id: sid(1),
            expected_version: original.version,
            actor: IssueActor::User,
        })
        .await
        .unwrap()
        .issue;
    assert_eq!(assigned.sprint, Some(sid(1)));

    let unassigned =
        UnassignTicketFromSprint::new(issues, Arc::new(FixedClock(3_000)), bus.clone())
            .execute(UnassignTicketFromSprintInput {
                project: project(),
                issue_ref: original.reference(),
                expected_version: assigned.version,
                actor: IssueActor::User,
            })
            .await
            .unwrap()
            .issue;
    assert_eq!(unassigned.sprint, None);
    assert!(bus.events().iter().any(|event| {
        matches!(
            event,
            DomainEvent::IssueSprintChanged {
                issue_ref,
                from: None,
                to: Some(target),
                ..
            } if *issue_ref == original.reference() && *target == sid(1)
        )
    }));
}

#[tokio::test]
async fn delete_sprint_unassigns_tickets_without_deleting_them() {
    let sprints = Arc::new(FakeSprints::default());
    sprints
        .create(&project().root, &sprint(sid(1), 1, "One"))
        .await
        .unwrap();
    let issues = Arc::new(FakeIssues::default());
    let assigned = issue(1, "Assigned", Some(sid(1)));
    let backlog = issue(2, "Backlog", None);
    issues.create(&project().root, &assigned).await.unwrap();
    issues.create(&project().root, &backlog).await.unwrap();
    let bus = Arc::new(SpyBus::default());

    DeleteSprint::new(
        sprints.clone(),
        issues.clone(),
        Arc::new(FixedClock(2_000)),
        bus.clone(),
    )
    .execute(DeleteSprintInput {
        project: project(),
        sprint_id: sid(1),
        actor: IssueActor::System,
    })
    .await
    .unwrap();

    assert!(matches!(
        sprints.get(&project().root, sid(1)).await.unwrap_err(),
        SprintStoreError::NotFound
    ));
    assert_eq!(
        issues
            .get_by_ref(&project().root, assigned.reference())
            .await
            .unwrap()
            .sprint,
        None
    );
    assert!(issues
        .get_by_ref(&project().root, backlog.reference())
        .await
        .is_ok());
    assert!(bus
        .events()
        .contains(&DomainEvent::SprintDeleted { sprint_id: sid(1) }));
}

#[tokio::test]
async fn list_sprints_returns_ticket_counts() {
    let sprints = Arc::new(FakeSprints::default());
    sprints
        .create(&project().root, &sprint(sid(1), 1, "One"))
        .await
        .unwrap();
    let issues = Arc::new(FakeIssues::default());
    issues
        .create(&project().root, &issue(1, "A", Some(sid(1))))
        .await
        .unwrap();
    issues
        .create(&project().root, &issue(2, "B", Some(sid(1))))
        .await
        .unwrap();

    let out = ListSprints::new(sprints, issues)
        .execute(ListSprintsInput { project: project() })
        .await
        .unwrap();

    assert_eq!(out.sprints.len(), 1);
    assert_eq!(out.sprints[0].ticket_count, 2);
}
