//! Comprehensive tests for the M2 query explainer module.
//!
//! Tests cover: plan walking, lineage extraction, cost analysis,
//! optimization suggestions, integration with explain(), CLI explain,
//! snapshot tests, and edge cases.

use altimate_core::explainer::engine::explain;
use altimate_core::explainer::types::*;
use altimate_core::schema::types::SchemaDefinition;
use std::path::Path;

// ─── Helpers ──────────────────────────────────────────────────────────

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

fn load_fixture(path: &str) -> String {
    std::fs::read_to_string(
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../")
            .join(path),
    )
    .unwrap_or_else(|_| panic!("Failed to load fixture: {}", path))
}

/// Helper to check if a list of PlanSteps contains a given operation type.
fn has_operation(steps: &[PlanStep], predicate: &dyn Fn(&PlanOperation) -> bool) -> bool {
    steps.iter().any(|s| {
        if predicate(&s.operation) {
            return true;
        }
        // Also check inner_steps of subqueries
        if let PlanOperation::Subquery { inner_steps, .. } = &s.operation {
            return has_operation(inner_steps, predicate);
        }
        false
    })
}

// ─── Plan Walker Unit Tests ──────────────────────────────────────────

#[tokio::test]
async fn walker_simple_select_has_table_scan_and_projection() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id, email FROM users", &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have a TableScan for 'users'
    assert!(
        has_operation(
            steps,
            &|op| matches!(op, PlanOperation::TableScan { table, .. } if table == "users")
        ),
        "Expected TableScan for 'users', got steps: {:?}",
        steps
    );

    // Must have a Projection
    assert!(
        has_operation(steps, &|op| matches!(op, PlanOperation::Projection { .. })),
        "Expected Projection step, got steps: {:?}",
        steps
    );
}

#[tokio::test]
async fn walker_select_with_where_has_filter() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id, name FROM users WHERE age > 25", &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have TableScan
    assert!(has_operation(steps, &|op| matches!(
        op,
        PlanOperation::TableScan { .. }
    )));

    // Must have Filter
    assert!(
        has_operation(steps, &|op| matches!(op, PlanOperation::Filter { .. })),
        "Expected Filter step for WHERE clause, got steps: {:?}",
        steps
    );

    // Must have Projection
    assert!(has_operation(steps, &|op| matches!(
        op,
        PlanOperation::Projection { .. }
    )));
}

#[tokio::test]
async fn walker_join_has_multiple_table_scans_and_join_step() {
    let schema = ecommerce_schema();
    let sql = "SELECT u.name, o.total_amount FROM users u JOIN orders o ON u.id = o.user_id";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have TableScan for 'users'
    assert!(has_operation(steps, &|op| matches!(
        op,
        PlanOperation::TableScan { table, .. } if table == "users"
    )));

    // Must have TableScan for 'orders'
    assert!(has_operation(steps, &|op| matches!(
        op,
        PlanOperation::TableScan { table, .. } if table == "orders"
    )));

    // Must have a Join step with Inner kind
    let has_join = has_operation(steps, &|op| {
        matches!(
            op,
            PlanOperation::Join {
                join_type: JoinKind::Inner,
                ..
            }
        )
    });
    assert!(has_join, "Expected INNER JOIN step, got steps: {:?}", steps);
}

#[tokio::test]
async fn walker_group_by_with_aggregation() {
    let schema = ecommerce_schema();
    let sql = "SELECT u.name, COUNT(o.id) AS cnt FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have Aggregate step with group_by and functions
    let has_agg = has_operation(steps, &|op| {
        if let PlanOperation::Aggregate {
            group_by,
            functions,
        } = op
        {
            !group_by.is_empty() && !functions.is_empty()
        } else {
            false
        }
    });
    assert!(
        has_agg,
        "Expected Aggregate step with group_by and functions, got steps: {:?}",
        steps
    );
}

