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

mod helpers;

use attune_common::{
    repositories::{
        trigger::{CreateSensorInput, SensorRepository, UpdateSensorInput},
        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_sensor_minimal() {
    let pool = create_test_pool().await.unwrap();

    // Create dependencies
    let pack = PackFixture::new_unique("sensor_pack")
        .create(&pool)
        .await
        .unwrap();

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

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

    // Create sensor
    let sensor = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "webhook_sensor",
    )
    .create(&pool)
    .await
    .unwrap();

    assert!(sensor.id > 0);
    assert!(sensor.r#ref.contains(&pack.r#ref));
    assert_eq!(sensor.pack, Some(pack.id));
    assert_eq!(sensor.pack_ref, Some(pack.r#ref));
    assert_eq!(sensor.runtime, runtime.id);
    assert_eq!(sensor.runtime_ref, runtime.r#ref);
    assert_eq!(sensor.trigger, trigger.id);
    assert_eq!(sensor.trigger_ref, trigger.r#ref);
    assert!(sensor.enabled);
    assert_eq!(sensor.param_schema, None);
    assert!(sensor.created.timestamp() > 0);
    assert!(sensor.updated.timestamp() > 0);
}

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

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

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

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

    let param_schema = json!({
        "type": "object",
        "properties": {
            "interval": {
                "type": "integer",
                "minimum": 1
            },
            "endpoint": {
                "type": "string",
                "format": "uri"
            }
        },
        "required": ["interval"]
    });

    let sensor = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "polling_sensor",
    )
    .with_param_schema(param_schema.clone())
    .create(&pool)
    .await
    .unwrap();

    assert_eq!(sensor.param_schema, Some(param_schema));
}

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

    let trigger = TriggerFixture::new_unique(None, None, "webhook")
        .create(&pool)
        .await
        .unwrap();

    let runtime = RuntimeFixture::new_unique(None, None, "python3")
        .create(&pool)
        .await
        .unwrap();

    let sensor = SensorFixture::new_unique(
        None,
        None,
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "system_sensor",
    )
    .create(&pool)
    .await
    .unwrap();

    assert_eq!(sensor.pack, None);
    assert_eq!(sensor.pack_ref, None);
}

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

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

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

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

    // Create first sensor
    let sensor_ref = format!("{}.duplicate_sensor", pack.r#ref);
    let input = CreateSensorInput {
        r#ref: sensor_ref.clone(),
        pack: Some(pack.id),
        pack_ref: Some(pack.r#ref.clone()),
        label: "Duplicate Sensor".to_string(),
        description: Some("Test sensor".to_string()),
        entrypoint: "sensors/dup.py".to_string(),
        runtime: runtime.id,
        runtime_ref: runtime.r#ref.clone(),
        runtime_version_constraint: None,
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        enabled: true,
        param_schema: None,
        config: None,
    };

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

    // Try to create second sensor with same ref
    let result = SensorRepository::create(&pool, input).await;
    assert!(result.is_err());
    // Should fail with database error due to unique constraint violation
    assert!(result.is_err());
}

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

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

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

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

    // Try invalid ref formats
    let invalid_refs = vec![
        "no_dot",             // Missing dot
        "too.many.dots.here", // Too many dots
        "UPPERCASE.sensor",   // Uppercase not allowed
    ];

    for invalid_ref in invalid_refs {
        let input = CreateSensorInput {
            r#ref: invalid_ref.to_string(),
            pack: Some(pack.id),
            pack_ref: Some(pack.r#ref.clone()),
            label: "Invalid Sensor".to_string(),
            description: Some("Test sensor".to_string()),
            entrypoint: "sensors/invalid.py".to_string(),
            runtime: runtime.id,
            runtime_ref: runtime.r#ref.clone(),
            runtime_version_constraint: None,
            trigger: trigger.id,
            trigger_ref: trigger.r#ref.clone(),
            enabled: true,
            param_schema: None,
            config: None,
        };

        let result = SensorRepository::create(&pool, input).await;
        assert!(
            result.is_err(),
            "Expected error for invalid ref: {}",
            invalid_ref
        );
    }
}

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

    let trigger = TriggerFixture::new_unique(None, None, "event")
        .create(&pool)
        .await
        .unwrap();

    let runtime = RuntimeFixture::new_unique(None, None, "python3")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateSensorInput {
        r#ref: "invalid.sensor".to_string(),
        pack: Some(99999), // Non-existent pack
        pack_ref: Some("invalid".to_string()),
        label: "Invalid Pack Sensor".to_string(),
        description: Some("Test sensor".to_string()),
        entrypoint: "sensors/invalid.py".to_string(),
        runtime: runtime.id,
        runtime_ref: runtime.r#ref.clone(),
        runtime_version_constraint: None,
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        enabled: true,
        param_schema: None,
        config: None,
    };

    let result = SensorRepository::create(&pool, input).await;
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), Error::Database(_)));
}

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

    let runtime = RuntimeFixture::new_unique(None, None, "python3")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateSensorInput {
        r#ref: "invalid.sensor".to_string(),
        pack: None,
        pack_ref: None,
        label: "Invalid Trigger Sensor".to_string(),
        description: Some("Test sensor".to_string()),
        entrypoint: "sensors/invalid.py".to_string(),
        runtime: runtime.id,
        runtime_ref: runtime.r#ref.clone(),
        runtime_version_constraint: None,
        trigger: 99999, // Non-existent trigger
        trigger_ref: "invalid.trigger".to_string(),
        enabled: true,
        param_schema: None,
        config: None,
    };

    let result = SensorRepository::create(&pool, input).await;
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), Error::Database(_)));
}

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

    let trigger = TriggerFixture::new_unique(None, None, "event")
        .create(&pool)
        .await
        .unwrap();

    let input = CreateSensorInput {
        r#ref: "invalid.sensor".to_string(),
        pack: None,
        pack_ref: None,
        label: "Invalid Runtime Sensor".to_string(),
        description: Some("Test sensor".to_string()),
        entrypoint: "sensors/invalid.py".to_string(),
        runtime: 99999, // Non-existent runtime
        runtime_ref: "invalid.runtime".to_string(),
        runtime_version_constraint: None,
        trigger: trigger.id,
        trigger_ref: trigger.r#ref.clone(),
        enabled: true,
        param_schema: None,
        config: None,
    };

    let result = SensorRepository::create(&pool, input).await;
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), Error::Database(_)));
}

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

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

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

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

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

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

    let found = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap();

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

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

    let result = SensorRepository::find_by_id(&pool, 99999).await.unwrap();
    assert!(result.is_none());
}

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

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

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

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

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

    let found = SensorRepository::get_by_id(&pool, sensor.id).await.unwrap();

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

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

    let result = SensorRepository::get_by_id(&pool, 99999).await;
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), Error::NotFound { .. }));
}

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

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

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

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

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

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

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

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

    let result = SensorRepository::find_by_ref(&pool, "nonexistent.sensor")
        .await
        .unwrap();
    assert!(result.is_none());
}

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

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

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

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

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

    let found = SensorRepository::get_by_ref(&pool, &sensor.r#ref)
        .await
        .unwrap();

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

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

    let result = SensorRepository::get_by_ref(&pool, "nonexistent.sensor").await;
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), Error::NotFound { .. }));
}

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

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

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

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

    // Create multiple sensors
    let _sensor1 = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "sensor_a",
    )
    .create(&pool)
    .await
    .unwrap();

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

    let sensors = SensorRepository::list(&pool).await.unwrap();

    // Should have at least our 2 sensors (may have more from other parallel tests)
    assert!(sensors.len() >= 2);

    // Verify sensors are sorted by ref
    for i in 1..sensors.len() {
        assert!(sensors[i - 1].r#ref <= sensors[i].r#ref);
    }
}

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

    // Count should be at least 0 (may have sensors from parallel tests)
    let sensors = SensorRepository::list(&pool).await.unwrap();
    // Just verify we can list sensors without error
    drop(sensors);
}

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

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

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

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

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

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

    let original_updated = sensor.updated;

    // Small delay to ensure updated timestamp changes
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let input = UpdateSensorInput {
        label: Some("Updated Sensor Label".to_string()),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert_eq!(updated.id, sensor.id);
    assert_eq!(updated.label, "Updated Sensor Label");
    assert_eq!(updated.description, sensor.description); // Unchanged
    assert!(updated.updated > original_updated);
}

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

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

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

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

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

    let input = UpdateSensorInput {
        description: Some(Patch::Set("New description for the sensor".to_string())),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

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

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

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

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

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

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

    let input = UpdateSensorInput {
        entrypoint: Some("sensors/new_entrypoint.py".to_string()),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert_eq!(updated.entrypoint, "sensors/new_entrypoint.py");
}

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

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

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

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

    let sensor = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "enabled_sensor",
    )
    .with_enabled(true)
    .create(&pool)
    .await
    .unwrap();

    assert!(sensor.enabled);

    let input = UpdateSensorInput {
        enabled: Some(false),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert!(!updated.enabled);

    // Enable it again
    let input = UpdateSensorInput {
        enabled: Some(true),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert!(updated.enabled);
}

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

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

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

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

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

    let new_schema = json!({
        "type": "object",
        "properties": {
            "timeout": {
                "type": "integer",
                "minimum": 0
            }
        }
    });

    let input = UpdateSensorInput {
        param_schema: Some(Patch::Set(new_schema.clone())),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert_eq!(updated.param_schema, Some(new_schema));
}

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

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

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

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

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

    let input = UpdateSensorInput {
        label: Some("Multi Update".to_string()),
        description: Some(Patch::Set("Updated multiple fields".to_string())),
        entrypoint: Some("sensors/multi.py".to_string()),
        enabled: Some(false),
        param_schema: Some(Patch::Set(json!({"type": "object"}))),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert_eq!(updated.label, "Multi Update");
    assert_eq!(
        updated.description,
        Some("Updated multiple fields".to_string())
    );
    assert_eq!(updated.entrypoint, "sensors/multi.py");
    assert!(!updated.enabled);
    assert_eq!(updated.param_schema, Some(json!({"type": "object"})));
}

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

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

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

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

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

    let original_updated = sensor.updated;

    let input = UpdateSensorInput::default();

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert_eq!(updated.id, sensor.id);
    assert_eq!(updated.label, sensor.label);
    assert_eq!(updated.description, sensor.description);
    assert_eq!(updated.entrypoint, sensor.entrypoint);
    assert_eq!(updated.enabled, sensor.enabled);
    // Updated timestamp should not change when no fields are updated
    assert_eq!(updated.updated, original_updated);
}

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

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

    let result = SensorRepository::update(&pool, 99999, input).await;
    assert!(result.is_err());
}

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

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

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

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

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

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

    let deleted = SensorRepository::delete(&pool, sensor.id).await.unwrap();
    assert!(deleted);

    // Verify sensor is gone
    let result = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap();
    assert!(result.is_none());
}

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

    let deleted = SensorRepository::delete(&pool, 99999).await.unwrap();
    assert!(!deleted);
}

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

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

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

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

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

    // Delete the pack
    use attune_common::repositories::{pack::PackRepository, Delete as _};
    PackRepository::delete(&pool, pack.id).await.unwrap();

    // Sensor should also be deleted due to CASCADE
    let result = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap();
    assert!(result.is_none());
}

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

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

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

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

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

    // Delete the trigger
    use attune_common::repositories::{trigger::TriggerRepository, Delete as _};
    TriggerRepository::delete(&pool, trigger.id).await.unwrap();

    // Sensor should also be deleted due to CASCADE
    let result = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap();
    assert!(result.is_none());
}

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

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

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

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

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

    // Delete the runtime
    use attune_common::repositories::{runtime::RuntimeRepository, Delete as _};
    RuntimeRepository::delete(&pool, runtime.id).await.unwrap();

    // Sensor should also be deleted due to CASCADE
    let result = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap();
    assert!(result.is_none());
}

