//! Comprehensive tests for the semantic validation module.
//!
//! Tests cover all 10 detection rules plus edge cases with
//! Snowflake, BigQuery, Databricks, and dbt-style SQL patterns.

use altimate_core::schema::types::SchemaDefinition;
use altimate_core::semantic::engine::check_semantics;
use altimate_core::semantic::types::SemanticSeverity;

fn ecommerce_schema() -> SchemaDefinition {
    SchemaDefinition::from_yaml(include_str!(
        "../../../tests/fixtures/schemas/ecommerce.yaml"
    ))
    .expect("Failed to load ecommerce schema")
}

/// Helper to check that a specific rule fired.
fn has_finding(result: &altimate_core::semantic::types::SemanticResult, rule: &str) -> bool {
    result.findings.iter().any(|f| f.rule == rule)
}

/// Helper to check a rule did NOT fire.
fn no_finding(result: &altimate_core::semantic::types::SemanticResult, rule: &str) -> bool {
    !has_finding(result, rule)
}

// ═══════════════════════════════════════════════════════════════════════
// Rule 1: Missing JOIN condition (cartesian product)
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_cross_join_comma_syntax() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT * FROM users, orders", &schema).await;
    assert!(result.valid);
    assert!(has_finding(&result, "missing_join_condition"));
    let finding = result
        .findings
        .iter()
        .find(|f| f.rule == "missing_join_condition")
        .unwrap();
    assert_eq!(finding.severity, SemanticSeverity::Error);
    assert!(finding.confidence > 0.8);
}

#[tokio::test]
async fn semantic_cross_join_explicit() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT * FROM users CROSS JOIN orders", &schema).await;
    assert!(result.valid);
    assert!(has_finding(&result, "missing_join_condition"));
}

#[tokio::test]
async fn semantic_no_cross_join_with_on() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT * FROM users JOIN orders ON users.id = orders.user_id",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "missing_join_condition"));
}

#[tokio::test]
async fn semantic_three_way_cross_join() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT * FROM users, orders, products", &schema).await;
    assert!(result.valid);
    // Should detect at least one cartesian product
    let cross_findings: Vec<_> = result
        .findings
        .iter()
        .filter(|f| f.rule == "missing_join_condition")
        .collect();
    assert!(!cross_findings.is_empty());
}

// ═══════════════════════════════════════════════════════════════════════
// Rule 2: Wrong JOIN key
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_wrong_join_key_non_fk() {
    let schema = ecommerce_schema();
    // Joining on name = status is obviously wrong
    let result = check_semantics(
        "SELECT * FROM users JOIN orders ON users.name = orders.status",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(has_finding(&result, "wrong_join_key"));
}

#[tokio::test]
async fn semantic_correct_join_key_fk() {
    let schema = ecommerce_schema();
    // Correct FK relationship: orders.user_id -> users.id
    let result = check_semantics(
        "SELECT * FROM users JOIN orders ON users.id = orders.user_id",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "wrong_join_key"));
}

#[tokio::test]
async fn semantic_wrong_join_key_suggestion() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT * FROM users u JOIN orders o ON u.name = o.status",
        &schema,
    )
    .await;
    assert!(result.valid);
    // Should suggest the correct FK
    let finding = result.findings.iter().find(|f| f.rule == "wrong_join_key");
    assert!(finding.is_some());
}

#[tokio::test]
async fn semantic_join_on_pk_is_ok() {
    let schema = ecommerce_schema();
    // Joining on PK columns should be OK even without FK
    let result = check_semantics(
        "SELECT * FROM users u JOIN orders o ON u.id = o.id",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "wrong_join_key"));
}

// ═══════════════════════════════════════════════════════════════════════
// Rule 3: Aggregate without GROUP BY
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_aggregate_without_group_by() {
    let schema = ecommerce_schema();
    // DataFusion may or may not allow this depending on the version,
    // but we check at the logical plan level
    let result = check_semantics("SELECT COUNT(*) FROM users", &schema).await;
    assert!(result.valid);
    // COUNT(*) alone without non-agg columns should be fine
    assert!(no_finding(&result, "aggregate_without_group_by"));
}

#[tokio::test]
async fn semantic_aggregate_with_group_by() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT status, COUNT(*) FROM users GROUP BY status",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "aggregate_without_group_by"));
}