#[tokio::test]
async fn walker_order_by_has_sort_step() {
    let schema = ecommerce_schema();
    let sql = "SELECT id, name FROM users ORDER BY name ASC";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have Sort step
    let has_sort = has_operation(steps, &|op| {
        if let PlanOperation::Sort { order_by } = op {
            !order_by.is_empty() && order_by.iter().any(|k| k.direction == SortDirection::Asc)
        } else {
            false
        }
    });
    assert!(
        has_sort,
        "Expected Sort step with ASC, got steps: {:?}",
        steps
    );
}

#[tokio::test]
async fn walker_limit_has_correct_count() {
    let schema = ecommerce_schema();
    let sql = "SELECT * FROM users LIMIT 10";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have Limit step with limit=10
    let has_limit = has_operation(steps, &|op| {
        matches!(
            op,
            PlanOperation::Limit {
                limit: Some(10),
                ..
            }
        )
    });
    assert!(
        has_limit,
        "Expected Limit step with limit=10, got steps: {:?}",
        steps
    );
}

#[tokio::test]
async fn walker_window_function_has_window_step() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/complex/window_function.sql");
    let result = explain(&sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // Must have Window step
    assert!(
        has_operation(steps, &|op| matches!(op, PlanOperation::Window { .. })),
        "Expected Window step for window function, got steps: {:?}",
        steps
    );
}

#[tokio::test]
async fn walker_subquery_alias_has_subquery_step() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/cte.sql");
    let result = explain(&sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();
    let steps = &explanation.plan_steps;

    // CTE produces a SubqueryAlias node
    let has_subquery = has_operation(steps, &|op| {
        if let PlanOperation::Subquery { alias, inner_steps } = op {
            !alias.is_empty() && !inner_steps.is_empty()
        } else {
            false
        }
    });
    assert!(
        has_subquery,
        "Expected Subquery step for CTE, got steps: {:?}",
        steps
    );
}

// ─── Lineage Tests ──────────────────────────────────────────────────

#[tokio::test]
async fn lineage_simple_select_tables_and_columns() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id, email FROM users", &schema).await;
    assert!(result.valid);
    let lineage = &result.explanation.unwrap().lineage;

    // Tables
    assert!(
        lineage.tables.contains(&"users".to_string()),
        "Expected 'users' in tables, got: {:?}",
        lineage.tables
    );
    assert_eq!(lineage.tables.len(), 1);

    // Columns read should include 'id' and 'email'
    let col_names: Vec<&str> = lineage
        .columns_read
        .iter()
        .map(|c| c.column.as_str())
        .collect();
    assert!(col_names.contains(&"id"), "Expected 'id' in columns_read");
    assert!(
        col_names.contains(&"email"),
        "Expected 'email' in columns_read"
    );
}

#[tokio::test]
async fn lineage_join_has_both_tables_and_join_graph() {
    let schema = ecommerce_schema();
    let sql = "SELECT u.name, o.total_amount FROM users u JOIN orders o ON u.id = o.user_id";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let lineage = &result.explanation.unwrap().lineage;

    // Both tables present
    assert!(lineage.tables.contains(&"users".to_string()));
    assert!(lineage.tables.contains(&"orders".to_string()));
    assert_eq!(lineage.tables.len(), 2);

    // Join graph should have an edge
    assert!(
        !lineage.join_graph.is_empty(),
        "Expected join_graph to be populated"
    );
    let edge = &lineage.join_graph[0];
    assert_eq!(edge.join_type, JoinKind::Inner);
}

#[tokio::test]
async fn lineage_select_star_outputs_all_columns() {
    let schema = ecommerce_schema();
    let result = explain("SELECT * FROM users", &schema).await;
    assert!(result.valid);
    let lineage = &result.explanation.unwrap().lineage;

    // users has 7 columns
    assert!(
        lineage.columns_output.len() >= 7,
        "SELECT * FROM users should output all user columns, got {}",
        lineage.columns_output.len()
    );
}

