// tests/tables_data/handlers/put_table_data_test.rs

use rstest::{fixture, rstest};
use sqlx::{PgPool, Row};
use std::collections::HashMap;
use prost_types::{value::Kind, Value};
use common::proto::komp_ac::table_definition::{
    PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition, TableLink,
};
use common::proto::komp_ac::tables_data::{
    PostTableDataRequest, PutTableDataRequest,
};
use server::table_definition::handlers::post_table_definition;
// The post_table_data handler is used in the "Arrange" step of each test to create initial data.
use server::tables_data::handlers::post_table_data;
// The put_table_data handler is the function we are testing.
use server::tables_data::handlers::put_table_data;
use rust_decimal_macros::dec;
use crate::common::setup_test_db;
use tokio::sync::mpsc;
use server::indexer::IndexCommand;
use rand::Rng;
use rand::distr::Alphanumeric;
use futures;

// ========= Test Helpers =========

fn generate_unique_id() -> String {
    rand::rng()
        .sample_iter(&Alphanumeric)
        .take(8)
        .map(char::from)
        .collect::<String>()
        .to_lowercase()
}

fn string_to_proto_value(s: &str) -> Value {
    Value {
        kind: Some(Kind::StringValue(s.to_string())),
    }
}

fn bool_to_proto_value(b: bool) -> Value {
    Value {
        kind: Some(Kind::BoolValue(b)),
    }
}

async fn create_adresar_table(
    pool: &PgPool,
    table_name: &str,
    profile_name: &str,
) -> Result<(), tonic::Status> {
    let table_def_request = PostTableDefinitionRequest {
        profile_name: profile_name.into(),
        table_name: table_name.into(),
        columns: vec![
            TableColumnDefinition { name: "firma".into(), field_type: "text".into() },
            TableColumnDefinition { name: "kz".into(), field_type: "text".into() },
            TableColumnDefinition { name: "ulica".into(), field_type: "text".into() },
            TableColumnDefinition { name: "mesto".into(), field_type: "text".into() },
            TableColumnDefinition { name: "telefon".into(), field_type: "text".into() },
        ],
        indexes: vec![],
        links: vec![],
    };
    post_table_definition(pool, table_def_request).await?;
    Ok(())
}

// Helper to create a record and return its ID for tests
async fn create_initial_record(
    context: &TestContext,
    initial_data: HashMap<String, Value>,
) -> i64 {
    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data: initial_data,
    };
    let response = post_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .expect("Setup: Failed to create initial record");
    response.inserted_id
}

// ========= Fixtures =========

#[derive(Clone)]
struct TestContext {
    pool: PgPool,
    profile_name: String,
    table_name: String,
    indexer_tx: mpsc::Sender<IndexCommand>,
}



#[fixture]
async fn test_context() -> TestContext {
    let pool = setup_test_db().await;
    let unique_id = generate_unique_id();
    let profile_name = format!("test_profile_{}", unique_id);
    let table_name = format!("adresar_test_{}", unique_id);
    create_adresar_table(&pool, &table_name, &profile_name)
        .await
        .expect("Failed to create test table");
    let (tx, mut rx) = mpsc::channel(100);
    // Drain receiver to prevent blocking
    tokio::spawn(async move { while rx.recv().await.is_some() {} });
    TestContext { pool, profile_name, table_name, indexer_tx: tx }
}

// ========= Update Tests (Converted from Post Tests) =========

