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

mod helpers;

use attune_common::{
    repositories::{
        rule::{CreateRuleInput, RuleRepository, UpdateRuleInput},
        Create, Delete, FindById, FindByRef, List, Patch, Update,
    },
    Error,
};
use helpers::*;
use serde_json::json;

// ============================================================================
// CREATE Tests
// ============================================================================

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

    // Setup: Create pack, action, and trigger
    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 trigger =
        TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "test_trigger")
            .create(&pool)
            .await
            .unwrap();

    // Create rule
    let rule_ref = format!("{}.test_rule", pack.r#ref);
    let input = CreateRuleInput {
        r#ref: rule_ref.clone(),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Test Rule".to_string(),
        description: Some("A test rule".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!({"equals": {"event.status": "success"}}),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let rule = RuleRepository::create(&pool, input).await.unwrap();

    assert_eq!(rule.r#ref, rule_ref);
    assert_eq!(rule.pack, pack.id);
    assert_eq!(rule.pack_ref, pack.r#ref);
    assert_eq!(rule.label, "Test Rule");
    assert_eq!(rule.description, Some("A test rule".to_string()));
    assert_eq!(rule.action, Some(action.id));
    assert_eq!(rule.action_ref, action.r#ref);
    assert_eq!(rule.trigger, Some(trigger.id));
    assert_eq!(rule.trigger_ref, trigger.r#ref);
    assert_eq!(
        rule.conditions,
        json!({"equals": {"event.status": "success"}})
    );
    assert!(rule.enabled);
    assert!(rule.created.timestamp() > 0);
    assert!(rule.updated.timestamp() > 0);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.disabled_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Disabled Rule".to_string(),
        description: Some("A disabled rule".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: false,
        is_adhoc: false,
    };

    let rule = RuleRepository::create(&pool, input).await.unwrap();

    assert!(!rule.enabled);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let conditions = json!({
        "and": [
            {"equals": {"event.type": "webhook"}},
            {"greater_than": {"event.priority": 5}},
            {"contains": {"event.tags": "important"}}
        ]
    });

    let input = CreateRuleInput {
        r#ref: format!("{}.complex_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Complex Rule".to_string(),
        description: Some("Rule with complex conditions".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: conditions.clone(),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let rule = RuleRepository::create(&pool, input).await.unwrap();

    assert_eq!(rule.conditions, conditions);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let rule_ref = format!("{}.duplicate_rule", pack.r#ref);

    // Create first rule
    let input1 = CreateRuleInput {
        r#ref: rule_ref.clone(),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "First Rule".to_string(),
        description: Some("First".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    RuleRepository::create(&pool, input1).await.unwrap();

    // Try to create second rule with same ref
    let input2 = CreateRuleInput {
        r#ref: rule_ref.clone(),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Second Rule".to_string(),
        description: Some("Second".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let result = RuleRepository::create(&pool, input2).await;

    assert!(result.is_err());
    match result.unwrap_err() {
        Error::AlreadyExists {
            entity,
            field,
            value,
        } => {
            assert_eq!(entity, "Rule");
            assert_eq!(field, "ref");
            assert_eq!(value, rule_ref);
        }
        _ => panic!("Expected AlreadyExists error"),
    }
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.UPPERCASE_RULE", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Upper Rule".to_string(),
        description: Some("Invalid uppercase ref".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

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

    assert!(result.is_err());
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: "nodotinref".to_string(),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "No Dot Rule".to_string(),
        description: Some("Invalid ref without dot".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

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

    assert!(result.is_err());
}

// ============================================================================
// READ Tests
// ============================================================================

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.find_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Find Rule".to_string(),
        description: Some("Rule to find".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let found = RuleRepository::find_by_id(&pool, created.id)
        .await
        .unwrap()
        .expect("Rule should exist");

    assert_eq!(found.id, created.id);
    assert_eq!(found.r#ref, created.r#ref);
    assert_eq!(found.label, created.label);
}

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

    let result = RuleRepository::find_by_id(&pool, 999999).await.unwrap();

    assert!(result.is_none());
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let rule_ref = format!("{}.find_by_ref", pack.r#ref);
    let input = CreateRuleInput {
        r#ref: rule_ref.clone(),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Find By Ref Rule".to_string(),
        description: Some("Find by ref".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let found = RuleRepository::find_by_ref(&pool, &rule_ref)
        .await
        .unwrap()
        .expect("Rule should exist");

    assert_eq!(found.id, created.id);
    assert_eq!(found.r#ref, rule_ref);
}

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

    let result = RuleRepository::find_by_ref(&pool, "nonexistent.rule")
        .await
        .unwrap();

    assert!(result.is_none());
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    // Create multiple rules
    for i in 1..=3 {
        let input = CreateRuleInput {
            r#ref: format!("{}.list_rule_{}", pack.r#ref, i),
            pack: pack.id,
            pack_ref: pack.r#ref.clone(),
            label: format!("List Rule {}", i),
            description: Some(format!("Rule {}", i)),
            action: action.id,
            action_ref: action.r#ref.clone(),
            trigger: trigger.id,
            trigger_ref: trigger.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: true,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    let rules = RuleRepository::list(&pool).await.unwrap();

    // Should have at least our 3 rules (may have more from parallel tests)
    let our_rules: Vec<_> = rules
        .iter()
        .filter(|r| r.r#ref.starts_with(&pack.r#ref))
        .collect();

    assert_eq!(our_rules.len(), 3);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    // Create rules in non-alphabetical order
    let names = vec!["charlie", "alice", "bob"];
    for name in names {
        let input = CreateRuleInput {
            r#ref: format!("{}.{}", pack.r#ref, name),
            pack: pack.id,
            pack_ref: pack.r#ref.clone(),
            label: name.to_string(),
            description: Some(name.to_string()),
            action: action.id,
            action_ref: action.r#ref.clone(),
            trigger: trigger.id,
            trigger_ref: trigger.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: true,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    let rules = RuleRepository::list(&pool).await.unwrap();
    let our_rules: Vec<_> = rules
        .iter()
        .filter(|r| r.r#ref.starts_with(&pack.r#ref))
        .collect();

    // Check they are ordered alphabetically
    assert!(our_rules[0].r#ref.contains("alice"));
    assert!(our_rules[1].r#ref.contains("bob"));
    assert!(our_rules[2].r#ref.contains("charlie"));
}

// ============================================================================
// UPDATE Tests
// ============================================================================

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.update_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Original Label".to_string(),
        description: Some("Original".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

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

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert_eq!(updated.label, "Updated Label");
    assert_eq!(updated.description, Some("Original".to_string())); // unchanged
    assert!(updated.updated > created.updated);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.desc_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Test".to_string(),
        description: Some("Old description".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let update = UpdateRuleInput {
        description: Some(Patch::Set("New description".to_string())),
        ..Default::default()
    };

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert_eq!(updated.description, Some("New description".to_string()));
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.cond_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Test".to_string(),
        description: Some("Test".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!({"old": "condition"}),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let new_conditions = json!({"new": "condition", "count": 42});
    let update = UpdateRuleInput {
        conditions: Some(new_conditions.clone()),
        ..Default::default()
    };

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert_eq!(updated.conditions, new_conditions);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.enabled_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Test".to_string(),
        description: Some("Test".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let update = UpdateRuleInput {
        enabled: Some(false),
        action_params: None,
        trigger_params: None,
        ..Default::default()
    };

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert!(!updated.enabled);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.multi_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Old".to_string(),
        description: Some("Old".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let update = UpdateRuleInput {
        label: Some("New Label".to_string()),
        description: Some(Patch::Set("New Description".to_string())),
        conditions: Some(json!({"updated": true})),
        action_params: None,
        trigger_params: None,
        enabled: Some(false),
    };

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert_eq!(updated.label, "New Label");
    assert_eq!(updated.description, Some("New Description".to_string()));
    assert_eq!(updated.conditions, json!({"updated": true}));
    assert!(!updated.enabled);
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.nochange_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Test".to_string(),
        description: Some("Test".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let update = UpdateRuleInput::default();

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert_eq!(updated.label, created.label);
    assert_eq!(updated.description, created.description);
}

// ============================================================================
// DELETE Tests
// ============================================================================

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.delete_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "To Delete".to_string(),
        description: Some("Will be deleted".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    let deleted = RuleRepository::delete(&pool, created.id).await.unwrap();

    assert!(deleted);

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

    assert!(found.is_none());
}

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

    let deleted = RuleRepository::delete(&pool, 999999).await.unwrap();

    assert!(!deleted);
}

// ============================================================================
// SPECIALIZED QUERY Tests
// ============================================================================

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

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

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

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

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

    let trigger1 =
        TriggerFixture::new_unique(Some(pack1.id), Some(pack1.r#ref.clone()), "trigger1")
            .create(&pool)
            .await
            .unwrap();

    let trigger2 =
        TriggerFixture::new_unique(Some(pack2.id), Some(pack2.r#ref.clone()), "trigger2")
            .create(&pool)
            .await
            .unwrap();

    // Create 2 rules for pack1
    for i in 1..=2 {
        let input = CreateRuleInput {
            r#ref: format!("{}.rule{}", pack1.r#ref, i),
            pack: pack1.id,
            pack_ref: pack1.r#ref.clone(),
            label: format!("Rule {}", i),
            description: Some(format!("Rule {}", i)),
            action: action1.id,
            action_ref: action1.r#ref.clone(),
            trigger: trigger1.id,
            trigger_ref: trigger1.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: true,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    // Create 1 rule for pack2
    let input = CreateRuleInput {
        r#ref: format!("{}.rule1", pack2.r#ref),
        pack: pack2.id,
        pack_ref: pack2.r#ref.clone(),
        label: "Pack2 Rule".to_string(),
        description: Some("Pack2".to_string()),
        action: action2.id,
        action_ref: action2.r#ref.clone(),
        trigger: trigger2.id,
        trigger_ref: trigger2.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    RuleRepository::create(&pool, input).await.unwrap();

    let pack1_rules = RuleRepository::find_by_pack(&pool, pack1.id).await.unwrap();

    assert_eq!(pack1_rules.len(), 2);
    assert!(pack1_rules.iter().all(|r| r.pack == pack1.id));

    let pack2_rules = RuleRepository::find_by_pack(&pool, pack2.id).await.unwrap();

    assert_eq!(pack2_rules.len(), 1);
    assert_eq!(pack2_rules[0].pack, pack2.id);
}

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

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

    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();

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    // Create 2 rules for action1
    for i in 1..=2 {
        let input = CreateRuleInput {
            r#ref: format!("{}.rule_a1_{}", pack.r#ref, i),
            pack: pack.id,
            pack_ref: pack.r#ref.clone(),
            label: format!("Action1 Rule {}", i),
            description: Some("Test".to_string()),
            action: action1.id,
            action_ref: action1.r#ref.clone(),
            trigger: trigger.id,
            trigger_ref: trigger.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: true,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    // Create 1 rule for action2
    let input = CreateRuleInput {
        r#ref: format!("{}.rule_a2_1", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Action2 Rule".to_string(),
        description: Some("Test".to_string()),
        action: action2.id,
        action_ref: action2.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    RuleRepository::create(&pool, input).await.unwrap();

    let action1_rules = RuleRepository::find_by_action(&pool, action1.id)
        .await
        .unwrap();

    assert_eq!(action1_rules.len(), 2);
    assert!(action1_rules.iter().all(|r| r.action == Some(action1.id)));

    let action2_rules = RuleRepository::find_by_action(&pool, action2.id)
        .await
        .unwrap();

    assert_eq!(action2_rules.len(), 1);
    assert_eq!(action2_rules[0].action, Some(action2.id));
}

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

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

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

    let trigger1 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger1")
        .create(&pool)
        .await
        .unwrap();

    let trigger2 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger2")
        .create(&pool)
        .await
        .unwrap();

    // Create 2 rules for trigger1
    for i in 1..=2 {
        let input = CreateRuleInput {
            r#ref: format!("{}.rule_t1_{}", pack.r#ref, i),
            pack: pack.id,
            pack_ref: pack.r#ref.clone(),
            label: format!("Trigger1 Rule {}", i),
            description: Some("Test".to_string()),
            action: action.id,
            action_ref: action.r#ref.clone(),
            trigger: trigger1.id,
            trigger_ref: trigger1.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: true,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    // Create 1 rule for trigger2
    let input = CreateRuleInput {
        r#ref: format!("{}.rule_t2_1", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Trigger2 Rule".to_string(),
        description: Some("Test".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger2.id,
        trigger_ref: trigger2.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    RuleRepository::create(&pool, input).await.unwrap();

    let trigger1_rules = RuleRepository::find_by_trigger(&pool, trigger1.id)
        .await
        .unwrap();

    assert_eq!(trigger1_rules.len(), 2);
    assert!(trigger1_rules
        .iter()
        .all(|r| r.trigger == Some(trigger1.id)));

    let trigger2_rules = RuleRepository::find_by_trigger(&pool, trigger2.id)
        .await
        .unwrap();

    assert_eq!(trigger2_rules.len(), 1);
    assert_eq!(trigger2_rules[0].trigger, Some(trigger2.id));
}

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    // Create enabled rules
    for i in 1..=2 {
        let input = CreateRuleInput {
            r#ref: format!("{}.enabled_{}", pack.r#ref, i),
            pack: pack.id,
            pack_ref: pack.r#ref.clone(),
            label: format!("Enabled {}", i),
            description: Some("Test".to_string()),
            action: action.id,
            action_ref: action.r#ref.clone(),
            trigger: trigger.id,
            trigger_ref: trigger.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: true,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    // Create disabled rules
    for i in 1..=2 {
        let input = CreateRuleInput {
            r#ref: format!("{}.disabled_{}", pack.r#ref, i),
            pack: pack.id,
            pack_ref: pack.r#ref.clone(),
            label: format!("Disabled {}", i),
            description: Some("Test".to_string()),
            action: action.id,
            action_ref: action.r#ref.clone(),
            trigger: trigger.id,
            trigger_ref: trigger.r#ref.clone(),
            conditions: json!([]),
            action_params: json!({}),
            trigger_params: json!({}),
            enabled: false,
            is_adhoc: false,
        };

        RuleRepository::create(&pool, input).await.unwrap();
    }

    let enabled_rules = RuleRepository::find_enabled(&pool).await.unwrap();

    // Filter to only our pack's rules
    let our_enabled: Vec<_> = enabled_rules
        .iter()
        .filter(|r| r.r#ref.starts_with(&pack.r#ref))
        .collect();

    assert_eq!(our_enabled.len(), 2);
    assert!(our_enabled.iter().all(|r| r.enabled));
}

// ============================================================================
// FOREIGN KEY CONSTRAINT Tests
// ============================================================================

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.cascade_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Cascade Rule".to_string(),
        description: Some("Will be cascade deleted".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let rule = RuleRepository::create(&pool, input).await.unwrap();

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

    // Rule should be cascade deleted
    let found = RuleRepository::find_by_id(&pool, rule.id).await.unwrap();

    assert!(found.is_none());
}

// ============================================================================
// TIMESTAMP Tests
// ============================================================================

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

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

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

    let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateRuleInput {
        r#ref: format!("{}.ts_rule", pack.r#ref),
        pack: pack.id,
        pack_ref: pack.r#ref.clone(),
        label: "Timestamp Rule".to_string(),
        description: Some("Test timestamps".to_string()),
        action: action.id,
        action_ref: action.r#ref.clone(),
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        conditions: json!([]),
        action_params: json!({}),
        trigger_params: json!({}),
        enabled: true,
        is_adhoc: false,
    };

    let created = RuleRepository::create(&pool, input).await.unwrap();

    assert!(created.created.timestamp() > 0);
    assert!(created.updated.timestamp() > 0);
    assert_eq!(created.created, created.updated);

    // Sleep briefly to ensure timestamp difference
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

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

    let updated = RuleRepository::update(&pool, created.id, update)
        .await
        .unwrap();

    assert_eq!(updated.created, created.created); // created unchanged
    assert!(updated.updated > created.updated); // updated changed
}