#[tokio::test]
async fn lineage_cte_tracks_source_tables() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/cte.sql");
    let result = explain(&sql, &schema).await;
    assert!(result.valid);
    let lineage = &result.explanation.unwrap().lineage;

    // CTE references 'users' and 'orders'
    assert!(
        lineage.tables.contains(&"users".to_string()),
        "Expected 'users' in lineage tables"
    );
    assert!(
        lineage.tables.contains(&"orders".to_string()),
        "Expected 'orders' in lineage tables"
    );
}

#[tokio::test]
async fn lineage_multi_join_all_tables_tracked() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/complex/multi_join.sql");
    let result = explain(&sql, &schema).await;
    assert!(result.valid);
    let lineage = &result.explanation.unwrap().lineage;

    // multi_join.sql joins users, orders, order_items, products
    assert!(lineage.tables.contains(&"users".to_string()));
    assert!(lineage.tables.contains(&"orders".to_string()));
    assert!(lineage.tables.contains(&"order_items".to_string()));
    assert!(lineage.tables.contains(&"products".to_string()));
    assert_eq!(lineage.tables.len(), 4);

    // Should have 3 join edges (3 JOINs)
    assert_eq!(
        lineage.join_graph.len(),
        3,
        "Expected 3 join edges, got {}",
        lineage.join_graph.len()
    );
}

// ─── Cost Analysis Tests ────────────────────────────────────────────

#[tokio::test]
async fn cost_simple_query_is_low_complexity() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id, email FROM users WHERE age > 25", &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    assert_eq!(
        cost.complexity,
        QueryComplexity::Low,
        "Simple query should be Low complexity, got {:?} (score={})",
        cost.complexity,
        cost.complexity_score
    );
    assert!(cost.complexity_score <= 2);
}

#[tokio::test]
async fn cost_multi_join_higher_complexity() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/complex/multi_join.sql");
    let result = explain(&sql, &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    // 3 JOINs => score at least 6 (3*2), plus no-limit => should be High or above
    assert!(
        cost.complexity_score >= 3,
        "Multi-join should have higher complexity score, got {}",
        cost.complexity_score
    );
    assert!(
        cost.join_count >= 3,
        "Expected >= 3 joins, got {}",
        cost.join_count
    );
}

#[tokio::test]
async fn cost_cross_join_query_is_valid_and_complex() {
    let schema = ecommerce_schema();
    let sql = "SELECT * FROM users CROSS JOIN orders";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let explanation = result.explanation.unwrap();

    // Cross join should reference both tables
    assert!(explanation.lineage.tables.contains(&"users".to_string()));
    assert!(explanation.lineage.tables.contains(&"orders".to_string()));

    // Should be multi-table and have some complexity
    assert!(
        explanation.cost_signals.table_count >= 2,
        "Expected >= 2 tables for cross join"
    );

    // If the plan walker detects cross join, verify the critical flag
    if explanation.cost_signals.has_cross_join {
        let has_critical = explanation
            .cost_signals
            .risk_flags
            .iter()
            .any(|f| f.severity == RiskSeverity::Critical);
        assert!(has_critical, "Expected Critical risk flag for CROSS JOIN");
    }
}

#[tokio::test]
async fn cost_aggregation_and_window_flags() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/complex/window_function.sql");
    let result = explain(&sql, &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    assert!(
        cost.has_window_function,
        "Expected has_window_function=true for window function query"
    );
}

#[tokio::test]
async fn cost_no_limit_multi_table_risk_flag() {
    let schema = ecommerce_schema();
    let sql = "SELECT u.name, o.total_amount FROM users u JOIN orders o ON u.id = o.user_id";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    assert!(!cost.has_limit, "No LIMIT was used");
    assert!(cost.table_count > 1);

    let has_no_limit_flag = cost.risk_flags.iter().any(|f| f.flag.contains("no_limit"));
    assert!(
        has_no_limit_flag,
        "Expected no_limit risk flag for multi-table query without LIMIT, got flags: {:?}",
        cost.risk_flags
    );
}

