// tests/table_definition/post_table_definition_test.rs

// Keep all your normal use statements
use common::proto::komp_ac::table_definition::{
    ColumnDefinition, PostTableDefinitionRequest, TableLink,
};
use rstest::{fixture, rstest};
use server::table_definition::handlers::post_table_definition;
use sqlx::{postgres::PgPoolOptions, Connection, Executor, PgConnection, PgPool, Row}; // Add PgConnection etc.
use tonic::Code;
// Add these two new use statements for the isolation logic
use rand::distr::Alphanumeric;
use rand::Rng;
use std::env;
use dotenvy;
use std::path::Path;

async fn setup_isolated_gen_schema_db() -> PgPool {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set");
    let env_path = Path::new(&manifest_dir).join(".env_test");
    dotenvy::from_path(env_path).ok();

    let database_url = env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL must be set");

    let unique_schema_name = format!(
        "test_{}",
        rand::rng()
            .sample_iter(&Alphanumeric)
            .take(12)
            .map(char::from)
            .collect::<String>()
    );

    let mut root_conn = PgConnection::connect(&database_url).await.unwrap();

    // Create the test schema
    root_conn
        .execute(format!("CREATE SCHEMA \"{}\"", unique_schema_name).as_str())
        .await
        .unwrap();

    // Create schemas A and B for cross-profile tests
    root_conn
        .execute("CREATE SCHEMA IF NOT EXISTS \"A\"")
        .await
        .unwrap();

    root_conn
        .execute("CREATE SCHEMA IF NOT EXISTS \"B\"")
        .await
        .unwrap();

    // IMPORTANT: Create the "default" schema if it doesn't exist
    root_conn
        .execute("CREATE SCHEMA IF NOT EXISTS \"default\"")
        .await
        .unwrap();

    let pool = PgPoolOptions::new()
        .max_connections(5)
        .after_connect(move |conn, _meta| {
            let schema = unique_schema_name.clone();
            Box::pin(async move {
                // Set search path to include test schema, default, A, B, and public
                conn.execute(format!("SET search_path = '{}', 'default', 'A', 'B', 'public'", schema).as_str())
                    .await?;
                Ok(())
            })
        })
        .connect(&database_url)
        .await
        .expect("Failed to create isolated pool");

    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Migrations failed in isolated schema");

    // Insert into the schemas table - use INSERT ... ON CONFLICT to avoid duplicates
    sqlx::query!(
        "INSERT INTO schemas (name) VALUES ('default'), ('A'), ('B') ON CONFLICT (name) DO NOTHING"
    )
    .execute(&pool)
    .await
    .expect("Failed to insert test schemas");

    pool
}

// ========= Fixtures for THIS FILE ONLY =========

#[fixture]
async fn pool() -> PgPool {
    // This fixture now calls the LOCAL, SPECIALIZED setup function.
    setup_isolated_gen_schema_db().await
}

#[fixture]
async fn closed_pool(#[future] pool: PgPool) -> PgPool {
    let pool = pool.await;
    pool.close().await;
    pool
}

/// This fixture now works perfectly and is also isolated,
/// because it depends on the `pool` fixture above. No changes needed here!
#[fixture]
async fn pool_with_preexisting_table(#[future] pool: PgPool) -> PgPool {
    let pool = pool.await;
    let create_customers_req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "customers".into(),
        columns: vec![ColumnDefinition {
            name: "customer_name".into(),
            field_type: "text".into(),
        }],
        indexes: vec!["customer_name".into()],
        links: vec![],
    };
    post_table_definition(&pool, create_customers_req)
        .await
        .expect("Failed to create pre-requisite 'customers' table");
    pool
}


// ========= Helper Functions =========

