//! Integration tests for Action repository
//!
//! These tests verify CRUD operations, queries, and constraints
//! for the Action repository.

mod helpers;

use attune_common::repositories::{
    action::{ActionRepository, CreateActionInput, UpdateActionInput},
    Create, Delete, FindById, FindByRef, List, Update,
};
use helpers::*;
use serde_json::json;

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_action() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    assert_eq!(action.pack, pack.id);
    assert_eq!(action.pack_ref, pack.r#ref);
    assert!(action.r#ref.contains("test_pack_"));
    assert!(action.r#ref.contains(".test_action_"));
    assert!(action.created.timestamp() > 0);
    assert!(action.updated.timestamp() > 0);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_action_with_optional_fields() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "full_action")
        .with_label("Full Test Action")
        .with_description("Action with all optional fields")
        .with_entrypoint("custom.py")
        .with_param_schema(json!({
            "type": "object",
            "properties": {
                "name": {"type": "string"}
            }
        }))
        .with_out_schema(json!({
            "type": "object",
            "properties": {
                "result": {"type": "string"}
            }
        }))
        .create(&pool)
        .await
        .unwrap();

    assert_eq!(action.label, "Full Test Action");
    assert_eq!(
        action.description,
        Some("Action with all optional fields".to_string())
    );
    assert_eq!(action.entrypoint, "custom.py");
    assert!(action.param_schema.is_some());
    assert!(action.out_schema.is_some());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_action_by_id() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let created = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    let found = ActionRepository::find_by_id(&pool, created.id)
        .await
        .unwrap();

    assert!(found.is_some());
    let action = found.unwrap();
    assert_eq!(action.id, created.id);
    assert_eq!(action.r#ref, created.r#ref);
    assert_eq!(action.pack, pack.id);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_action_by_id_not_found() {
    let pool = create_test_pool().await.unwrap();

    let found = ActionRepository::find_by_id(&pool, 99999).await.unwrap();

    assert!(found.is_none());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_action_by_ref() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let created = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    let found = ActionRepository::find_by_ref(&pool, &created.r#ref)
        .await
        .unwrap();

    assert!(found.is_some());
    let action = found.unwrap();
    assert_eq!(action.id, created.id);
    assert_eq!(action.r#ref, created.r#ref);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_action_by_ref_not_found() {
    let pool = create_test_pool().await.unwrap();

    let found = ActionRepository::find_by_ref(&pool, "nonexistent.action")
        .await
        .unwrap();

    assert!(found.is_none());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_list_actions() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    // Create multiple actions
    ActionFixture::new_unique(pack.id, &pack.r#ref, "action1")
        .create(&pool)
        .await
        .unwrap();
    ActionFixture::new_unique(pack.id, &pack.r#ref, "action2")
        .create(&pool)
        .await
        .unwrap();
    ActionFixture::new_unique(pack.id, &pack.r#ref, "action3")
        .create(&pool)
        .await
        .unwrap();

    let actions = ActionRepository::list(&pool).await.unwrap();

    // Should contain at least our created actions
    assert!(actions.len() >= 3);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_list_actions_empty() {
    let pool = create_test_pool().await.unwrap();

    let actions = ActionRepository::list(&pool).await.unwrap();
    // May have actions from other tests, just verify we can list without error
    drop(actions);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_action() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    let original_updated = action.updated;

    // Wait a bit to ensure timestamp difference
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let update = UpdateActionInput {
        label: Some("Updated Label".to_string()),
        description: Some(attune_common::repositories::Patch::Set(
            "Updated description".to_string(),
        )),
        ..Default::default()
    };

    let updated = ActionRepository::update(&pool, action.id, update)
        .await
        .unwrap();

    assert_eq!(updated.id, action.id);
    assert_eq!(updated.label, "Updated Label");
    assert_eq!(updated.description, Some("Updated description".to_string()));
    assert_eq!(updated.entrypoint, action.entrypoint); // Unchanged
    assert!(updated.updated > original_updated);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_action_not_found() {
    let pool = create_test_pool().await.unwrap();

    let update = UpdateActionInput {
        label: Some("New Label".to_string()),
        ..Default::default()
    };

    let result = ActionRepository::update(&pool, 99999, update).await;

    assert!(result.is_err());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_action_partial() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .with_label("Original")
        .with_description("Original description")
        .create(&pool)
        .await
        .unwrap();

    // Update only the label
    let update = UpdateActionInput {
        label: Some("Updated Label Only".to_string()),
        ..Default::default()
    };

    let updated = ActionRepository::update(&pool, action.id, update)
        .await
        .unwrap();

    assert_eq!(updated.label, "Updated Label Only");
    assert_eq!(updated.description, action.description); // Unchanged
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_action() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    let deleted = ActionRepository::delete(&pool, action.id).await.unwrap();

    assert!(deleted);

    // Verify it's gone
    let found = ActionRepository::find_by_id(&pool, action.id)
        .await
        .unwrap();
    assert!(found.is_none());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_action_not_found() {
    let pool = create_test_pool().await.unwrap();

    let deleted = ActionRepository::delete(&pool, 99999).await.unwrap();

    assert!(!deleted);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_actions_cascade_delete_with_pack() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    // Delete the pack
    sqlx::query("DELETE FROM pack WHERE id = $1")
        .bind(pack.id)
        .execute(&pool)
        .await
        .unwrap();

    // Action should be cascade deleted
    let found = ActionRepository::find_by_id(&pool, action.id)
        .await
        .unwrap();
    assert!(found.is_none());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_action_foreign_key_constraint() {
    let pool = create_test_pool().await.unwrap();

    // Try to create action with non-existent pack
    let input = CreateActionInput {
        r#ref: "test.action".to_string(),
        pack: 99999,
        pack_ref: "nonexistent.pack".to_string(),
        label: "Test Action".to_string(),
        description: Some("Test".to_string()),
        entrypoint: "main.py".to_string(),
        runtime: None,
        runtime_version_constraint: None,
        param_schema: None,
        out_schema: None,
        is_adhoc: false,
    };

    let result = ActionRepository::create(&pool, input).await;

    assert!(result.is_err());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_multiple_actions_same_pack() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    // Create multiple actions in the same pack
    let action1 = ActionFixture::new_unique(pack.id, &pack.r#ref, "action1")
        .create(&pool)
        .await
        .unwrap();
    let action2 = ActionFixture::new_unique(pack.id, &pack.r#ref, "action2")
        .create(&pool)
        .await
        .unwrap();

    assert_eq!(action1.pack, pack.id);
    assert_eq!(action2.pack, pack.id);
    assert_ne!(action1.id, action2.id);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_action_unique_ref_constraint() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    // Create first action - use non-unique name since we're testing duplicate detection
    let action_name = helpers::unique_action_name("duplicate");
    ActionFixture::new(pack.id, &pack.r#ref, &action_name)
        .create(&pool)
        .await
        .unwrap();

    // Try to create another action with same ref (should fail)
    let result = ActionFixture::new(pack.id, &pack.r#ref, &action_name)
        .create(&pool)
        .await;

    assert!(result.is_err());
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_action_with_json_schemas() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    let param_schema = json!({
        "type": "object",
        "properties": {
            "input": {"type": "string"},
            "count": {"type": "integer"}
        },
        "required": ["input"]
    });

    let out_schema = json!({
        "type": "object",
        "properties": {
            "output": {"type": "string"},
            "status": {"type": "string"}
        }
    });

    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "schema_action")
        .with_param_schema(param_schema.clone())
        .with_out_schema(out_schema.clone())
        .create(&pool)
        .await
        .unwrap();

    assert_eq!(action.param_schema, Some(param_schema));
    assert_eq!(action.out_schema, Some(out_schema));
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_action_timestamps_auto_populated() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();

    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    let now = chrono::Utc::now();
    assert!(action.created <= now);
    assert!(action.updated <= now);
    assert!(action.created <= action.updated);
}

#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_action_updated_changes_on_update() {
    let pool = create_test_pool().await.unwrap();

    let pack = PackFixture::new_unique("test_pack")
        .create(&pool)
        .await
        .unwrap();
    let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action")
        .create(&pool)
        .await
        .unwrap();

    let original_created = action.created;
    let original_updated = action.updated;

    // Wait a bit
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let update = UpdateActionInput {
        label: Some("Updated".to_string()),
        ..Default::default()
    };

    let updated = ActionRepository::update(&pool, action.id, update)
        .await
        .unwrap();

    assert_eq!(updated.created, original_created); // Created unchanged
    assert!(updated.updated > original_updated); // Updated changed
}