// ─── Suggestion Tests ───────────────────────────────────────────────

#[tokio::test]
async fn suggestion_add_limit_for_multi_table() {
    let schema = ecommerce_schema();
    let sql = "SELECT u.name, o.total_amount FROM users u JOIN orders o ON u.id = o.user_id";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let suggestions = &result.explanation.unwrap().suggestions;

    let has_add_limit = suggestions
        .iter()
        .any(|s| s.kind == SuggestionType::AddLimit);
    assert!(
        has_add_limit,
        "Expected AddLimit suggestion for multi-table query without LIMIT, got: {:?}",
        suggestions
    );
}

#[tokio::test]
async fn suggestion_cross_join_produces_relevant_suggestions() {
    let schema = ecommerce_schema();
    let sql = "SELECT * FROM users CROSS JOIN orders";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let suggestions = &result.explanation.unwrap().suggestions;

    // Cross join without filter/limit should produce at least AddFilter or AddLimit suggestions
    assert!(
        !suggestions.is_empty(),
        "Expected suggestions for unfiltered cross join, got none"
    );

    // Should suggest adding a limit (multi-table, no limit)
    let has_add_limit = suggestions
        .iter()
        .any(|s| s.kind == SuggestionType::AddLimit);
    assert!(
        has_add_limit,
        "Expected AddLimit suggestion for multi-table query without LIMIT"
    );
}

#[tokio::test]
async fn suggestion_sorted_by_priority() {
    let schema = ecommerce_schema();
    // Cross join without limit should produce multiple suggestions
    let sql = "SELECT * FROM users CROSS JOIN orders";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let suggestions = &result.explanation.unwrap().suggestions;

    if suggestions.len() >= 2 {
        // Verify that high-priority suggestions come first
        for window in suggestions.windows(2) {
            let current_rank = match window[0].priority {
                SuggestionPriority::High => 0,
                SuggestionPriority::Medium => 1,
                SuggestionPriority::Low => 2,
            };
            let next_rank = match window[1].priority {
                SuggestionPriority::High => 0,
                SuggestionPriority::Medium => 1,
                SuggestionPriority::Low => 2,
            };
            assert!(
                current_rank <= next_rank,
                "Suggestions not sorted by priority: {:?} should come before {:?}",
                window[0].priority,
                window[1].priority
            );
        }
    }
}

// ─── Integration Tests (explain on fixtures) ───────────────────────

#[tokio::test]
async fn integration_valid_simple_select() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/simple_select.sql");
    let result = explain(&sql, &schema).await;

    assert!(result.valid);
    assert!(result.explanation.is_some());
    let explanation = result.explanation.unwrap();
    assert!(has_operation(&explanation.plan_steps, &|op| matches!(
        op,
        PlanOperation::TableScan { table, .. } if table == "users"
    )));
    assert_eq!(result.validation.error_count, 0);
}

#[tokio::test]
async fn integration_valid_join() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/join.sql");
    let result = explain(&sql, &schema).await;

    assert!(result.valid);
    let explanation = result.explanation.unwrap();

    // Should have Join step
    assert!(has_operation(&explanation.plan_steps, &|op| matches!(
        op,
        PlanOperation::Join { .. }
    )));

    // Should have 2 tables in lineage
    assert_eq!(explanation.lineage.tables.len(), 2);
}

#[tokio::test]
async fn integration_valid_aggregation() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/aggregation.sql");
    let result = explain(&sql, &schema).await;

    assert!(result.valid);
    let explanation = result.explanation.unwrap();

    // Should have Aggregate step
    assert!(has_operation(&explanation.plan_steps, &|op| matches!(
        op,
        PlanOperation::Aggregate { .. }
    )));
    assert!(explanation.cost_signals.has_aggregation);
}