#[rstest]
#[tokio::test]
async fn test_update_table_data_success(#[future] test_context: TestContext) {
    // Arrange
    let context = test_context.await;
    let mut initial_data = HashMap::new();
    initial_data.insert("firma".to_string(), string_to_proto_value("Original Company"));
    initial_data.insert("ulica".to_string(), string_to_proto_value("Original Street"));
    let record_id = create_initial_record(&context, initial_data).await;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("firma".to_string(), string_to_proto_value("Updated Company"));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: update_data,
    };
    let response = put_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();

    // Assert
    assert!(response.success);
    assert_eq!(response.updated_id, record_id);

    let row = sqlx::query(&format!(
        r#"SELECT firma, ulica FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(record_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();

    let firma: String = row.get("firma");
    let ulica: String = row.get("ulica");
    assert_eq!(firma, "Updated Company");
    assert_eq!(ulica, "Original Street"); // Should be unchanged
}

#[rstest]
#[tokio::test]
async fn test_update_table_data_whitespace_trimming(
    #[future] test_context: TestContext,
) {
    // Arrange
    let context = test_context.await;
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "firma".to_string(),
            string_to_proto_value("Original"),
        )]),
    )
    .await;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("firma".to_string(), string_to_proto_value("  Trimmed Co.  "));
    update_data.insert("telefon".to_string(), string_to_proto_value("  12345  "));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: update_data,
    };
    put_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();

    // Assert
    let row = sqlx::query(&format!(
        r#"SELECT firma, telefon FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(record_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();
    let firma: String = row.get("firma");
    let telefon: Option<String> = row.get("telefon");
    assert_eq!(firma, "Trimmed Co.");
    assert_eq!(telefon.unwrap(), "12345");
}

#[rstest]
#[tokio::test]
async fn test_update_field_to_null_with_empty_string(
    #[future] test_context: TestContext,
) {
    // Arrange
    let context = test_context.await;
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "telefon".to_string(),
            string_to_proto_value("555-1234"),
        )]),
    )
    .await;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("telefon".to_string(), string_to_proto_value("   ")); // Update to empty
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: update_data,
    };
    put_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();

    // Assert
    let telefon: Option<String> =
        sqlx::query_scalar(&format!(
            r#"SELECT telefon FROM "{}"."{}" WHERE id = $1"#,
            context.profile_name, context.table_name
        ))
        .bind(record_id)
        .fetch_one(&context.pool)
        .await
        .unwrap();
    assert!(telefon.is_none());
}

#[rstest]
#[tokio::test]
async fn test_update_telefon_length_limit_error(
    #[future] test_context: TestContext,
) {
    // Arrange
    let context = test_context.await;
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "telefon".to_string(),
            string_to_proto_value("valid-number"),
        )]),
    )
    .await;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("telefon".to_string(), string_to_proto_value("1".repeat(16).as_str()));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: update_data,
    };
    let result = put_table_data(&context.pool, request, &context.indexer_tx).await;

    // Assert
    assert!(result.is_err());
    assert_eq!(result.unwrap_err().code(), tonic::Code::Internal);

    // Verify original data is untouched
    let telefon: String = sqlx::query_scalar(&format!(
        r#"SELECT telefon FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(record_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();
    assert_eq!(telefon, "valid-number");
}

#[rstest]
#[tokio::test]
async fn test_update_with_invalid_column_name(#[future] test_context: TestContext) {
    // Arrange
    let context = test_context.await;
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "firma".to_string(),
            string_to_proto_value("Original"),
        )]),
    )
    .await;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("nonexistent_col".to_string(), string_to_proto_value("invalid"));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: update_data,
    };
    let result = put_table_data(&context.pool, request, &context.indexer_tx).await;

    // Assert
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert_eq!(err.code(), tonic::Code::InvalidArgument);
    assert!(err.message().contains("Invalid column: nonexistent_col"));
}

#[rstest]
#[tokio::test]
async fn test_update_with_empty_data_request(#[future] test_context: TestContext) {
    // Arrange
    let context = test_context.await;
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "firma".to_string(),
            string_to_proto_value("Original"),
        )]),
    )
    .await;

    // Act
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: HashMap::new(), // Empty data map
    };
    let result = put_table_data(&context.pool, request, &context.indexer_tx).await;

    // Assert: An update with no fields should be a no-op and succeed.
    assert!(result.is_ok());
    let response = result.unwrap();
    assert!(response.success);
    assert_eq!(response.updated_id, record_id);

    // Verify original data is untouched
    let firma: String = sqlx::query_scalar(&format!(
        r#"SELECT firma FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(record_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();
    assert_eq!(firma, "Original");
}

#[rstest]
#[tokio::test]
async fn test_update_sql_injection_protection(#[future] test_context: TestContext) {
    // Arrange
    let context = test_context.await;
    let injection_attempt = "admin'; UPDATE adresar SET firma='hacked' WHERE '1'='1";
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "firma".to_string(),
            string_to_proto_value("Safe Company"),
        )]),
    )
    .await;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("firma".to_string(), string_to_proto_value(injection_attempt));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: update_data,
    };
    put_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();

    // Assert
    let firma: String = sqlx::query_scalar(&format!(
        r#"SELECT firma FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(record_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();
    assert_eq!(firma, injection_attempt); // Should be stored as a literal string
}

#[rstest]
#[tokio::test]
async fn test_update_nonexistent_record_error(#[future] test_context: TestContext) {
    // Arrange
    let context = test_context.await;
    let nonexistent_id = 999999;

    // Act
    let mut update_data = HashMap::new();
    update_data.insert("firma".to_string(), string_to_proto_value("No one to update"));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: nonexistent_id,
        data: update_data,
    };
    let result = put_table_data(&context.pool, request, &context.indexer_tx).await;

    // Assert
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert_eq!(err.code(), tonic::Code::NotFound);
    assert!(err.message().contains("Record not found"));
}

