//! Detect functions applied to columns in WHERE clauses.
//!
//! Functions on columns in WHERE clauses prevent index usage, causing full table scans.

use crate::tagger::predicates::find_column_functions;
use crate::tagger::types::{TagReport, TagResult};
use crate::tagger::walk::deep_dfs;
use polyglot_sql::dialects::DialectType;
use polyglot_sql::expressions::{Expression, Where};

pub const TAG_NAME: &str = "filter_has_func";

pub fn check(expr: &Expression, dialect: DialectType) -> TagResult {
    let mut where_parents: Vec<&Expression> = Vec::new();
    deep_dfs(expr, &mut |e| match e {
        Expression::Select(sel) if sel.where_clause.is_some() => where_parents.push(e),
        Expression::Delete(del) if del.where_clause.is_some() => where_parents.push(e),
        Expression::Update(upd) if upd.where_clause.is_some() => where_parents.push(e),
        _ => {}
    });

    let mut reports = Vec::new();
    for parent in where_parents {
        let wc = match parent {
            Expression::Select(sel) => sel.where_clause.as_ref().unwrap(),
            Expression::Delete(del) => del.where_clause.as_ref().unwrap(),
            Expression::Update(upd) => upd.where_clause.as_ref().unwrap(),
            _ => unreachable!(),
        };
        check_where_clause(wc, dialect, &mut reports);
    }

    if reports.is_empty() {
        TagResult::not_triggered(TAG_NAME)
    } else {
        TagResult {
            tag_name: TAG_NAME.to_string(),
            triggered: true,
            reports,
        }
    }
}

fn check_where_clause(where_clause: &Where, dialect: DialectType, reports: &mut Vec<TagReport>) {
    let col_funcs = find_column_functions(&where_clause.this);
    if col_funcs.is_empty() {
        return;
    }

    let where_expr = Expression::Where(Box::new(where_clause.clone()));
    let query_span = where_expr.sql_for(dialect);
    if query_span.is_empty() {
        return;
    }

    let functions: Vec<String> = col_funcs
        .iter()
        .map(|f| f.sql_for(dialect))
        .filter(|s| !s.is_empty())
        .collect();

    if !functions.is_empty() {
        reports.push(TagReport {
            query_span,
            functions: Some(functions),
            description: None,
            metadata: None,
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn with_large_stack<F: FnOnce() + Send + 'static>(f: F) {
        std::thread::Builder::new()
            .stack_size(32 * 1024 * 1024)
            .spawn(f)
            .unwrap()
            .join()
            .unwrap();
    }

    #[test]
    fn triggered_with_function_on_column() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT x FROM t WHERE UPPER(name) = 'FOO'",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
            assert_eq!(r.reports.len(), 1);
            let funcs = r.reports[0].functions.as_ref().unwrap();
            assert!(funcs.iter().any(|f| f.contains("UPPER")));
        });
    }

    #[test]
    fn not_triggered_without_function() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT x FROM t WHERE name = 'FOO'",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(!r.triggered);
        });
    }

    #[test]
    fn not_triggered_with_function_on_literal() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT x FROM t WHERE UPPER('hello') = 'HELLO'",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(!r.triggered);
        });
    }

    #[test]
    fn triggered_in_delete_where() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "DELETE FROM t WHERE UPPER(name) = 'FOO'",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }

    #[test]
    fn triggered_in_update_where() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "UPDATE t SET x = 1 WHERE LOWER(name) = 'foo'",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }

    #[test]
    fn triggered_in_nested_subquery_where() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT * FROM t WHERE EXISTS (SELECT 1 FROM s WHERE UPPER(s.name) = t.name)",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }

    #[test]
    fn triggered_in_delete_with_exists_subquery() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "DELETE FROM t WHERE EXISTS (SELECT 1 FROM s WHERE UPPER(s.name) = t.name)",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
        });
    }
}