#[tokio::test]
async fn integration_valid_cte() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/cte.sql");
    let result = explain(&sql, &schema).await;

    assert!(result.valid);
    let explanation = result.explanation.unwrap();

    // Should have Subquery step
    assert!(has_operation(&explanation.plan_steps, &|op| matches!(
        op,
        PlanOperation::Subquery { .. }
    )));
}

#[tokio::test]
async fn integration_valid_subquery() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/subquery.sql");
    let result = explain(&sql, &schema).await;

    assert!(result.valid);
    assert!(result.explanation.is_some());
}

#[tokio::test]
async fn integration_invalid_table_returns_no_explanation() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/invalid_table/nonexistent.sql");
    let result = explain(&sql, &schema).await;

    assert!(!result.valid);
    assert!(
        result.explanation.is_none(),
        "Invalid query should have no explanation"
    );
    assert!(result.validation.error_count > 0);
}

#[tokio::test]
async fn integration_syntax_error_returns_no_explanation() {
    let schema = ecommerce_schema();
    let result = explain("SELECTZ * FROM users", &schema).await;

    assert!(!result.valid);
    assert!(
        result.explanation.is_none(),
        "Syntax error should have no explanation"
    );
    assert!(result.validation.error_count > 0);
}

// ─── Snapshot Tests ─────────────────────────────────────────────────

#[tokio::test]
async fn snapshot_explain_simple_select() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id, email FROM users WHERE age > 25", &schema).await;
    // Snapshot core structure (excluding timing)
    let snapshot = serde_json::json!({
        "valid": result.valid,
        "has_explanation": result.explanation.is_some(),
        "table_count": result.explanation.as_ref().map(|e| e.lineage.tables.len()),
        "step_count": result.explanation.as_ref().map(|e| e.plan_steps.len()),
        "complexity": result.explanation.as_ref().map(|e| format!("{}", e.cost_signals.complexity)),
        "suggestion_count": result.explanation.as_ref().map(|e| e.suggestions.len()),
    });
    insta::assert_yaml_snapshot!("explain_simple_select", snapshot);
}

#[tokio::test]
async fn snapshot_explain_complex_join() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/complex/multi_join.sql");
    let result = explain(&sql, &schema).await;
    let snapshot = serde_json::json!({
        "valid": result.valid,
        "has_explanation": result.explanation.is_some(),
        "table_count": result.explanation.as_ref().map(|e| e.lineage.tables.len()),
        "join_count": result.explanation.as_ref().map(|e| e.cost_signals.join_count),
        "complexity": result.explanation.as_ref().map(|e| format!("{}", e.cost_signals.complexity)),
    });
    insta::assert_yaml_snapshot!("explain_complex_join", snapshot);
}

#[tokio::test]
async fn snapshot_explain_aggregation() {
    let schema = ecommerce_schema();
    let sql = load_fixture("tests/fixtures/queries/valid/aggregation.sql");
    let result = explain(&sql, &schema).await;
    let snapshot = serde_json::json!({
        "valid": result.valid,
        "has_aggregation": result.explanation.as_ref().map(|e| e.cost_signals.has_aggregation),
        "has_limit": result.explanation.as_ref().map(|e| e.cost_signals.has_limit),
        "complexity": result.explanation.as_ref().map(|e| format!("{}", e.cost_signals.complexity)),
    });
    insta::assert_yaml_snapshot!("explain_aggregation", snapshot);
}

// ─── Edge Case Tests ────────────────────────────────────────────────