#[rstest]
#[tokio::test]
async fn test_concurrent_updates_different_records(
    #[future] test_context: TestContext,
) {
    // Arrange
    let context = test_context.await;
    let mut record_ids = Vec::new();
    for i in 0..10 {
        let record_id = create_initial_record(
            &context,
            HashMap::from([(
                "firma".to_string(),
                string_to_proto_value(&format!("Concurrent-{}", i)),
            )]),
        )
        .await;
        record_ids.push(record_id);
    }

    // Act
    let mut tasks = Vec::new();
    for (i, record_id) in record_ids.iter().enumerate() {
        let context = context.clone();
        let record_id = *record_id;
        let task = tokio::spawn(async move {
            let mut update_data = HashMap::new();
            update_data.insert(
                "mesto".to_string(),
                string_to_proto_value(&format!("City-{}", i)),
            );
            let request = PutTableDataRequest {
                profile_name: context.profile_name.clone(),
                table_name: context.table_name.clone(),
                id: record_id,
                data: update_data,
            };
            put_table_data(&context.pool, request, &context.indexer_tx).await
        });
        tasks.push(task);
    }
    let results = futures::future::join_all(tasks).await;

    // Assert
    for result in results {
        assert!(result.unwrap().is_ok());
    }

    let count: i64 = sqlx::query_scalar(&format!(
        r#"SELECT COUNT(*) FROM "{}"."{}" WHERE mesto LIKE 'City-%'"#,
        context.profile_name, context.table_name
    ))
    .fetch_one(&context.pool)
    .await
    .unwrap();
    assert_eq!(count, 10);
}

#[rstest]
#[tokio::test]
async fn test_update_boolean_system_column_validation(
    #[future] test_context: TestContext,
) {
    // Arrange
    let context = test_context.await;
    let record_id = create_initial_record(
        &context,
        HashMap::from([(
            "firma".to_string(),
            string_to_proto_value("To be deleted"),
        )]),
    )
    .await;

    // Act: Try to update 'deleted' with a string, which is invalid
    let mut invalid_data = HashMap::new();
    invalid_data.insert("deleted".to_string(), string_to_proto_value("true"));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: invalid_data,
    };
    let result = put_table_data(&context.pool, request, &context.indexer_tx).await;

    // Assert: The operation must fail
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert_eq!(err.code(), tonic::Code::InvalidArgument);
    assert!(err.message().contains("Expected boolean for column 'deleted'"));

    // Act: Try to update 'deleted' with a proper boolean
    let mut valid_data = HashMap::new();
    valid_data.insert("deleted".to_string(), bool_to_proto_value(true));
    let request = PutTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        id: record_id,
        data: valid_data,
    };
    let result = put_table_data(&context.pool, request, &context.indexer_tx).await;

    // Assert: The operation must succeed
    assert!(result.is_ok());
    let deleted: bool = sqlx::query_scalar(&format!(
        r#"SELECT deleted FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(record_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();
    assert!(deleted);
}

include!("put_table_data_test2.rs");
include!("put_table_data_test3.rs");
include!("put_table_data_test4.rs");
include!("put_table_data_test5.rs");
