//! Comprehensive tests for the SQL linter module.

use altimate_core::linter::lint;
use altimate_core::linter::types::LintSeverity;
use altimate_core::schema::types::{ColumnDef, SchemaDefinition, SqlDialect, TableDef};
use std::collections::HashMap;

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

fn empty_schema() -> SchemaDefinition {
    SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::Generic,
        tables: HashMap::new(),
        database: None,
        schema_name: None,
        glossary: None,
    }
}

fn typed_schema() -> SchemaDefinition {
    let mut tables = HashMap::new();
    tables.insert(
        "users".to_string(),
        TableDef {
            description: None,
            database: None,
            schema: None,
            columns: vec![
                col("id", "INT"),
                col("name", "VARCHAR"),
                col("email", "VARCHAR"),
                col("age", "INT"),
            ],
            primary_key: None,
            foreign_keys: vec![],
            statistics: None,

            clustering_keys: vec![],
        },
    );
    tables.insert(
        "orders".to_string(),
        TableDef {
            description: None,
            database: None,
            schema: None,
            columns: vec![
                col("id", "INT"),
                col("user_id", "VARCHAR"),
                col("total", "DECIMAL(10,2)"),
            ],
            primary_key: None,
            foreign_keys: vec![],
            statistics: None,

            clustering_keys: vec![],
        },
    );
    SchemaDefinition {
        version: "1".to_string(),
        dialect: SqlDialect::Generic,
        tables,
        database: None,
        schema_name: None,
        glossary: None,
    }
}

fn col(name: &str, dt: &str) -> ColumnDef {
    ColumnDef {
        name: name.to_string(),
        data_type: dt.to_string(),
        nullable: false,
        description: None,
        tags: vec![],
        synonyms: vec![],

        statistics: None,
    }
}

fn codes(result: &altimate_core::linter::types::LintResult) -> Vec<String> {
    result.findings.iter().map(|f| f.code.clone()).collect()
}

// ─── L001: SELECT * ──────────────────────────────────────────────

#[test]
fn test_l001_select_star_detected() {
    let r = lint("SELECT * FROM users", &empty_schema());
    assert!(codes(&r).contains(&"L001".to_string()));
    assert!(!r.clean);
}

#[test]
fn test_l001_no_star_clean() {
    let r = lint("SELECT id, name FROM users", &empty_schema());
    assert!(!codes(&r).contains(&"L001".to_string()));
}

#[test]
fn test_l001_star_in_subquery() {
    let r = lint("SELECT id FROM (SELECT * FROM users) sub", &empty_schema());
    assert!(codes(&r).contains(&"L001".to_string()));
}

#[test]
fn test_l001_severity_is_warning() {
    let r = lint("SELECT * FROM users", &empty_schema());
    let l001 = r.findings.iter().find(|f| f.code == "L001");
    assert!(l001.is_some());
    assert_eq!(l001.unwrap().severity, LintSeverity::Warning);
}

#[test]
fn test_l001_has_suggestion() {
    let r = lint("SELECT * FROM users", &empty_schema());
    let l001 = r.findings.iter().find(|f| f.code == "L001");
    assert!(l001.unwrap().suggestion.is_some());
}

// ─── L002: UPDATE without WHERE ──────────────────────────────────

#[test]
fn test_l002_update_no_where() {
    let r = lint("UPDATE users SET name = 'test'", &empty_schema());
    assert!(codes(&r).contains(&"L002".to_string()));
}

#[test]
fn test_l002_update_with_where_clean() {
    let r = lint(
        "UPDATE users SET name = 'test' WHERE id = 1",
        &empty_schema(),
    );
    assert!(!codes(&r).contains(&"L002".to_string()));
}

#[test]
fn test_l002_severity_is_error() {
    let r = lint("UPDATE users SET name = 'test'", &empty_schema());
    let l002 = r.findings.iter().find(|f| f.code == "L002");
    assert_eq!(l002.unwrap().severity, LintSeverity::Error);
}

// ─── L003: DELETE without WHERE ──────────────────────────────────

#[test]
fn test_l003_delete_no_where() {
    let r = lint("DELETE FROM users", &empty_schema());
    assert!(codes(&r).contains(&"L003".to_string()));
}

#[test]
fn test_l003_delete_with_where_clean() {
    let r = lint("DELETE FROM users WHERE id = 1", &empty_schema());
    assert!(!codes(&r).contains(&"L003".to_string()));
}

#[test]
fn test_l003_severity_is_error() {
    let r = lint("DELETE FROM users", &empty_schema());
    let l003 = r.findings.iter().find(|f| f.code == "L003");
    assert_eq!(l003.unwrap().severity, LintSeverity::Error);
}

// ─── L004: Cartesian product ─────────────────────────────────────

#[test]
fn test_l004_comma_join_no_where() {
    let r = lint("SELECT * FROM users, orders", &empty_schema());
    assert!(codes(&r).contains(&"L004".to_string()));
}