// ============================================================================
// Specialized Query Tests
// ============================================================================

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

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

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

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

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

    // Create sensors for trigger1
    let sensor1 = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger1.id,
        trigger1.r#ref.clone(),
        "sensor1",
    )
    .create(&pool)
    .await
    .unwrap();

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

    // Create sensor for trigger2
    let _sensor3 = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger2.id,
        trigger2.r#ref.clone(),
        "sensor3",
    )
    .create(&pool)
    .await
    .unwrap();

    let sensors = SensorRepository::find_by_trigger(&pool, trigger1.id)
        .await
        .unwrap();

    assert_eq!(sensors.len(), 2);
    assert!(sensors.iter().any(|s| s.id == sensor1.id));
    assert!(sensors.iter().any(|s| s.id == sensor2.id));
}

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

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

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

    let sensors = SensorRepository::find_by_trigger(&pool, trigger.id)
        .await
        .unwrap();

    assert_eq!(sensors.len(), 0);
}

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

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

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

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

    // Create enabled sensor
    let enabled_sensor = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "enabled_sensor",
    )
    .with_enabled(true)
    .create(&pool)
    .await
    .unwrap();

    // Create disabled sensor
    let _disabled_sensor = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "disabled_sensor",
    )
    .with_enabled(false)
    .create(&pool)
    .await
    .unwrap();

    let enabled_sensors = SensorRepository::find_enabled(&pool).await.unwrap();

    // Should only contain enabled sensors
    assert!(enabled_sensors.iter().all(|s| s.enabled));
    assert!(enabled_sensors.iter().any(|s| s.id == enabled_sensor.id));
}

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

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

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

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

    // Create only disabled sensor
    let disabled = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "disabled",
    )
    .with_enabled(false)
    .create(&pool)
    .await
    .unwrap();

    let enabled_sensors = SensorRepository::find_enabled(&pool).await.unwrap();
    // May have enabled sensors from other parallel tests, just verify our disabled sensor is not in the list
    assert!(enabled_sensors.iter().all(|s| s.id != disabled.id));
}

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

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

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

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

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

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

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

    // Create sensors for pack1
    let sensor1 = SensorFixture::new_unique(
        Some(pack1.id),
        Some(pack1.r#ref.clone()),
        runtime1.id,
        runtime1.r#ref.clone(),
        trigger1.id,
        trigger1.r#ref.clone(),
        "pack1_sensor1",
    )
    .create(&pool)
    .await
    .unwrap();

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

    // Create sensor for pack2
    let _sensor3 = SensorFixture::new_unique(
        Some(pack2.id),
        Some(pack2.r#ref.clone()),
        runtime2.id,
        runtime2.r#ref.clone(),
        trigger2.id,
        trigger2.r#ref.clone(),
        "pack2_sensor",
    )
    .create(&pool)
    .await
    .unwrap();

    let pack1_sensors = SensorRepository::find_by_pack(&pool, pack1.id)
        .await
        .unwrap();

    assert_eq!(pack1_sensors.len(), 2);
    assert!(pack1_sensors.iter().all(|s| s.pack == Some(pack1.id)));
    assert!(pack1_sensors.iter().any(|s| s.id == sensor1.id));
    assert!(pack1_sensors.iter().any(|s| s.id == sensor2.id));
}

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

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

    let sensors = SensorRepository::find_by_pack(&pool, pack.id)
        .await
        .unwrap();

    assert_eq!(sensors.len(), 0);
}

