//! Atomic claim guard tests — verifies TOCTOU-safe claiming via IMMEDIATE transactions.

use beads_rust::model::{Priority, Status};
use beads_rust::storage::{IssueUpdate, SqliteStorage};
use chrono::{TimeZone, Utc};
use std::path::Path;
use std::sync::{Arc, Barrier};
use std::thread;

/// Helper to create a minimal issue for testing.
fn seed_issue(storage: &mut SqliteStorage, id: &str, assignee: Option<&str>) {
    let t = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
    let issue = beads_rust::model::Issue {
        id: id.to_string(),
        title: format!("Test issue {id}"),
        status: Status::Open,
        priority: Priority(2),
        issue_type: beads_rust::model::IssueType::Task,
        created_at: t,
        updated_at: t,
        assignee: assignee.map(str::to_string),
        content_hash: None,
        description: None,
        design: None,
        acceptance_criteria: None,
        notes: None,
        owner: None,
        estimated_minutes: None,
        created_by: None,
        closed_at: None,
        close_reason: None,
        closed_by_session: None,
        due_at: None,
        defer_until: None,
        external_ref: None,
        source_system: None,
        source_repo: None,
        deleted_at: None,
        deleted_by: None,
        delete_reason: None,
        original_type: None,
        compaction_level: None,
        compacted_at: None,
        compacted_at_commit: None,
        original_size: None,
        sender: None,
        ephemeral: false,
        pinned: false,
        is_template: false,
        labels: vec![],
        dependencies: vec![],
        comments: vec![],
    };
    storage.create_issue(&issue, "seed").unwrap();
}

#[test]
fn test_claim_unassigned_succeeds() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-1", None);

    let update = IssueUpdate {
        status: Some(Status::InProgress),
        assignee: Some(Some("alice".to_string())),
        expect_unassigned: true,
        claim_actor: Some("alice".to_string()),
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-1", &update, "alice");
    assert!(result.is_ok());
    let issue = result.unwrap();
    assert_eq!(issue.assignee.as_deref(), Some("alice"));
    assert_eq!(issue.status, Status::InProgress);
}

#[test]
fn test_claim_already_assigned_different_actor_fails() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-2", Some("bob"));

    let update = IssueUpdate {
        status: Some(Status::InProgress),
        assignee: Some(Some("alice".to_string())),
        expect_unassigned: true,
        claim_actor: Some("alice".to_string()),
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-2", &update, "alice");
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("already assigned to bob"), "Error was: {err}");
}

#[test]
fn test_claim_same_actor_idempotent() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-3", Some("alice"));

    let update = IssueUpdate {
        status: Some(Status::InProgress),
        assignee: Some(Some("alice".to_string())),
        expect_unassigned: true,
        claim_actor: Some("alice".to_string()),
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-3", &update, "alice");
    assert!(result.is_ok(), "Same-actor re-claim should be idempotent");
}

#[test]
fn test_claim_exclusive_rejects_same_actor() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-4", Some("alice"));

    let update = IssueUpdate {
        status: Some(Status::InProgress),
        assignee: Some(Some("alice".to_string())),
        expect_unassigned: true,
        claim_exclusive: true,
        claim_actor: Some("alice".to_string()),
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-4", &update, "alice");
    assert!(
        result.is_err(),
        "Exclusive mode should reject same-actor re-claim"
    );
    let err = result.unwrap_err().to_string();
    assert!(
        err.contains("already assigned to alice"),
        "Error was: {err}"
    );
}

#[test]
fn test_claim_whitespace_assignee_treated_as_unassigned() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-5", Some("   "));

    let update = IssueUpdate {
        status: Some(Status::InProgress),
        assignee: Some(Some("alice".to_string())),
        expect_unassigned: true,
        claim_actor: Some("alice".to_string()),
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-5", &update, "alice");
    assert!(
        result.is_ok(),
        "Whitespace-only assignee should be treated as unassigned"
    );
}