#[test]
fn test_l004_comma_join_with_where_clean() {
    let r = lint(
        "SELECT * FROM users, orders WHERE users.id = orders.user_id",
        &empty_schema(),
    );
    assert!(!codes(&r).contains(&"L004".to_string()));
}

#[test]
fn test_l004_severity_is_warning() {
    let r = lint("SELECT * FROM users, orders", &empty_schema());
    let l004 = r.findings.iter().find(|f| f.code == "L004");
    assert_eq!(l004.unwrap().severity, LintSeverity::Warning);
}

// ─── L005: SELECT DISTINCT with JOINs ───────────────────────────

#[test]
fn test_l005_distinct_with_join() {
    let r = lint(
        "SELECT DISTINCT u.name FROM users u JOIN orders o ON u.id = o.user_id",
        &empty_schema(),
    );
    assert!(codes(&r).contains(&"L005".to_string()));
}

#[test]
fn test_l005_distinct_without_join_clean() {
    let r = lint("SELECT DISTINCT name FROM users", &empty_schema());
    assert!(!codes(&r).contains(&"L005".to_string()));
}

#[test]
fn test_l005_severity_is_info() {
    let r = lint(
        "SELECT DISTINCT u.name FROM users u JOIN orders o ON u.id = o.user_id",
        &empty_schema(),
    );
    let l005 = r.findings.iter().find(|f| f.code == "L005");
    assert_eq!(l005.unwrap().severity, LintSeverity::Info);
}

// ─── L007: Implicit type coercion ────────────────────────────────

#[test]
fn test_l007_int_vs_varchar_coercion() {
    let s = typed_schema();
    let r = lint(
        "SELECT * FROM users u JOIN orders o ON u.id = o.user_id",
        &s,
    );
    assert!(codes(&r).contains(&"L007".to_string()));
}

#[test]
fn test_l007_same_type_clean() {
    let s = typed_schema();
    let r = lint("SELECT * FROM users u JOIN orders o ON u.id = o.id", &s);
    assert!(!codes(&r).contains(&"L007".to_string()));
}

#[test]
fn test_l007_severity_is_warning() {
    let s = typed_schema();
    let r = lint(
        "SELECT * FROM users u JOIN orders o ON u.id = o.user_id",
        &s,
    );
    let l007 = r.findings.iter().find(|f| f.code == "L007");
    assert_eq!(l007.unwrap().severity, LintSeverity::Warning);
}

// ─── L008: LIKE with leading wildcard ────────────────────────────

#[test]
fn test_l008_like_leading_percent() {
    let r = lint(
        "SELECT * FROM users WHERE name LIKE '%test'",
        &empty_schema(),
    );
    assert!(codes(&r).contains(&"L008".to_string()));
}

#[test]
fn test_l008_like_trailing_percent_clean() {
    let r = lint(
        "SELECT * FROM users WHERE name LIKE 'test%'",
        &empty_schema(),
    );
    assert!(!codes(&r).contains(&"L008".to_string()));
}

#[test]
fn test_l008_severity_is_warning() {
    let r = lint(
        "SELECT * FROM users WHERE name LIKE '%test'",
        &empty_schema(),
    );
    let l008 = r.findings.iter().find(|f| f.code == "L008");
    assert_eq!(l008.unwrap().severity, LintSeverity::Warning);
}

// ─── L009: NULL comparison with = ────────────────────────────────

#[test]
fn test_l009_equals_null() {
    let r = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
    assert!(codes(&r).contains(&"L009".to_string()));
}

#[test]
fn test_l009_not_equals_null() {
    let r = lint("SELECT * FROM users WHERE name != NULL", &empty_schema());
    assert!(codes(&r).contains(&"L009".to_string()));
}

#[test]
fn test_l009_is_null_clean() {
    let r = lint("SELECT * FROM users WHERE name IS NULL", &empty_schema());
    assert!(!codes(&r).contains(&"L009".to_string()));
}

#[test]
fn test_l009_is_not_null_clean() {
    let r = lint(
        "SELECT * FROM users WHERE name IS NOT NULL",
        &empty_schema(),
    );
    assert!(!codes(&r).contains(&"L009".to_string()));
}

#[test]
fn test_l009_severity_is_error() {
    let r = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
    let l009 = r.findings.iter().find(|f| f.code == "L009");
    assert_eq!(l009.unwrap().severity, LintSeverity::Error);
}

#[test]
fn test_l009_has_suggestion() {
    let r = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
    let l009 = r.findings.iter().find(|f| f.code == "L009");
    assert!(
        l009.unwrap()
            .suggestion
            .as_ref()
            .unwrap()
            .contains("IS NULL")
    );
}

// ─── L010: COUNT(column) vs COUNT(*) ─────────────────────────────

