use psql_scip::{
    core::{ddl::walk_ddl, resolver::walk_references, schema::SchemaModel},
    parse::split_and_parse,
};

fn fixture_sql(name: &str) -> String {
    let base =
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo test");
    let path = std::path::PathBuf::from(base)
        .join("tests/fixtures")
        .join(name);
    std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("fixture {name} must be readable"))
}

fn schema_model() -> SchemaModel {
    let sql = fixture_sql("schema_simple.sql");
    let (stmts, _) = split_and_parse(&sql).expect("schema_simple.sql must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    model
}

fn refs_from_fixture(name: &str) -> Vec<psql_scip::core::resolver::ResolvedRef> {
    let model = schema_model();
    let sql = fixture_sql(name);
    let (stmts, _) = split_and_parse(&sql).expect("fixture must parse");
    walk_references(&stmts, &sql, &model)
}

#[test]
fn simple_select_emits_table_ref() {
    let refs = refs_from_fixture("ref_simple_select.sql");
    assert!(
        refs.iter().any(|r| r.symbol == "public/users"),
        "must contain a ref to public/users; got: {refs:?}"
    );
}

#[test]
fn column_ref_resolves_to_qualified_symbol() {
    let refs = refs_from_fixture("ref_simple_select.sql");
    assert!(
        refs.iter().any(|r| r.symbol == "public/users#email"),
        "must contain a column ref public/users#email; got: {refs:?}"
    );
}

#[test]
fn aliased_table_emits_schema_ref_not_alias() {
    let refs = refs_from_fixture("ref_table_alias.sql");
    assert!(
        refs.iter().any(|r| r.symbol == "public/orders"),
        "must emit schema ref public/orders, not alias; got: {refs:?}"
    );
    assert!(
        !refs.iter().any(|r| r.symbol == "public/o"),
        "must not emit a ref to the alias 'o'; got: {refs:?}"
    );
}

#[test]
fn aliased_column_resolves_through_alias() {
    let refs = refs_from_fixture("ref_table_alias.sql");
    assert!(
        refs.iter().any(|r| r.symbol == "public/orders#id"),
        "must resolve o.id to public/orders#id; got: {refs:?}"
    );
}

#[test]
fn cte_name_not_emitted_as_schema_ref() {
    let refs = refs_from_fixture("ref_cte.sql");
    assert!(
        !refs.iter().any(|r| r.symbol == "public/recent"),
        "CTE name 'recent' must not appear as a schema ref; got: {refs:?}"
    );
}

#[test]
fn cte_inner_table_ref_resolves() {
    let refs = refs_from_fixture("ref_cte.sql");
    assert!(
        refs.iter().any(|r| r.symbol == "public/users"),
        "FROM public.users inside CTE body must emit a ref; got: {refs:?}"
    );
}

#[test]
fn function_call_emits_ref() {
    let refs = refs_from_fixture("ref_function_call.sql");
    assert!(
        refs.iter().any(|r| r.symbol == "public/get_user"),
        "must emit ref to public/get_user; got: {refs:?}"
    );
}

#[test]
fn ambiguous_bare_col_produces_no_ref() {
    let refs = refs_from_fixture("ref_ambiguous_col.sql");
    let col_refs: Vec<_> = refs.iter().filter(|r| r.symbol.contains('#')).collect();
    assert!(
        col_refs.is_empty(),
        "ambiguous bare column must produce no column refs; got: {col_refs:?}"
    );
    let table_refs: Vec<_> = refs.iter().filter(|r| !r.symbol.contains('#')).collect();
    assert_eq!(
        table_refs.len(),
        2,
        "must produce exactly two table refs; got: {table_refs:?}"
    );
}

#[test]
fn unresolvable_table_skipped() {
    let model = schema_model();
    let (stmts, _) = split_and_parse("SELECT id FROM nonexistent_table;").expect("must parse");
    let refs = walk_references(&stmts, "SELECT id FROM nonexistent_table;", &model);
    assert!(
        refs.is_empty(),
        "unknown table must produce zero refs; got: {refs:?}"
    );
}

#[test]
fn refs_have_nonzero_ranges() {
    let refs = refs_from_fixture("ref_simple_select.sql");
    assert!(!refs.is_empty(), "must produce at least one ref");
    for r in &refs {
        assert!(
            r.range.start() < r.range.end(),
            "ref {} must have a nonzero range; got {:?}",
            r.symbol,
            r.range
        );
    }
}
