//! Detect functions applied to columns in JOIN ON conditions.
//!
//! Functions in JOIN conditions prevent index-based join strategies.

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, Join};

pub const TAG_NAME: &str = "join_has_func";

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

    let mut reports = Vec::new();
    for parent in join_parents {
        match parent {
            Expression::Select(sel) => {
                for join in &sel.joins {
                    check_join(join, dialect, &mut reports);
                }
            }
            Expression::Delete(del) => {
                for join in &del.joins {
                    check_join(join, dialect, &mut reports);
                }
            }
            Expression::Update(upd) => {
                for join in &upd.from_joins {
                    check_join(join, dialect, &mut reports);
                }
                for join in &upd.table_joins {
                    check_join(join, dialect, &mut reports);
                }
            }
            _ => unreachable!(),
        }
    }

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

fn check_join(join: &Join, dialect: DialectType, reports: &mut Vec<TagReport>) {
    let on_expr = match &join.on {
        Some(on) => on,
        None => return,
    };

    let col_funcs = find_column_functions(on_expr);
    if col_funcs.is_empty() {
        return;
    }

    let query_span = on_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_in_join() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT * FROM t1 JOIN t2 ON LOWER(t1.a) = t2.a",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(r.triggered);
            assert_eq!(r.reports.len(), 1);
        });
    }

    #[test]
    fn not_triggered_without_function() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT * FROM t1 JOIN t2 ON t1.a = t2.a",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr, DialectType::Snowflake);
            assert!(!r.triggered);
        });
    }
}
