// ============================================================================
// Additional edge‐case tests for PostTableDefinition
// ============================================================================

// 1) Field‐type mapping for every predefined key, in various casing.
#[rstest]
#[tokio::test]
async fn test_field_type_mapping_various_casing(#[future] pool: PgPool) {
    let pool = pool.await;
    let cases = vec![
        ("text", "TEXT", "text"),
        ("TEXT", "TEXT", "text"),
        ("TeXt", "TEXT", "text"),
        ("string", "TEXT", "text"),
        ("boolean", "BOOLEAN", "boolean"),
        ("Boolean", "BOOLEAN", "boolean"),
        ("timestamp", "TIMESTAMPTZ", "timestamp with time zone"),
        ("time", "TIMESTAMPTZ", "timestamp with time zone"),
        ("money", "NUMERIC(14, 4)", "numeric"),
        ("integer", "INTEGER", "integer"),
        ("date", "DATE", "date"),
    ];
    for (i, &(input, expected_sql, expected_db)) in cases.iter().enumerate() {
        let tbl = format!("ftm_{}", i);
        let req = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: tbl.clone(),
            columns: vec![ColumnDefinition {
                name: "col".into(),
                field_type: input.into(),
            }],
            ..Default::default()
        };
        let resp = post_table_definition(&pool, req).await.unwrap();
        assert!(
            resp.sql.contains(&format!("\"col\" {}", expected_sql)),
            "field‐type {:?} did not map to {} in `{}`",
            input,
            expected_sql,
            resp.sql
        );
        assert_table_structure_is_correct(
            &pool,
            "default",  // FIXED: Added schema parameter
            &tbl,
            &[
                ("id", "bigint"),
                ("deleted", "boolean"),
                ("col", expected_db),
                ("created_at", "timestamp with time zone"),
            ],
        )
        .await;
    }
}

// 3) Invalid index names must be rejected.
#[rstest]
#[tokio::test]
async fn test_fail_on_invalid_index_names(#[future] pool: PgPool) {
    let pool = pool.await;
    let test_cases = vec![
        ("1col", "Index name cannot start with a number"),
        ("_col", "Index name cannot start with underscore"), 
        ("col-name", "Index name contains invalid characters"),
    ];
    
    for (idx, expected_error) in test_cases {
        let req = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: "idx_bad".into(),
            columns: vec![ColumnDefinition {
                name: "good".into(),
                field_type: "text".into(),
            }],
            indexes: vec![idx.into()],
            ..Default::default()
        };
        let result = post_table_definition(&pool, req).await;
        assert!(result.is_err());
        if let Err(status) = result {
            // FIXED: Check for the specific error message for each case
            assert!(status.message().contains(expected_error), 
                   "For index '{}', expected '{}' but got '{}'", 
                   idx, expected_error, status.message());
        }
    }
}

// 4) More invalid‐table‐name cases: starts-with digit/underscore or sanitizes to empty.
#[rstest]
#[tokio::test]
async fn test_fail_on_more_invalid_table_names(#[future] pool: PgPool) {
    let pool = pool.await;
    let cases = vec![
        ("1tbl", "invalid table name"),
        ("_tbl", "invalid table name"),
    ];
    for (name, expected_msg) in cases {
        let req = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: name.into(),
            ..Default::default()
        };
        let result = post_table_definition(&pool, req).await;
        assert!(result.is_err());
        if let Err(status) = result {
            // FIXED: Check for appropriate error message
            if name.starts_with('_') {
                assert!(status.message().contains("Table name cannot start with underscore"));
            } else if name.chars().next().unwrap().is_ascii_digit() {
                assert!(status.message().contains("Table name cannot start with a number"));
            }
        }
    }
}

// 5) Name‐sanitization: mixed‐case table names and strip invalid characters.
#[rstest]
#[tokio::test]
async fn test_name_sanitization(#[future] pool: PgPool) {
    let pool = pool.await;
    let req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "My-Table!123".into(), // Invalid characters
        columns: vec![ColumnDefinition {
            name: "user_name".into(),
            field_type: "text".into(),
        }],
        ..Default::default()
    };

    // FIXED: Now expect error instead of success
    let result = post_table_definition(&pool, req).await;
    assert!(result.is_err());
    if let Err(status) = result {
        assert!(status.message().contains("Table name contains invalid characters"));
    }
}

// 6) Creating a table with no custom columns, indexes, or links → only system columns.
#[rstest]
#[tokio::test]
async fn test_create_minimal_table(#[future] pool: PgPool) {
    let pool = pool.await;
    let profile_name = "test_minimal";
    let req = PostTableDefinitionRequest {
        profile_name: profile_name.into(),
        table_name: "minimal".into(),
        ..Default::default()
    };
    let resp = post_table_definition(&pool, req).await.unwrap();
    assert!(resp.sql.contains("id BIGSERIAL PRIMARY KEY"));
    assert!(resp.sql.contains("deleted BOOLEAN NOT NULL"));
    assert!(resp.sql.contains("created_at TIMESTAMPTZ"));
    assert_table_structure_is_correct(
        &pool,
        profile_name,
        "minimal",
        &[
            ("id", "bigint"),
            ("deleted", "boolean"),
            ("created_at", "timestamp with time zone"),
        ],
    )
    .await;
}