// ═══════════════════════════════════════════════════════════════════════
// Rule 5: Duplicate counting after one-to-many JOIN
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_duplicate_counting_sum_after_join() {
    let schema = ecommerce_schema();
    // SUM on orders.total_amount after joining with order_items (one-to-many)
    let result = check_semantics(
        "SELECT o.id, SUM(o.total_amount) FROM orders o JOIN order_items oi ON o.id = oi.order_id GROUP BY o.id",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(has_finding(&result, "duplicate_counting"));
}

#[tokio::test]
async fn semantic_no_duplicate_counting_without_join() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT SUM(total_amount) FROM orders", &schema).await;
    assert!(result.valid);
    assert!(no_finding(&result, "duplicate_counting"));
}

#[tokio::test]
async fn semantic_count_after_one_to_many_join() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT u.id, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.id",
        &schema,
    )
    .await;
    assert!(result.valid);
    // COUNT after one-to-many is usually intentional but we flag it
    assert!(has_finding(&result, "duplicate_counting"));
}

// ═══════════════════════════════════════════════════════════════════════
// Rule 6: Unnecessary DISTINCT
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_distinct_after_join() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT DISTINCT u.id, u.name FROM users u JOIN orders o ON u.id = o.user_id",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(has_finding(&result, "unnecessary_distinct"));
}

#[tokio::test]
async fn semantic_distinct_without_join_ok() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT DISTINCT status FROM users", &schema).await;
    assert!(result.valid);
    assert!(no_finding(&result, "unnecessary_distinct"));
}

// ═══════════════════════════════════════════════════════════════════════
// Rule 10: Self-join without alias
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_self_join_with_aliases_ok() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT a.id, b.id FROM users a JOIN users b ON a.id = b.id",
        &schema,
    )
    .await;
    assert!(result.valid);
    // With aliases, this should not fire (aliases provide disambiguation)
}

// ═══════════════════════════════════════════════════════════════════════
// Score computation tests
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_perfect_score_for_clean_query() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT id, email FROM users WHERE age > 25", &schema).await;
    assert!(result.valid);
    assert!(
        result.semantic_score > 0.9,
        "Expected high score, got {}",
        result.semantic_score
    );
}

#[tokio::test]
async fn semantic_low_score_for_bad_query() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT * FROM users, orders, products", &schema).await;
    assert!(result.valid);
    assert!(
        result.semantic_score < 0.8,
        "Expected low score for cartesian product, got {}",
        result.semantic_score
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Invalid query handling
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_invalid_sql_returns_invalid() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECTZ * FROM users", &schema).await;
    assert!(!result.valid);
    assert!(!result.validation_errors.is_empty());
}

#[tokio::test]
async fn semantic_nonexistent_table_returns_invalid() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT * FROM nonexistent_table", &schema).await;
    assert!(!result.valid);
}

// ═══════════════════════════════════════════════════════════════════════
// Passed checks tracking
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_passed_checks_populated() {
    let schema = ecommerce_schema();
    let result = check_semantics("SELECT id FROM users WHERE age > 25", &schema).await;
    assert!(result.valid);
    assert!(
        !result.passed_checks.is_empty(),
        "Expected passed checks to be populated"
    );
    // Simple query should pass most rules
    assert!(
        result
            .passed_checks
            .contains(&"missing_join_condition".to_string())
    );
    assert!(
        result
            .passed_checks
            .contains(&"null_unsafe_comparison".to_string())
    );
}

// ═══════════════════════════════════════════════════════════════════════
// Snowflake-style patterns
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_snowflake_qualify_pattern() {
    let schema = ecommerce_schema();
    // Snowflake QUALIFY can't be parsed by generic dialect, but
    // we can test window function patterns that are common in Snowflake
    let result = check_semantics(
        "SELECT id, email, ROW_NUMBER() OVER (PARTITION BY status ORDER BY created_at) as rn FROM users",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(result.semantic_score > 0.8);
}

#[tokio::test]
async fn semantic_snowflake_lateral_flatten_approximation() {
    let schema = ecommerce_schema();
    // Test a query that uses multiple joins (common Snowflake pattern)
    let result = check_semantics(
        "SELECT u.id, o.id, oi.quantity FROM users u \
         JOIN orders o ON u.id = o.user_id \
         JOIN order_items oi ON o.id = oi.order_id",
        &schema,
    )
    .await;
    assert!(result.valid);
}

// ═══════════════════════════════════════════════════════════════════════
// BigQuery-style patterns
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_bigquery_countif_pattern() {
    let schema = ecommerce_schema();
    // BigQuery COUNTIF approximation using CASE
    let result = check_semantics(
        "SELECT status, COUNT(CASE WHEN age > 25 THEN 1 END) as count_adults \
         FROM users GROUP BY status",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "aggregate_without_group_by"));
}

