use chrono::{Duration, Utc};
use runat::{
    datastore::PostgresDatastore,
    job::{Job, JobStatus, Retry},
    queue::JobQueue,
};
use serde::{Deserialize, Serialize};
use serial_test::serial;
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestJob {
    message: String,
}

/// Helper function to get database URL from environment
fn get_database_url() -> String {
    std::env::var("DATABASE_URL").unwrap_or_else(|_| {
        "postgres://admin:admin@localhost:5432/jobs".to_string()
    })
}

/// Setup test database and return datastore
async fn setup_test_db() -> PostgresDatastore {
    let database_url = get_database_url();
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&database_url)
        .await
        .expect("Failed to connect to test database");

    let datastore = PostgresDatastore::new(pool)
        .await
        .expect("Failed to create datastore");

    datastore.migrate().await.expect("Failed to run migrations");

    // Clean database before each test to ensure clean state
    cleanup_test_db(&datastore).await;

    datastore
}

/// Cleanup test data
async fn cleanup_test_db(datastore: &PostgresDatastore) {
    sqlx::query("TRUNCATE TABLE _runat_jobs")
        .execute(datastore.pool())
        .await
        .expect("Failed to cleanup test data");
}

#[tokio::test]
#[serial]
async fn test_enqueue_and_get_job() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "test message".to_string(),
    };
    let job = Job::new(test_payload).expect("Failed to create job");
    let job_id = *job.id();

    // Act
    queue.enqueue(job).await.expect("Failed to enqueue job");
    let retrieved_job = queue.get_job(job_id).await.expect("Failed to get job");

    // Assert
    assert!(retrieved_job.is_some());
    let retrieved_job = retrieved_job.unwrap();
    assert_eq!(retrieved_job.id, job_id);
    assert_eq!(retrieved_job.status, JobStatus::Queued);

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_claim_next_job() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "test job".to_string(),
    };
    let job = Job::new(test_payload).expect("Failed to create job");
    queue.enqueue(job).await.expect("Failed to enqueue job");

    // Act
    let claimed_job = queue
        .claim_next_job()
        .await
        .expect("Failed to claim job");

    // Assert
    assert!(claimed_job.is_some());
    let claimed_job = claimed_job.unwrap();
    assert_eq!(claimed_job.status, JobStatus::Running);
    assert!(claimed_job.started_at.is_some());
    assert!(claimed_job.locked_until.is_some());

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_complete_job() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "test job".to_string(),
    };
    let job = Job::new(test_payload).expect("Failed to create job");
    let job_id = *job.id();
    queue.enqueue(job).await.expect("Failed to enqueue job");
    let claimed_job = queue
        .claim_next_job()
        .await
        .expect("Failed to claim job")
        .unwrap();

    // Act
    queue
        .complete_job(claimed_job)
        .await
        .expect("Failed to complete job");
    let completed_job = queue.get_job(job_id).await.expect("Failed to get job");

    // Assert
    assert!(completed_job.is_none(), "Job should be deleted after completion");

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_fail_job_with_retry() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "test job".to_string(),
    };
    let retry_strategy = Retry::fixed(Duration::seconds(10));
    let job = Job::new(test_payload)
        .expect("Failed to create job")
        .set_retry(retry_strategy);
    let job_id = *job.id();
    queue.enqueue(job).await.expect("Failed to enqueue job");
    let claimed_job = queue
        .claim_next_job()
        .await
        .expect("Failed to claim job")
        .unwrap();

    // Act
    queue
        .fail_job(claimed_job, "Test failure")
        .await
        .expect("Failed to fail job");
    let failed_job = queue
        .get_job(job_id)
        .await
        .expect("Failed to get job")
        .unwrap();

    // Assert
    assert_eq!(failed_job.status, JobStatus::Queued);
    assert_eq!(failed_job.attempts, 1);
    assert!(failed_job.error.is_some());
    assert_eq!(failed_job.error.unwrap(), "Test failure");

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_fail_job_max_attempts() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "test job".to_string(),
    };
    let retry_strategy = Retry::fixed(Duration::seconds(10));
    let mut job = Job::new(test_payload)
        .expect("Failed to create job")
        .set_retry(retry_strategy);
    job.max_attempts = 1;
    let job_id = *job.id();
    queue.enqueue(job).await.expect("Failed to enqueue job");
    let claimed_job = queue
        .claim_next_job()
        .await
        .expect("Failed to claim job")
        .unwrap();

    // Act
    queue
        .fail_job(claimed_job, "Test failure")
        .await
        .expect("Failed to fail job");
    let failed_job = queue
        .get_job(job_id)
        .await
        .expect("Failed to get job")
        .unwrap();

    // Assert
    assert_eq!(failed_job.status, JobStatus::Failed);
    assert_eq!(failed_job.attempts, 1);
    assert!(failed_job.completed_at.is_some());

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_cancel_job() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "test job".to_string(),
    };
    let job = Job::new(test_payload).expect("Failed to create job");
    let job_id = *job.id();
    queue.enqueue(job).await.expect("Failed to enqueue job");
    let queued_job = queue
        .get_job(job_id)
        .await
        .expect("Failed to get job")
        .unwrap();

    // Act
    queue
        .cancel_job(queued_job)
        .await
        .expect("Failed to cancel job");
    let canceled_job = queue
        .get_job(job_id)
        .await
        .expect("Failed to get job")
        .unwrap();

    // Assert
    assert_eq!(canceled_job.status, JobStatus::Canceled);
    assert!(canceled_job.completed_at.is_some());

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_schedule_job_in_future() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));
    let test_payload = TestJob {
        message: "future job".to_string(),
    };
    let future_time = Utc::now() + Duration::hours(1);
    let job = Job::new(test_payload)
        .expect("Failed to create job")
        .schedule_at(future_time);
    queue.enqueue(job).await.expect("Failed to enqueue job");

    // Act
    let claimed_job = queue
        .claim_next_job()
        .await
        .expect("Failed to claim job");

    // Assert
    assert!(
        claimed_job.is_none(),
        "Should not claim job scheduled in future"
    );

    cleanup_test_db(&datastore).await;
}

#[tokio::test]
#[serial]
async fn test_multiple_jobs_claim_order() {
    // Arrange
    let datastore = setup_test_db().await;
    let queue = JobQueue::with_datastore(Arc::new(datastore.clone()));

    let job1 = Job::new(TestJob {
        message: "job 1".to_string(),
    })
    .expect("Failed to create job");
    let job2 = Job::new(TestJob {
        message: "job 2".to_string(),
    })
    .expect("Failed to create job")
    .schedule_at(Utc::now() + Duration::seconds(5));
    let job3 = Job::new(TestJob {
        message: "job 3".to_string(),
    })
    .expect("Failed to create job");

    let job1_id = *job1.id();

    // Act
    queue.enqueue(job1).await.expect("Failed to enqueue job1");
    queue.enqueue(job2).await.expect("Failed to enqueue job2");
    queue.enqueue(job3).await.expect("Failed to enqueue job3");

    let claimed_job = queue
        .claim_next_job()
        .await
        .expect("Failed to claim job")
        .unwrap();

    // Assert
    assert_eq!(
        claimed_job.id, job1_id,
        "Should claim earliest scheduled job"
    );

    cleanup_test_db(&datastore).await;
}