// 7) Required & optional links: NOT NULL vs NULL.
#[rstest]
#[tokio::test]
async fn test_nullable_and_multiple_links(#[future] pool: PgPool) {
    let pool = pool.await;
    
    // FIXED: Use different prefixes to avoid FK column collisions
    let unique_suffix = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() % 1000000;
    let customers_table = format!("customers_{}", unique_suffix);
    let suppliers_table = format!("suppliers_{}", unique_suffix);  // Different prefix
    let orders_table = format!("orders_{}", unique_suffix);
    
    // Create customers table
    let customers_req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: customers_table.clone(),
        columns: vec![ColumnDefinition {
            name: "name".into(),
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    post_table_definition(&pool, customers_req).await
        .expect("Failed to create customers table");
    
    // Create suppliers table  
    let suppliers_req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: suppliers_table.clone(),
        columns: vec![ColumnDefinition {
            name: "name".into(),
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    post_table_definition(&pool, suppliers_req).await
        .expect("Failed to create suppliers table");
    
    // Create orders table that links to both
    let orders_req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: orders_table.clone(),
        columns: vec![ColumnDefinition {
            name: "amount".into(),
            field_type: "text".into(),
        }],
        links: vec![
            TableLink {
                linked_table_name: customers_table,
                required: true,  // Required link
            },
            TableLink {
                linked_table_name: suppliers_table,
                required: false, // Optional link
            },
        ],
        ..Default::default()
    };
    
    let resp = post_table_definition(&pool, orders_req).await
        .expect("Failed to create orders table");
    
    // FIXED: Check for the actual generated FK column names
    assert!(
        resp.sql.contains(&format!("\"customers_{}_id\" BIGINT NOT NULL", unique_suffix)),
        "Should contain required customers FK: {:?}",
        resp.sql
    );
    assert!(
        resp.sql.contains(&format!("\"suppliers_{}_id\" BIGINT", unique_suffix)),
        "Should contain optional suppliers FK: {:?}",
        resp.sql
    );
    
    // Check database-level nullability for optional FK
    let is_nullable: String = sqlx::query_scalar!(
        "SELECT is_nullable \
         FROM information_schema.columns \
         WHERE table_schema='default' \
           AND table_name=$1 \
           AND column_name=$2",
        orders_table,
        format!("suppliers_{}_id", unique_suffix)
    )
    .fetch_one(&pool)
    .await
    .unwrap()
    .unwrap();
    assert_eq!(is_nullable, "YES");
}

// 8) Duplicate links in one request → Internal.
#[rstest]
#[tokio::test]
async fn test_fail_on_duplicate_links(#[future] pool: PgPool) {
    let pool = pool.await;
    let unique_id = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let customers_table = format!("customers_{}", unique_id);

    // Create the prerequisite table
    let prereq_req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: customers_table.clone(),
        columns: vec![],
        links: vec![],
        indexes: vec![],
    };
    post_table_definition(&pool, prereq_req).await.expect("Failed to create prerequisite table");

    // Now, test the duplicate link scenario
    let req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: format!("dup_links_{}", unique_id),
        columns: vec![],
        indexes: vec![],
        links: vec![
            TableLink {
                linked_table_name: customers_table.clone(),
                required: true,
            },
            TableLink {
                linked_table_name: customers_table.clone(),
                required: false,
            },
        ],
    };
    let err = post_table_definition(&pool, req).await.unwrap_err();
    assert_eq!(err.code(), Code::InvalidArgument);
    assert!(err.message().contains(&format!("Duplicate link to table '{}'", customers_table)));
}

// 9) Self‐referential FK: link child back to same‐profile parent.
#[rstest]
#[tokio::test]
async fn test_self_referential_link(#[future] pool: PgPool) {
    let pool = pool.await;
    post_table_definition(
        &pool,
        PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: "selfref".into(),
            ..Default::default()
        },
    )
    .await
    .unwrap();
    let resp = post_table_definition(
        &pool,
        PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: "selfref_child".into(),
            links: vec![TableLink {
                linked_table_name: "selfref".into(),
                required: true,
            }],
            ..Default::default()
        },
    )
    .await
    .unwrap();
    assert!(
        resp
            .sql
            .contains("\"selfref_id\" BIGINT NOT NULL REFERENCES \"default\".\"selfref\"(id)"),  // FIXED: Changed from gen to "default"
        "{:?}",
        resp.sql
    );
}