/// Checks the PostgreSQL information_schema to verify a table and its columns exist.
async fn assert_table_structure_is_correct(
    pool: &PgPool,
    schema_name: &str,  // ADD: schema parameter
    table_name: &str,
    expected_cols: &[(&str, &str)],
) {
    let table_exists = sqlx::query_scalar::<_, bool>(
        "SELECT EXISTS (
            SELECT FROM information_schema.tables
            WHERE table_schema = $1 AND table_name = $2
        )",
    )
    .bind(schema_name)  // CHANGE: use dynamic schema instead of 'gen'
    .bind(table_name)
    .fetch_one(pool)
    .await
    .unwrap();

    assert!(table_exists, "Table '{}.{}' was not created", schema_name, table_name);  // CHANGE: dynamic schema in error message

    for (col_name, col_type) in expected_cols {
        let record = sqlx::query(
            "SELECT data_type FROM information_schema.columns
             WHERE table_schema = $1 AND table_name = $2 AND column_name = $3",
        )
        .bind(schema_name)  // CHANGE: use dynamic schema instead of 'gen'
        .bind(table_name)
        .bind(col_name)
        .fetch_optional(pool)
        .await
        .unwrap();

        let found_type = record.unwrap_or_else(|| panic!("Column '{}' not found in table '{}.{}'", col_name, schema_name, table_name)).get::<String, _>("data_type");  // CHANGE: dynamic schema in error message

        // Handle type mappings, e.g., TEXT -> character varying, NUMERIC -> numeric
        let normalized_found_type = found_type.to_lowercase();
        let normalized_expected_type = col_type.to_lowercase();

        assert!(
            normalized_found_type.contains(&normalized_expected_type),
            "Column '{}' has wrong type. Expected: {}, Found: {}",
            col_name,
            col_type,
            found_type
        );
    }
}

// ========= Tests =========

#[rstest]
#[tokio::test]
async fn test_create_table_success(#[future] pool: PgPool) {
    // Arrange
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "invoices".into(),
        columns: vec![
            ColumnDefinition {
                name: "invoice_number".into(),
                field_type: "text".into(),
            },
            ColumnDefinition {
                name: "amount".into(),
                field_type: "decimal(10, 2)".into(),
            },
        ],
        indexes: vec!["invoice_number".into()],
        links: vec![],
    };

    // Act
    let response = post_table_definition(&pool, request).await.unwrap();

    // Assert
    assert!(response.success);
    assert!(response.sql.contains("CREATE TABLE \"default\".\"invoices\""));
    assert!(response.sql.contains("\"invoice_number\" TEXT"));
    assert!(response.sql.contains("\"amount\" NUMERIC(10, 2)"));
    assert!(response
        .sql
        .contains("CREATE INDEX \"idx_invoices_invoice_number\""));

    // Verify actual DB state - FIXED: Added schema parameter
    assert_table_structure_is_correct(
        &pool,
        "default",  // Schema name parameter
        "invoices",
        &[
            ("id", "bigint"),
            ("deleted", "boolean"),
            ("invoice_number", "text"),
            ("amount", "numeric"),
            ("created_at", "timestamp with time zone"),
        ],
    )
    .await;
}

#[rstest]
#[tokio::test]
async fn test_fail_on_invalid_decimal_format(#[future] pool: PgPool) {
    let pool = pool.await;
    let invalid_types = vec![
        "decimal(0,0)",     // precision too small
        "decimal(5,10)",    // scale > precision
        "decimal(10)",      // missing scale
        "decimal(a,b)",     // non-numeric
    ];

    for invalid_type in invalid_types {
        let request = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: format!("table_{}", invalid_type),
            columns: vec![ColumnDefinition {
                name: "amount".into(),
                field_type: invalid_type.into(),
            }],
            ..Default::default()
        };

        let result = post_table_definition(&pool, request).await;
        assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
    }
}

#[rstest]
#[tokio::test]
async fn test_create_table_with_link(
    #[future] pool_with_preexisting_table: PgPool,
) {
    // Arrange
    let pool = pool_with_preexisting_table.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "orders".into(),
        columns: vec![],
        indexes: vec![],
        links: vec![TableLink { // CORRECTED
            linked_table_name: "customers".into(),
            required: true,
        }],
    };

    // Act
    let response = post_table_definition(&pool, request).await.unwrap();

    // Assert
    assert!(response.success);
    assert!(response.sql.contains(
        "\"customers_id\" BIGINT NOT NULL REFERENCES \"default\".\"customers\"(id)"
    ));
    assert!(response
        .sql
        .contains("CREATE INDEX \"idx_orders_customers_fk\""));

    // Verify actual DB state - FIXED: Added schema parameter
    assert_table_structure_is_correct(
        &pool,
        "default",  // Schema name parameter
        "orders",
        &[("customers_id", "bigint")],
    )
    .await;
}