#[test]
fn test_claim_empty_string_assignee_treated_as_unassigned() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-6", Some(""));

    let update = IssueUpdate {
        status: Some(Status::InProgress),
        assignee: Some(Some("alice".to_string())),
        expect_unassigned: true,
        claim_actor: Some("alice".to_string()),
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-6", &update, "alice");
    assert!(
        result.is_ok(),
        "Empty-string assignee should be treated as unassigned"
    );
}

#[test]
#[allow(clippy::needless_collect)]
fn test_concurrent_claim_exactly_one_wins() {
    // Use a file-based DB so two threads can share it (in-memory is single-connection).
    let tmp = tempfile::NamedTempFile::new().unwrap();
    let db_path = tmp.path().to_str().unwrap();

    // Seed the issue
    {
        let mut storage = SqliteStorage::open(Path::new(db_path)).unwrap();
        seed_issue(&mut storage, "race-1", None);
    }

    let barrier = Arc::new(Barrier::new(2));
    let path = db_path.to_string();

    let handles: Vec<_> = ["alice", "bob"]
        .iter()
        .map(|actor| {
            let barrier = Arc::clone(&barrier);
            let path = path.clone();
            let actor = actor.to_string();
            thread::spawn(move || {
                let mut storage = SqliteStorage::open(Path::new(&path)).unwrap();
                barrier.wait();

                let update = IssueUpdate {
                    status: Some(Status::InProgress),
                    assignee: Some(Some(actor.clone())),
                    expect_unassigned: true,
                    claim_actor: Some(actor.clone()),
                    ..IssueUpdate::default()
                };

                storage.update_issue("race-1", &update, &actor)
            })
        })
        .collect();

    let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();

    let successes = results.iter().filter(|r| r.is_ok()).count();
    let failures = results.iter().filter(|r| r.is_err()).count();

    assert_eq!(successes, 1, "Exactly one agent should win the race");
    assert_eq!(failures, 1, "Exactly one agent should lose the race");
}

#[test]
fn test_concurrent_claim_different_issues_both_succeed() {
    let tmp = tempfile::NamedTempFile::new().unwrap();
    let db_path = tmp.path().to_str().unwrap();

    {
        let mut storage = SqliteStorage::open(Path::new(db_path)).unwrap();
        seed_issue(&mut storage, "diff-1", None);
        seed_issue(&mut storage, "diff-2", None);
    }

    let barrier = Arc::new(Barrier::new(2));
    let path = db_path.to_string();

    let h1 = {
        let barrier = Arc::clone(&barrier);
        let path = path.clone();
        thread::spawn(move || {
            let mut storage = SqliteStorage::open(Path::new(&path)).unwrap();
            barrier.wait();
            let update = IssueUpdate {
                status: Some(Status::InProgress),
                assignee: Some(Some("alice".to_string())),
                expect_unassigned: true,
                claim_actor: Some("alice".to_string()),
                ..IssueUpdate::default()
            };
            storage.update_issue("diff-1", &update, "alice")
        })
    };

    let h2 = {
        let barrier = Arc::clone(&barrier);
        thread::spawn(move || {
            let mut storage = SqliteStorage::open(Path::new(&path)).unwrap();
            barrier.wait();
            let update = IssueUpdate {
                status: Some(Status::InProgress),
                assignee: Some(Some("bob".to_string())),
                expect_unassigned: true,
                claim_actor: Some("bob".to_string()),
                ..IssueUpdate::default()
            };
            storage.update_issue("diff-2", &update, "bob")
        })
    };

    assert!(h1.join().unwrap().is_ok(), "alice should claim diff-1");
    assert!(h2.join().unwrap().is_ok(), "bob should claim diff-2");
}

#[test]
fn test_non_claim_update_skips_guard() {
    let mut storage = SqliteStorage::open_memory().unwrap();
    seed_issue(&mut storage, "test-nc", Some("bob"));

    // Regular update (not a claim) should succeed even though assigned to someone else
    let update = IssueUpdate {
        title: Some("New title".to_string()),
        expect_unassigned: false,
        ..IssueUpdate::default()
    };

    let result = storage.update_issue("test-nc", &update, "alice");
    assert!(result.is_ok(), "Non-claim update should not check assignee");
}
