//! Detect SELECT without LIMIT clause.

use crate::tagger::types::TagResult;
use crate::tagger::walk::deep_dfs;
use polyglot_sql::expressions::Expression;

pub const TAG_NAME: &str = "select_without_limit";

/// Search the entire expression tree for any LIMIT clause.
fn has_limit_anywhere(expr: &Expression) -> bool {
    let mut found = false;
    deep_dfs(expr, &mut |e| {
        if found {
            return;
        }
        match e {
            Expression::Select(sel) => {
                if sel.limit.is_some() || sel.fetch.is_some() {
                    found = true;
                }
            }
            Expression::Union(u) => {
                if u.limit.is_some() {
                    found = true;
                }
            }
            Expression::Intersect(i) => {
                if i.limit.is_some() {
                    found = true;
                }
            }
            Expression::Except(e) => {
                if e.limit.is_some() {
                    found = true;
                }
            }
            Expression::Subquery(s) => {
                if s.limit.is_some() {
                    found = true;
                }
            }
            _ => {}
        }
    });
    found
}

pub fn check(expr: &Expression) -> TagResult {
    // Only trigger for SELECT-like statements
    match expr {
        Expression::Select(_)
        | Expression::Union(_)
        | Expression::Intersect(_)
        | Expression::Except(_) => {}
        _ => return TagResult::not_triggered(TAG_NAME),
    }

    if has_limit_anywhere(expr) {
        TagResult::not_triggered(TAG_NAME)
    } else {
        TagResult {
            tag_name: TAG_NAME.to_string(),
            triggered: true,
            reports: Vec::new(),
        }
    }
}

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

    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_without_limit() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one("SELECT * FROM t", DialectType::Snowflake).unwrap();
            let r = check(&expr);
            assert!(r.triggered);
        });
    }

    #[test]
    fn not_triggered_with_limit() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one("SELECT * FROM t LIMIT 10", DialectType::Snowflake)
                .unwrap();
            let r = check(&expr);
            assert!(!r.triggered);
        });
    }

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

    #[test]
    fn not_triggered_with_subquery_limit() {
        with_large_stack(|| {
            let expr = polyglot_sql::parse_one(
                "SELECT * FROM (SELECT * FROM t LIMIT 10)",
                DialectType::Snowflake,
            )
            .unwrap();
            let r = check(&expr);
            assert!(!r.triggered);
        });
    }
}