#[tokio::test]
async fn semantic_bigquery_struct_style_join() {
    let schema = ecommerce_schema();
    // Multiple aggregations in a single query
    let result = check_semantics(
        "SELECT u.status, COUNT(o.id) as order_count, SUM(o.total_amount) as total \
         FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.status",
        &schema,
    )
    .await;
    assert!(result.valid);
}

// ═══════════════════════════════════════════════════════════════════════
// dbt-style patterns
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_dbt_incremental_pattern() {
    let schema = ecommerce_schema();
    // dbt incremental models often use WHERE on timestamp
    let result = check_semantics(
        "SELECT id, email, created_at FROM users WHERE created_at > '2024-01-01'",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(result.semantic_score > 0.8);
}

#[tokio::test]
async fn semantic_dbt_staging_pattern() {
    let schema = ecommerce_schema();
    // dbt staging model with rename
    let result = check_semantics(
        "SELECT id as user_id, email as user_email, name as user_name FROM users",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(result.semantic_score > 0.9);
}

// ═══════════════════════════════════════════════════════════════════════
// Databricks-style patterns
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_databricks_window_ranking() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT *, RANK() OVER (PARTITION BY status ORDER BY created_at DESC) as rnk FROM orders",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_databricks_multi_join_analytics() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT u.email, COUNT(DISTINCT o.id) as num_orders, SUM(oi.quantity * oi.unit_price) as total_value \
         FROM users u \
         JOIN orders o ON u.id = o.user_id \
         JOIN order_items oi ON o.id = oi.order_id \
         GROUP BY u.email",
        &schema,
    )
    .await;
    assert!(result.valid);
}

// ═══════════════════════════════════════════════════════════════════════
// Edge cases
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn semantic_empty_query() {
    let schema = ecommerce_schema();
    let result = check_semantics("", &schema).await;
    assert!(!result.valid);
}

#[tokio::test]
async fn semantic_subquery_in_where() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'completed')",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_cte_query() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "WITH active_users AS (SELECT * FROM users WHERE status = 'active') \
         SELECT au.id, o.total_amount FROM active_users au \
         JOIN orders o ON au.id = o.user_id",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_union_query() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT id, name FROM users WHERE status = 'active' \
         UNION ALL \
         SELECT id, name FROM users WHERE status = 'suspended'",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_complex_multi_level_join() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT u.name, p.name as product_name, oi.quantity \
         FROM users u \
         JOIN orders o ON u.id = o.user_id \
         JOIN order_items oi ON o.id = oi.order_id \
         JOIN products p ON oi.product_id = p.id \
         WHERE u.status = 'active' AND o.status = 'completed'",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "missing_join_condition"));
    assert!(no_finding(&result, "wrong_join_key"));
}

#[tokio::test]
async fn semantic_left_join_preserves_nulls() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT u.id, u.email, o.total_amount \
         FROM users u LEFT JOIN orders o ON u.id = o.user_id",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_having_clause() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT user_id, COUNT(*) as order_count FROM orders \
         GROUP BY user_id HAVING COUNT(*) > 5",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(no_finding(&result, "where_vs_having"));
}

#[tokio::test]
async fn semantic_nested_aggregation() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT status, AVG(total_amount) as avg_total, MAX(total_amount) as max_total \
         FROM orders GROUP BY status",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_correlated_subquery() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT u.id, u.name FROM users u \
         WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'completed')",
        &schema,
    )
    .await;
    assert!(result.valid);
}

#[tokio::test]
async fn semantic_multiple_aggregates_same_table() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT MIN(total_amount), MAX(total_amount), AVG(total_amount), SUM(total_amount) \
         FROM orders",
        &schema,
    )
    .await;
    assert!(result.valid);
    assert!(result.semantic_score > 0.8);
}

#[tokio::test]
async fn semantic_filter_pushdown_suggestion() {
    let schema = ecommerce_schema();
    // Filter that could be pushed down
    let result = check_semantics(
        "SELECT u.id, o.total_amount \
         FROM users u JOIN orders o ON u.id = o.user_id \
         WHERE u.status = 'active'",
        &schema,
    )
    .await;
    assert!(result.valid);
    // The filter_order rule may or may not fire depending on DataFusion's optimization
}

#[tokio::test]
async fn semantic_select_star_from_join() {
    let schema = ecommerce_schema();
    let result = check_semantics(
        "SELECT * FROM users u JOIN orders o ON u.id = o.user_id",
        &schema,
    )
    .await;
    assert!(result.valid);
}