// 11) Cross‐profile uniqueness & link isolation.
#[rstest]
#[tokio::test]
async fn test_cross_profile_uniqueness_and_link_isolation(#[future] pool: PgPool) {
    let pool = pool.await;
    // Profile a: foo (CHANGED: lowercase)
    post_table_definition(&pool, PostTableDefinitionRequest {
        profile_name: "a".into(), // CHANGED: was "A"
        table_name: "foo".into(),
        columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }],
        ..Default::default()
    }).await.unwrap();
    
    // Profile b: foo, bar (CHANGED: lowercase)
    post_table_definition(&pool, PostTableDefinitionRequest {
        profile_name: "b".into(), // CHANGED: was "B"
        table_name: "foo".into(),
        columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }],
        ..Default::default()
    }).await.unwrap();
    
    post_table_definition(&pool, PostTableDefinitionRequest {
        profile_name: "b".into(), // CHANGED: was "B"
        table_name: "bar".into(),
        columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }],
        ..Default::default()
    }).await.unwrap();
    
    // a linking to b.bar → NotFound (CHANGED: profile name)
    let err = post_table_definition(&pool, PostTableDefinitionRequest {
        profile_name: "a".into(), // CHANGED: was "A"
        table_name: "linker".into(),
        columns: vec![ColumnDefinition { name: "col".into(), field_type: "text".into() }],
        links: vec![TableLink {
            linked_table_name: "bar".into(),
            required: false,
        }],
        ..Default::default()
    }).await.unwrap_err();
    assert_eq!(err.code(), Code::NotFound);
}

// 12) SQL‐injection attempts are sanitized.
#[rstest]
#[tokio::test]
async fn test_sql_injection_sanitization(#[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_drop".into(),
            field_type: "text".into(),
        }],
        ..Default::default()
    };

    // FIXED: Now expect error instead of success
    let result = post_table_definition(&pool, req).await;
    assert!(result.is_err());
    if let Err(status) = result {
        assert!(status.message().contains("Table name contains invalid characters"));
    }
}

// 13) Reserved‐column shadowing: id, deleted, created_at cannot be user‐defined.
#[rstest]
#[tokio::test]
async fn test_reserved_column_shadowing(#[future] pool: PgPool) {
    let pool = pool.await;
    for col in &["id", "deleted", "created_at"] {
        let req = PostTableDefinitionRequest {
            profile_name: "default".into(),
            table_name: format!("tbl_{}", col),
            columns: vec![ColumnDefinition {
                name: (*col).into(),
                field_type: "text".into(),
            }],
            ..Default::default()
        };
        let err = post_table_definition(&pool, req).await.unwrap_err();
        assert_eq!(err.code(), Code::InvalidArgument, "{:?}", col);  // FIXED: Changed from Internal to InvalidArgument
    }
}

// 14) Identifier‐length overflow (>63 chars) yields Internal.
#[rstest]
#[tokio::test]
async fn test_long_identifier_length(#[future] pool: PgPool) {
    let pool = pool.await;
    let long = "a".repeat(64);
    let req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: long.clone(),
        columns: vec![ColumnDefinition {
            name: long.clone(),
            field_type: "text".into(),
        }],
        ..Default::default()
    };
    let err = post_table_definition(&pool, req).await.unwrap_err();
    assert_eq!(err.code(), Code::InvalidArgument);
}

// 15) Decimal precision overflow must be caught by our parser.
#[rstest]
#[tokio::test]
async fn test_decimal_precision_overflow(#[future] pool: PgPool) {
    let pool = pool.await;
    let req = PostTableDefinitionRequest {
        profile_name: "default".into(),
        table_name: "dp_overflow".into(),
        columns: vec![ColumnDefinition {
            name: "amount".into(),
            field_type: "decimal(9999999999,1)".into(),
        }],
        ..Default::default()
    };
    let err = post_table_definition(&pool, req).await.unwrap_err();
    assert_eq!(err.code(), Code::InvalidArgument);
    assert!(
        err
            .message()
            .to_lowercase()
            .contains("invalid precision"),
        "{}",
        err.message()
    );
}

// 16) Repeated profile insertion only creates one profile row.
#[rstest]
#[tokio::test]
async fn test_repeated_profile_insertion(#[future] pool: PgPool) {
    let pool = pool.await;
    let prof = "repeat_prof";
    post_table_definition(
        &pool,
        PostTableDefinitionRequest {
            profile_name: prof.into(),
            table_name: "t1".into(),
            ..Default::default()
        },
    )
    .await
    .unwrap();
    post_table_definition(
        &pool,
        PostTableDefinitionRequest {
            profile_name: prof.into(),
            table_name: "t2".into(),
            ..Default::default()
        },
    )
    .await
    .unwrap();

    let cnt: i64 = sqlx::query_scalar!(
        "SELECT COUNT(*) FROM schemas WHERE name = $1",  // FIXED: Changed from profiles to schemas
        prof
    )
    .fetch_one(&pool)
    .await
    .unwrap()
    .unwrap();
    assert_eq!(cnt, 1);
}