#[test]
fn test_l010_count_column() {
    let r = lint("SELECT COUNT(name) FROM users", &empty_schema());
    assert!(codes(&r).contains(&"L010".to_string()));
}

#[test]
fn test_l010_count_star_clean() {
    let r = lint("SELECT COUNT(*) FROM users", &empty_schema());
    assert!(!codes(&r).contains(&"L010".to_string()));
}

#[test]
fn test_l010_severity_is_info() {
    let r = lint("SELECT COUNT(name) FROM users", &empty_schema());
    let l010 = r.findings.iter().find(|f| f.code == "L010");
    assert_eq!(l010.unwrap().severity, LintSeverity::Info);
}

// ─── L011: DISTINCT with GROUP BY ────────────────────────────────

#[test]
fn test_l011_distinct_group_by() {
    let r = lint(
        "SELECT DISTINCT name FROM users GROUP BY name",
        &empty_schema(),
    );
    assert!(codes(&r).contains(&"L011".to_string()));
}

#[test]
fn test_l011_group_by_without_distinct_clean() {
    let r = lint("SELECT name FROM users GROUP BY name", &empty_schema());
    assert!(!codes(&r).contains(&"L011".to_string()));
}

#[test]
fn test_l011_severity_is_info() {
    let r = lint(
        "SELECT DISTINCT name FROM users GROUP BY name",
        &empty_schema(),
    );
    let l011 = r.findings.iter().find(|f| f.code == "L011");
    assert_eq!(l011.unwrap().severity, LintSeverity::Info);
}

// ─── L012: Missing table alias ───────────────────────────────────

#[test]
fn test_l012_multi_table_no_aliases() {
    let r = lint(
        "SELECT * FROM users JOIN orders ON users.id = orders.user_id",
        &empty_schema(),
    );
    assert!(codes(&r).contains(&"L012".to_string()));
}

#[test]
fn test_l012_multi_table_with_aliases_clean() {
    let r = lint(
        "SELECT * FROM users u JOIN orders o ON u.id = o.user_id",
        &empty_schema(),
    );
    assert!(!codes(&r).contains(&"L012".to_string()));
}

#[test]
fn test_l012_single_table_no_alias_clean() {
    let r = lint("SELECT * FROM users", &empty_schema());
    assert!(!codes(&r).contains(&"L012".to_string()));
}

#[test]
fn test_l012_severity_is_info() {
    let r = lint(
        "SELECT * FROM users JOIN orders ON users.id = orders.user_id",
        &empty_schema(),
    );
    let l012 = r.findings.iter().find(|f| f.code == "L012");
    assert_eq!(l012.unwrap().severity, LintSeverity::Info);
}

// ─── Clean queries ───────────────────────────────────────────────

#[test]
fn test_clean_simple_select() {
    let r = lint(
        "SELECT id, name FROM users WHERE id = 1 LIMIT 100",
        &empty_schema(),
    );
    assert!(r.clean);
    assert_eq!(r.error_count, 0);
    assert_eq!(r.warning_count, 0);
}

#[test]
fn test_clean_insert_with_values() {
    let r = lint("INSERT INTO users (name) VALUES ('test')", &empty_schema());
    assert!(r.clean);
}

#[test]
fn test_clean_update_with_where() {
    let r = lint("UPDATE users SET name = 'x' WHERE id = 1", &empty_schema());
    assert!(r.clean);
}

#[test]
fn test_clean_delete_with_where() {
    let r = lint("DELETE FROM users WHERE id = 1", &empty_schema());
    assert!(r.clean);
}

// ─── Multiple findings ──────────────────────────────────────────

#[test]
fn test_multiple_findings_in_one_query() {
    let r = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
    assert!(!r.clean);
    let c = codes(&r);
    assert!(c.contains(&"L001".to_string()));
    assert!(c.contains(&"L009".to_string()));
}

#[test]
fn test_error_and_warning_counts() {
    let r = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
    assert!(r.error_count > 0); // L009 is error
    assert!(r.warning_count > 0); // L001 is warning
}

// ─── Findings ordering ──────────────────────────────────────────

#[test]
fn test_findings_sorted_by_line() {
    let r = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
    for window in r.findings.windows(2) {
        let line_a = window[0].line.unwrap_or(usize::MAX);
        let line_b = window[1].line.unwrap_or(usize::MAX);
        assert!(line_a <= line_b);
    }
}

// ─── SQL preservation ────────────────────────────────────────────

#[test]
fn test_original_sql_preserved() {
    let sql = "SELECT * FROM users";
    let r = lint(sql, &empty_schema());
    assert_eq!(r.sql, sql);
}

// ─── Invalid SQL ─────────────────────────────────────────────────

#[test]
fn test_invalid_sql_no_panic() {
    let r = lint("THIS IS NOT SQL", &empty_schema());
    assert!(r.clean); // invalid SQL can't be parsed, no findings
}