#[tokio::test]
async fn edge_case_select_constant_no_table() {
    let schema = ecommerce_schema();
    let result = explain("SELECT 1", &schema).await;

    assert!(result.valid);
    let explanation = result.explanation.unwrap();

    // Should have EmptyRelation or no TableScan
    let has_table_scan = has_operation(&explanation.plan_steps, &|op| {
        matches!(op, PlanOperation::TableScan { .. })
    });
    assert!(
        !has_table_scan
            || has_operation(&explanation.plan_steps, &|op| matches!(
                op,
                PlanOperation::EmptyRelation
            )),
        "SELECT 1 should not have a TableScan or should have EmptyRelation"
    );
}

#[tokio::test]
async fn edge_case_many_joins_does_not_crash() {
    let schema = ecommerce_schema();
    // Join all 4 tables together
    let sql = r#"
        SELECT u.name, o.total_amount, 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 o.status = 'completed'
        ORDER BY o.total_amount DESC
        LIMIT 100
    "#;
    let result = explain(sql, &schema).await;

    assert!(result.valid, "Multi-join query should be valid");
    let explanation = result.explanation.unwrap();
    assert!(
        explanation.cost_signals.join_count >= 3,
        "Expected >= 3 joins"
    );
    assert!(
        explanation.plan_steps.len() >= 4,
        "Expected at least 4 plan steps"
    );
}

#[tokio::test]
async fn edge_case_union_query() {
    let schema = ecommerce_schema();
    let sql = "SELECT name FROM users UNION ALL SELECT name FROM products";
    let result = explain(sql, &schema).await;

    assert!(result.valid);
    let explanation = result.explanation.unwrap();

    // Should have Union step
    assert!(
        has_operation(&explanation.plan_steps, &|op| matches!(
            op,
            PlanOperation::Union { .. }
        )),
        "Expected Union step, got: {:?}",
        explanation.plan_steps
    );
    assert!(explanation.cost_signals.has_union);
}

// ─── Metadata Tests ─────────────────────────────────────────────────

#[tokio::test]
async fn metadata_dialect_is_generic() {
    let schema = ecommerce_schema();
    let result = explain("SELECT 1", &schema).await;

    assert_eq!(
        result.metadata.dialect, "generic",
        "Expected generic dialect"
    );
}

#[tokio::test]
async fn metadata_plan_node_count_positive_for_valid() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id FROM users", &schema).await;

    assert!(result.valid);
    assert!(
        result.metadata.plan_node_count > 0,
        "Expected positive plan_node_count for valid query, got {}",
        result.metadata.plan_node_count
    );
}

#[tokio::test]
async fn metadata_plan_node_count_zero_for_invalid() {
    let schema = ecommerce_schema();
    let result = explain("SELECTZ blah", &schema).await;

    assert!(!result.valid);
    assert_eq!(
        result.metadata.plan_node_count, 0,
        "Expected 0 plan_node_count for invalid query"
    );
}

// ─── ValidationSummary Tests ────────────────────────────────────────

#[tokio::test]
async fn validation_summary_valid_query() {
    let schema = ecommerce_schema();
    let result = explain("SELECT * FROM users", &schema).await;

    assert_eq!(result.validation.error_count, 0);
    assert!(result.validation.errors.is_empty());
}

#[tokio::test]
async fn validation_summary_invalid_query_has_errors() {
    let schema = ecommerce_schema();
    let result = explain("SELECT * FROM nonexistent", &schema).await;

    assert!(result.validation.error_count > 0);
    assert!(!result.validation.errors.is_empty());
}

// ─── Summary String Tests ───────────────────────────────────────────

#[tokio::test]
async fn summary_contains_query_type() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id FROM users", &schema).await;
    assert!(result.valid);
    let summary = &result.explanation.unwrap().summary;

    assert!(
        summary.contains("query") || summary.contains("SELECT"),
        "Summary should mention query type, got: '{}'",
        summary
    );
}

#[tokio::test]
async fn summary_aggregate_query_mentioned() {
    let schema = ecommerce_schema();
    let sql = "SELECT COUNT(*) FROM users";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let summary = &result.explanation.unwrap().summary;

    assert!(
        summary.contains("aggregate"),
        "Aggregate query summary should mention 'aggregate', got: '{}'",
        summary
    );
}

