use std::path::PathBuf;
use std::str::FromStr;

use domain::{
    AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueId, IssueListFilter,
    IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError,
    MarkdownDoc, ProjectPath, SprintId,
};
use infrastructure::{FsIssueNumberAllocator, FsIssueStore};
use uuid::Uuid;

struct TempDir(PathBuf);

impl TempDir {
    fn new() -> Self {
        let path = std::env::temp_dir().join(format!("idea-issues-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&path).unwrap();
        Self(path)
    }

    fn root(&self) -> ProjectPath {
        ProjectPath::new(self.0.to_string_lossy().to_string()).unwrap()
    }

    fn child(&self, rel: &str) -> PathBuf {
        self.0.join(rel)
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

fn issue(root: &ProjectPath, number: u64, title: &str) -> Issue {
    let _ = root;
    Issue::new(
        IssueId::new_random(),
        domain::IssueNumber::new(number).unwrap(),
        title,
        MarkdownDoc::new("Initial description"),
        IssueStatus::Open,
        IssuePriority::High,
        MarkdownDoc::new("Initial carnet"),
        Vec::new(),
        vec![AgentIssueRef {
            agent_id: domain::AgentId::new_random(),
            role: AgentIssueRole::Assigned,
        }],
        IssueActor::User,
        1_000,
    )
    .unwrap()
}

#[tokio::test]
async fn issue_store_writes_markdown_docs_and_index() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    let issue = issue(&root, 1, "Wire issues");

    store.create(&root, &issue).await.unwrap();

    let issue_md = std::fs::read_to_string(tmp.child(".ideai/tickets/1/issue.md")).unwrap();
    let carnet_md = std::fs::read_to_string(tmp.child(".ideai/tickets/1/carnet.md")).unwrap();
    let index = std::fs::read_to_string(tmp.child(".ideai/tickets/index.json")).unwrap();

    assert!(issue_md.starts_with("---\n"));
    assert!(issue_md.contains("title: \"Wire issues\""));
    assert!(issue_md.ends_with("Initial description"));
    assert!(carnet_md.contains("issueRef: \"#1\""));
    assert!(carnet_md.ends_with("Initial carnet"));
    assert!(index.contains("\"issueRef\": \"#1\""));

    let loaded = store
        .get_by_ref(&root, IssueRef::from_str("#1").unwrap())
        .await
        .unwrap();
    assert_eq!(loaded.title, "Wire issues");
    assert_eq!(loaded.carnet.as_str(), "Initial carnet");
}

#[tokio::test]
async fn issue_store_update_checks_expected_version() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    let original = issue(&root, 2, "Conflict");
    store.create(&root, &original).await.unwrap();
    let updated = original
        .clone()
        .mutate(IssueActor::System, 2_000, |i| {
            i.status = IssueStatus::InProgress;
        })
        .unwrap();

    let err = store
        .update(&root, &updated, updated.version)
        .await
        .unwrap_err();
    assert!(matches!(err, IssueStoreError::VersionConflict { .. }));

    store
        .update(&root, &updated, original.version)
        .await
        .unwrap();
    let loaded = store.get_by_ref(&root, updated.reference()).await.unwrap();
    assert_eq!(loaded.status, IssueStatus::InProgress);
    assert_eq!(loaded.version.get(), 2);
}

#[tokio::test]
async fn issue_store_lists_by_index_filters_with_empty_or_and_and_semantics() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    store
        .create(&root, &issue(&root, 1, "Alpha"))
        .await
        .unwrap();
    let closed = issue(&root, 2, "Beta")
        .mutate(IssueActor::System, 2_000, |i| {
            i.status = IssueStatus::Closed;
            i.priority = IssuePriority::Low;
        })
        .unwrap();
    store.create(&root, &closed).await.unwrap();
    let qa_high = issue(&root, 3, "Gamma")
        .mutate(IssueActor::System, 3_000, |i| {
            i.status = IssueStatus::Qa;
            i.priority = IssuePriority::Critical;
        })
        .unwrap();
    store.create(&root, &qa_high).await.unwrap();

    let all = store.list(&root, IssueListFilter::default()).await.unwrap();
    assert_eq!(all.len(), 3);

    let by_status_or = store
        .list(
            &root,
            IssueListFilter {
                statuses: vec![IssueStatus::Closed, IssueStatus::Qa],
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();

    assert_eq!(by_status_or.len(), 2);
    assert_eq!(by_status_or[0].title, "Beta");
    assert_eq!(by_status_or[1].title, "Gamma");

    let by_priority_or = store
        .list(
            &root,
            IssueListFilter {
                priorities: vec![IssuePriority::Low, IssuePriority::Critical],
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();

    assert_eq!(by_priority_or.len(), 2);
    assert_eq!(by_priority_or[0].title, "Beta");
    assert_eq!(by_priority_or[1].title, "Gamma");

    let by_status_and_priority = store
        .list(
            &root,
            IssueListFilter {
                statuses: vec![IssueStatus::Closed, IssueStatus::Qa],
                priorities: vec![IssuePriority::Low],
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();

    assert_eq!(by_status_and_priority.len(), 1);
    assert_eq!(by_status_and_priority[0].title, "Beta");
}

#[tokio::test]
async fn issue_store_text_filter_matches_exact_ref_or_number_without_numeric_substring() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    store.create(&root, &issue(&root, 4, "Four")).await.unwrap();
    store
        .create(&root, &issue(&root, 40, "Forty"))
        .await
        .unwrap();
    store
        .create(&root, &issue(&root, 42, "Forty two"))
        .await
        .unwrap();

    let by_ref = store
        .list(
            &root,
            IssueListFilter {
                text: Some("#42".to_owned()),
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(
        by_ref.iter().map(|row| row.issue_ref).collect::<Vec<_>>(),
        vec![IssueRef::from_str("#42").unwrap()]
    );

    let by_number = store
        .list(
            &root,
            IssueListFilter {
                text: Some("42".to_owned()),
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(
        by_number
            .iter()
            .map(|row| row.issue_ref)
            .collect::<Vec<_>>(),
        vec![IssueRef::from_str("#42").unwrap()]
    );

    let by_padded_number = store
        .list(
            &root,
            IssueListFilter {
                text: Some("0042".to_owned()),
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(
        by_padded_number
            .iter()
            .map(|row| row.issue_ref)
            .collect::<Vec<_>>(),
        vec![IssueRef::from_str("#42").unwrap()]
    );

    let by_single_digit = store
        .list(
            &root,
            IssueListFilter {
                text: Some("4".to_owned()),
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(
        by_single_digit
            .iter()
            .map(|row| row.issue_ref)
            .collect::<Vec<_>>(),
        vec![IssueRef::from_str("#4").unwrap()]
    );
}

#[tokio::test]
async fn issue_store_text_filter_keeps_classic_title_description_and_carnet_matching() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    let by_title = issue(&root, 1, "Needle in title");
    let by_description = issue(&root, 2, "Plain title")
        .mutate(IssueActor::System, 2_000, |i| {
            i.description = MarkdownDoc::new("Needle in description");
        })
        .unwrap();
    let by_carnet = issue(&root, 3, "Other title")
        .mutate(IssueActor::System, 2_000, |i| {
            i.carnet = MarkdownDoc::new("Needle in carnet");
        })
        .unwrap();
    store.create(&root, &by_title).await.unwrap();
    store.create(&root, &by_description).await.unwrap();
    store.create(&root, &by_carnet).await.unwrap();

    let rows = store
        .list(
            &root,
            IssueListFilter {
                text: Some("needle".to_owned()),
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();

    assert_eq!(
        rows.iter().map(|row| row.issue_ref).collect::<Vec<_>>(),
        vec![
            IssueRef::from_str("#1").unwrap(),
            IssueRef::from_str("#2").unwrap(),
            IssueRef::from_str("#3").unwrap()
        ]
    );
}

#[tokio::test]
async fn issue_store_persists_and_filters_sprint_membership() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    let sprint_id = SprintId::from_uuid(Uuid::from_u128(42));
    let assigned = issue(&root, 1, "Assigned")
        .mutate(IssueActor::System, 2_000, |i| {
            i.sprint = Some(sprint_id);
        })
        .unwrap();
    store.create(&root, &assigned).await.unwrap();
    store
        .create(&root, &issue(&root, 2, "Backlog"))
        .await
        .unwrap();

    let loaded = store
        .get_by_ref(&root, IssueRef::from_str("#1").unwrap())
        .await
        .unwrap();
    assert_eq!(loaded.sprint, Some(sprint_id));

    let rows = store
        .list(
            &root,
            IssueListFilter {
                sprint: Some(sprint_id),
                ..IssueListFilter::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0].title, "Assigned");
    assert_eq!(rows[0].sprint, Some(sprint_id));
}

#[tokio::test]
async fn issue_store_delete_removes_files_and_index_entry() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();
    store
        .create(&root, &issue(&root, 1, "Delete me"))
        .await
        .unwrap();
    std::fs::write(tmp.child(".ideai/tickets/1/attachment.txt"), "payload").unwrap();

    store
        .delete(&root, IssueRef::from_str("#1").unwrap())
        .await
        .unwrap();

    assert!(!tmp.child(".ideai/tickets/1/issue.md").exists());
    assert!(!tmp.child(".ideai/tickets/1/carnet.md").exists());
    assert!(!tmp.child(".ideai/tickets/1/attachment.txt").exists());
    assert!(!tmp.child(".ideai/tickets/1").exists());
    let rows = store.list(&root, IssueListFilter::default()).await.unwrap();
    assert!(rows.is_empty());
    let index = std::fs::read_to_string(tmp.child(".ideai/tickets/index.json")).unwrap();
    assert!(!index.contains("\"issueRef\": \"#1\""));
}

#[tokio::test]
async fn issue_store_delete_returns_not_found_when_absent() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let store = FsIssueStore::new();

    let err = store
        .delete(&root, IssueRef::from_str("#404").unwrap())
        .await
        .unwrap_err();

    assert!(matches!(err, IssueStoreError::NotFound));
}

#[tokio::test]
async fn allocator_never_reuses_numbers() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let allocator = FsIssueNumberAllocator::new();

    let first = allocator.allocate_next(&root).await.unwrap();
    let second = allocator.allocate_next(&root).await.unwrap();
    let third = FsIssueNumberAllocator::new()
        .allocate_next(&root)
        .await
        .unwrap();

    assert_eq!(first.get(), 1);
    assert_eq!(second.get(), 2);
    assert_eq!(third.get(), 3);
}