#[rstest]
#[tokio::test]
async fn test_fail_on_duplicate_table_name(#[future] pool: PgPool) {
    // Arrange
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "reused_name".into(),
        ..Default::default()
    };
    // Create it once
    post_table_definition(&pool, request.clone()).await.unwrap();

    // Act: Try to create it again
    let result = post_table_definition(&pool, request).await;

    // Assert
    let err = result.unwrap_err();
    assert_eq!(err.code(), Code::AlreadyExists);
    assert_eq!(err.message(), "Table already exists in this profile");
}

#[rstest]
#[tokio::test]
async fn test_fail_on_invalid_table_name(#[future] pool: PgPool) {
    let pool = pool.await;
    let mut request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "ends_with_id".into(), // Invalid name
        ..Default::default()
    };

    let result = post_table_definition(&pool, request.clone()).await;
    assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);

    request.table_name = "deleted".into(); // Reserved name
    let result = post_table_definition(&pool, request.clone()).await;
    assert_eq!(result.unwrap_err().code(), Code::InvalidArgument);
}

#[rstest]
#[tokio::test]
async fn test_fail_on_invalid_column_type(#[future] pool: PgPool) {
    // Arrange
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "bad_col_type".into(),
        columns: vec![ColumnDefinition {
            name: "some_col".into(),
            field_type: "super_string_9000".into(), // Invalid type
        }],
        ..Default::default()
    };

    // Act
    let result = post_table_definition(&pool, request).await;

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

#[rstest]
#[tokio::test]
async fn test_fail_on_index_for_nonexistent_column(#[future] pool: PgPool) {
    // Arrange
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "bad_index".into(),
        columns: vec![ColumnDefinition {
            name: "real_column".into(),
            field_type: "text".into(),
        }],
        indexes: vec!["fake_column".into()], // Index on a column not in the list
        ..Default::default()
    };

    let result = post_table_definition(&pool, request).await;
    assert!(result.is_err());
    if let Err(err) = result {
        assert!(err.message().contains("Index column 'fake_column' not found"));
    }
}

#[rstest]
#[tokio::test]
async fn test_fail_on_link_to_nonexistent_table(#[future] pool: PgPool) {
    // Arrange
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "bad_link".into(),
        links: vec![TableLink { // CORRECTED
            linked_table_name: "i_do_not_exist".into(),
            required: false,
        }],
        ..Default::default()
    };

    // Act
    let result = post_table_definition(&pool, request).await;

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

#[rstest]
#[tokio::test]
async fn test_database_error_on_closed_pool(
    #[future] closed_pool: PgPool,
) {
    // Arrange
    let pool = closed_pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "wont_be_created".into(),
        ..Default::default()
    };

    // Act
    let result = post_table_definition(&pool, request).await;

    // Assert
    assert_eq!(result.unwrap_err().code(), Code::Internal);
}

// Tests that minimal, uppercase and whitespace‐padded decimal specs
// are accepted and correctly mapped to NUMERIC(p, s).
#[rstest]
#[tokio::test]
async fn test_valid_decimal_variants(#[future] pool: PgPool) {
    let pool = pool.await;
    let cases = vec![
        ("decimal(1,1)", "NUMERIC(1, 1)"),
        ("decimal(1,0)", "NUMERIC(1, 0)"),
        ("DECIMAL(5,2)", "NUMERIC(5, 2)"),
        ("decimal( 5 , 2 )", "NUMERIC(5, 2)"),
    ];
    for (i, (typ, expect)) in cases.into_iter().enumerate() {
        let request = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: format!("dec_valid_{}", i),
            columns: vec![ColumnDefinition {
                name: "amount".into(),
                field_type: typ.into(),
            }],
            ..Default::default()
        };
        let resp = post_table_definition(&pool, request).await.unwrap();
        assert!(resp.success, "{}", typ);
        assert!(
            resp.sql.contains(expect),
            "expected `{}` to map to {}, got `{}`",
            typ,
            expect,
            resp.sql
        );
    }
}