#[tokio::test]
async fn summary_mentions_table_name() {
    let schema = ecommerce_schema();
    let result = explain("SELECT * FROM users", &schema).await;
    assert!(result.valid);
    let summary = &result.explanation.unwrap().summary;

    assert!(
        summary.contains("users"),
        "Summary should mention table name 'users', got: '{}'",
        summary
    );
}

// ─── ExplanationResult Serialization Tests ──────────────────────────

#[tokio::test]
async fn explanation_result_serializes_to_json() {
    let schema = ecommerce_schema();
    let result = explain("SELECT id FROM users", &schema).await;

    let json_str = serde_json::to_string(&result).expect("Should serialize to JSON");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("Should parse back as valid JSON");

    assert_eq!(parsed["valid"], true);
    assert!(parsed["explanation"].is_object());
    assert!(parsed["metadata"].is_object());
}

#[tokio::test]
async fn explanation_result_invalid_has_no_explanation_field() {
    let schema = ecommerce_schema();
    let result = explain("SELECT * FROM nonexistent", &schema).await;

    let json_str = serde_json::to_string(&result).expect("Should serialize to JSON");
    let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();

    assert_eq!(parsed["valid"], false);
    // explanation should be absent (skip_serializing_if = "Option::is_none")
    assert!(
        parsed.get("explanation").is_none() || parsed["explanation"].is_null(),
        "Invalid result should not have explanation field"
    );
}

// ─── Cost Score Formula Tests ───────────────────────────────────────

#[tokio::test]
async fn cost_aggregation_adds_to_score() {
    let schema = ecommerce_schema();
    let result = explain("SELECT COUNT(*) FROM users", &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    assert!(cost.has_aggregation);
    // Aggregation adds 1 to score
    assert!(
        cost.complexity_score >= 1,
        "Aggregation should add to score, got {}",
        cost.complexity_score
    );
}

#[tokio::test]
async fn cost_limit_prevents_no_limit_penalty() {
    let schema = ecommerce_schema();
    let sql = "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id LIMIT 10";
    let result = explain(sql, &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    assert!(cost.has_limit);
    // No no_limit_multi_table flag when LIMIT is present
    let has_no_limit_flag = cost.risk_flags.iter().any(|f| f.flag.contains("no_limit"));
    assert!(
        !has_no_limit_flag,
        "Should NOT have no_limit flag when LIMIT is present"
    );
}

// ─── Step Numbering Tests ───────────────────────────────────────────

#[tokio::test]
async fn steps_are_sequentially_numbered() {
    let schema = ecommerce_schema();
    // Use a simple query without subqueries for predictable numbering
    let result = explain("SELECT id, name FROM users WHERE age > 25", &schema).await;
    assert!(result.valid);
    let steps = &result.explanation.unwrap().plan_steps;

    // Collect top-level step numbers
    let nums: Vec<usize> = steps.iter().map(|s| s.step).collect();
    assert!(!nums.is_empty(), "Should have at least one step");

    // Step numbers should be monotonically increasing
    for window in nums.windows(2) {
        assert!(
            window[1] > window[0],
            "Step numbers should be increasing: got {} followed by {}",
            window[0],
            window[1]
        );
    }

    // First step should start at 1
    assert_eq!(
        nums[0], 1,
        "First step should be numbered 1, got {}",
        nums[0]
    );
}

// ─── Distinct Test ──────────────────────────────────────────────────

#[tokio::test]
async fn distinct_detected_in_cost_signals() {
    let schema = ecommerce_schema();
    let result = explain("SELECT DISTINCT name FROM users", &schema).await;
    assert!(result.valid);
    let cost = &result.explanation.unwrap().cost_signals;

    assert!(cost.has_distinct, "Expected has_distinct=true");
}
