// tests/tables_data/handlers/post_table_data_test4.rs

use rust_decimal::Decimal;
use rust_decimal_macros::dec;

// Helper to create a protobuf Value from a string
fn proto_string(s: &str) -> Value {
    Value {
        kind: Some(Kind::StringValue(s.to_string())),
    }
}

// Helper to create a protobuf Value from a number
fn proto_number(n: f64) -> Value {
    Value {
        kind: Some(Kind::NumberValue(n)),
    }
}

// Helper to create a protobuf Null Value
fn proto_null() -> Value {
    Value {
        kind: Some(Kind::NullValue(0)),
    }
}

// Helper function to create a table with various decimal types for testing
async fn create_financial_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: "product_name".into(),
                field_type: "text".into(),
            },
            // Standard money column
            TableColumnDefinition {
                name: "price".into(),
                field_type: "decimal(19, 4)".into(),
            },
            // Column for things like exchange rates or precise factors
            TableColumnDefinition {
                name: "rate".into(),
                field_type: "decimal(10, 5)".into(),
            },
        ],
        indexes: vec![],
        links: vec![],
    };

    post_table_definition(pool, table_def_request).await?;
    Ok(())
}

// A new test context fixture for our financial table
#[fixture]
async fn decimal_test_context() -> TestContext {
    let pool = setup_test_db().await;
    let unique_id = generate_unique_id();
    let profile_name = format!("decimal_profile_{}", unique_id);
    let table_name = format!("invoices_{}", unique_id);

    create_financial_table(&pool, &table_name, &profile_name)
        .await
        .expect("Failed to create decimal test table");

    let (tx, _rx) = mpsc::channel(100);

    TestContext {
        pool,
        profile_name,
        table_name,
        indexer_tx: tx,
    }
}

// ========= DECIMAL/NUMERIC DATA TYPE TESTS =========

#[rstest]
#[tokio::test]
async fn test_insert_valid_decimal_string(#[future] decimal_test_context: TestContext) {
    let context = decimal_test_context.await;
    let mut data = HashMap::new();
    data.insert("product_name".into(), proto_string("Laptop"));
    data.insert("price".into(), proto_string("1499.99"));
    data.insert("rate".into(), proto_string("-0.12345"));

    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data,
    };

    let response = post_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();
    assert!(response.success);

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

    let price: Decimal = row.get("price");
    let rate: Decimal = row.get("rate");

    assert_eq!(price, dec!(1499.99));
    assert_eq!(rate, dec!(-0.12345));
}

#[rstest]
#[tokio::test]
async fn test_insert_decimal_from_number_fails(#[future] decimal_test_context: TestContext) {
    let context = decimal_test_context.await;
    let mut data = HashMap::new();
    data.insert("product_name".into(), proto_string("Mouse"));
    // THIS IS THE INVALID PART: using a number for a decimal field.
    data.insert("price".into(), proto_number(75.50));

    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data,
    };

    // The operation should fail.
    let result = post_table_data(&context.pool, request, &context.indexer_tx).await;
    assert!(result.is_err());

    // Verify the error is correct.
    let status = result.unwrap_err();
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    assert!(status
        .message()
        .contains("Expected a string representation for decimal column 'price'"));
}

#[rstest]
#[tokio::test]
async fn test_decimal_rounding_behavior(#[future] decimal_test_context: TestContext) {
    let context = decimal_test_context.await;
    let mut data = HashMap::new();
    data.insert("product_name".into(), proto_string("Keyboard"));
    // price is NUMERIC(19, 4), so this should be rounded up by the database
    data.insert("price".into(), proto_string("99.12345"));

    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data,
    };

    let response = post_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();
    assert!(response.success);

    let price: Decimal = sqlx::query_scalar(&format!(
        r#"SELECT price FROM "{}"."{}" WHERE id = $1"#,
        context.profile_name, context.table_name
    ))
    .bind(response.inserted_id)
    .fetch_one(&context.pool)
    .await
    .unwrap();

    // PostgreSQL rounds away from zero (0.5 rounds up)
    assert_eq!(price, dec!(99.1235));
}

#[rstest]
#[tokio::test]
async fn test_insert_null_and_empty_string_for_decimal(
    #[future] decimal_test_context: TestContext,
) {
    let context = decimal_test_context.await;
    let mut data = HashMap::new();
    data.insert("product_name".into(), proto_string("Monitor"));
    data.insert("price".into(), proto_string("   ")); // Empty string should be NULL
    data.insert("rate".into(), proto_null()); // Explicit NULL

    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data,
    };

    let response = post_table_data(&context.pool, request, &context.indexer_tx)
        .await
        .unwrap();
    assert!(response.success);

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

    let price: Option<Decimal> = row.get("price");
    let rate: Option<Decimal> = row.get("rate");

    assert!(price.is_none());
    assert!(rate.is_none());
}

#[rstest]
#[tokio::test]
async fn test_invalid_decimal_string_fails(#[future] decimal_test_context: TestContext) {
    let context = decimal_test_context.await;
    let mut data = HashMap::new();
    data.insert("product_name".into(), proto_string("Bad Data"));
    data.insert("price".into(), proto_string("not-a-number"));

    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data,
    };

    let result = post_table_data(&context.pool, request, &context.indexer_tx).await;
    assert!(result.is_err());
    let status = result.unwrap_err();
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    assert!(status
        .message()
        .contains("Invalid decimal string format for column 'price'"));
}

#[rstest]
#[tokio::test]
async fn test_decimal_precision_overflow_fails(#[future] decimal_test_context: TestContext) {
    let context = decimal_test_context.await;
    let mut data = HashMap::new();
    data.insert("product_name".into(), proto_string("Too Expensive"));
    // rate is NUMERIC(10, 5), so it allows 5 digits before the decimal.
    // 123456.1 is 6 digits before, so it should fail at the database level.
    data.insert("rate".into(), proto_string("123456.1"));

    let request = PostTableDataRequest {
        profile_name: context.profile_name.clone(),
        table_name: context.table_name.clone(),
        data,
    };

    let result = post_table_data(&context.pool, request, &context.indexer_tx).await;
    assert!(result.is_err());
    let status = result.unwrap_err();
    // This error comes from the database itself.
    assert_eq!(status.code(), tonic::Code::InvalidArgument);
    assert!(status.message().contains("Numeric field overflow"));
}