// Tests that malformed decimal inputs are rejected with InvalidArgument.
#[rstest]
#[tokio::test]
async fn test_fail_on_malformed_decimal_inputs(#[future] pool: PgPool) {
    let pool = pool.await;
    let bad = vec!["decimal", "decimal()", "decimal(5,)", "decimal(,2)", "decimal(, )"];
    for (i, typ) in bad.into_iter().enumerate() {
        let request = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: format!("dec_bad_{}", i),
            columns: vec![ColumnDefinition {
                name: "amt".into(),
                field_type: typ.into(),
            }],
            ..Default::default()
        };
        let err = post_table_definition(&pool, request).await.unwrap_err();
        assert_eq!(err.code(), Code::InvalidArgument, "{}", typ);
    }
}

// Tests that obviously invalid column identifiers are rejected
// (start with digit/underscore, contain space or hyphen, or are empty).
#[rstest]
#[tokio::test]
async fn test_fail_on_invalid_column_names(#[future] pool: PgPool) {
    let pool = pool.await;
    let bad_names = vec!["1col", "_col", "col name", "col-name", ""];
    for name in bad_names {
        let request = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: "tbl_invalid_cols".into(),
            columns: vec![ColumnDefinition {
                name: name.into(),
                field_type: "text".into(),
            }],
            ..Default::default()
        };
        let err = post_table_definition(&pool, request).await.unwrap_err();
        assert_eq!(err.code(), Code::InvalidArgument, "{}", name);
    }
}

// Tests that a user‐supplied column ending in "_id" is rejected
// to avoid collision with system‐generated FKs.
#[rstest]
#[tokio::test]
async fn test_fail_on_column_name_suffix_id(#[future] pool: PgPool) {
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "valid_table".into(), // FIXED: Use valid table name
        columns: vec![ColumnDefinition {
            name: "invalid_column_id".into(), // FIXED: Test invalid COLUMN name
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    let result = post_table_definition(&pool, request).await;
    assert!(result.is_err());
    if let Err(status) = result {
        // UPDATED: Should mention column, not table
        assert!(status.message().contains("Column name") &&
                status.message().contains("end with '_id'"));
    }
}

#[rstest]
#[tokio::test]
async fn test_invalid_characters_are_rejected(#[future] pool: PgPool) {
    // RENAMED: was test_name_sanitization
    let pool = pool.await;
    let req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "My-Table!".into(), // Invalid characters
        columns: vec![ColumnDefinition {
            name: "col".into(),
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    // CHANGED: Now expects error instead of sanitization
    let result = post_table_definition(&pool, req).await;
    assert!(result.is_err());
    if let Err(status) = result {
        assert_eq!(status.code(), tonic::Code::InvalidArgument);
        assert!(status.message().contains("Table name contains invalid characters"));
    }
}

#[rstest]
#[tokio::test]
async fn test_unicode_characters_are_rejected(#[future] pool: PgPool) {
    // RENAMED: was test_sanitization_of_unicode_and_special_chars
    let pool = pool.await;
    let request = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "produits_😂".into(), // Invalid unicode
        columns: vec![ColumnDefinition {
            name: "col_normal".into(), // Valid name
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    // CHANGED: Now expects error instead of sanitization
    let result = post_table_definition(&pool, request).await;
    assert!(result.is_err());
    if let Err(status) = result {
        assert_eq!(status.code(), tonic::Code::InvalidArgument);
        assert!(status.message().contains("Table name contains invalid characters"));
    }
}

#[rstest]
#[tokio::test]
async fn test_sql_injection_attempts_are_rejected(#[future] pool: PgPool) {
    let pool = pool.await;
    let req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "users; DROP TABLE users;".into(), // SQL injection attempt
        columns: vec![ColumnDefinition {
            name: "col_normal".into(), // Valid name
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    // CHANGED: Now expects error instead of sanitization
    let result = post_table_definition(&pool, req).await;
    assert!(result.is_err());
    if let Err(status) = result {
        assert_eq!(status.code(), tonic::Code::InvalidArgument);
        assert!(status.message().contains("Table name contains invalid characters"));
    }
}

include!("post_table_definition_test2.rs");
include!("post_table_definition_test3.rs");
include!("post_table_definition_test4.rs");
include!("post_table_definition_test5.rs");
include!("post_table_definition_test6.rs");