// ============================================================================
// Timestamp Tests
// ============================================================================

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

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

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

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

    let before = chrono::Utc::now();

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

    let after = chrono::Utc::now();

    assert!(sensor.created >= before);
    assert!(sensor.created <= after);
    assert_eq!(sensor.created, sensor.updated); // Should be equal on creation
}

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

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

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

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

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

    let original_updated = sensor.updated;

    // Small delay to ensure timestamp changes
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

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

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert!(updated.updated > original_updated);
    assert_eq!(updated.created, sensor.created); // Created should not change
}

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

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

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

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

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

    let original_updated = sensor.updated;

    // Small delay
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    // Read the sensor
    let found = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap()
        .unwrap();

    assert_eq!(found.updated, original_updated); // Should not change
}

// ============================================================================
// JSON Field Tests
// ============================================================================

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

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

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

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

    let complex_schema = json!({
        "type": "object",
        "properties": {
            "connection": {
                "type": "object",
                "properties": {
                    "host": { "type": "string" },
                    "port": { "type": "integer" },
                    "ssl": { "type": "boolean" }
                },
                "required": ["host", "port"]
            },
            "filters": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "field": { "type": "string" },
                        "operator": { "enum": ["eq", "ne", "gt", "lt"] },
                        "value": {}
                    }
                }
            },
            "poll_interval": {
                "type": "integer",
                "minimum": 1,
                "maximum": 3600
            }
        },
        "required": ["connection", "poll_interval"]
    });

    let sensor = SensorFixture::new_unique(
        Some(pack.id),
        Some(pack.r#ref.clone()),
        runtime.id,
        runtime.r#ref.clone(),
        trigger.id,
        trigger.r#ref.clone(),
        "complex_sensor",
    )
    .with_param_schema(complex_schema.clone())
    .create(&pool)
    .await
    .unwrap();

    // Retrieve and verify
    let found = SensorRepository::find_by_id(&pool, sensor.id)
        .await
        .unwrap()
        .unwrap();

    assert_eq!(found.param_schema, Some(complex_schema));
}

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

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

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

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

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

    assert_eq!(sensor.param_schema, None);

    // Update to add schema
    let schema = json!({"type": "object"});
    let input = UpdateSensorInput {
        param_schema: Some(Patch::Set(schema.clone())),
        ..Default::default()
    };

    let updated = SensorRepository::update(&pool, sensor.id, input)
        .await
        .unwrap();

    assert_eq!(updated.param_schema, Some(schema));
}
