//! Batch 2: relationships, trigger/policy/index definitions.
use camino::Utf8PathBuf;
use psql_scip::{
    core::{ddl::walk_ddl, schema::SchemaModel},
    emit::emit_index,
    parse::split_and_parse,
};
use scip::types::symbol_information::Kind;

fn manifest_dir() -> Utf8PathBuf {
    let base =
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo test");
    Utf8PathBuf::from(base)
}

fn fixture_sql(name: &str) -> String {
    let path = manifest_dir().join("tests/fixtures").join(name);
    std::fs::read_to_string(path.as_std_path())
        .unwrap_or_else(|_| panic!("fixture {name} must be readable"))
}

fn build_batch2_index() -> scip::types::Index {
    let schema_path = manifest_dir().join("tests/fixtures/schema_batch2.sql");
    let schema_content = fixture_sql("schema_batch2.sql");
    let (stmts, failures) = split_and_parse(&schema_content).expect("schema must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    emit_index(
        &schema_path,
        &schema_content,
        &failures,
        &model,
        &[],
        &manifest_dir(),
    )
}

fn schema_doc(index: &scip::types::Index) -> &scip::types::Document {
    &index.documents[0]
}

fn find_sym<'a>(
    doc: &'a scip::types::Document,
    needle: &str,
) -> &'a scip::types::SymbolInformation {
    doc.symbols
        .iter()
        .find(|s| s.symbol.contains(needle))
        .unwrap_or_else(|| panic!("must find symbol containing '{needle}'"))
}

#[test]
fn schema_model_default_has_empty_triggers() {
    let model = SchemaModel::default();
    assert!(model.triggers.is_empty());
}

#[test]
fn schema_model_default_has_empty_policies() {
    let model = SchemaModel::default();
    assert!(model.policies.is_empty());
}

#[test]
fn schema_model_default_has_empty_indexes() {
    let model = SchemaModel::default();
    assert!(model.indexes.is_empty());
}

#[test]
fn walk_ddl_populates_trigger() {
    let sql = concat!(
        "CREATE TABLE public.items (id bigint NOT NULL);",
        "CREATE TRIGGER trg_items_audit AFTER INSERT ON public.items ",
        "FOR EACH ROW EXECUTE FUNCTION public.noop();",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let key = psql_scip::core::schema::QualifiedName::new(Some("public"), "trg_items_audit");
    let trig = model.triggers.get(&key).expect("trigger must be populated");
    assert_eq!(trig.target.schema, "public");
    assert_eq!(trig.target.name, "items");
}

#[test]
fn walk_ddl_populates_policy() {
    let sql = concat!(
        "CREATE TABLE public.items (id bigint NOT NULL);",
        "CREATE POLICY pol_items_user ON public.items USING (id > 0);",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let key = psql_scip::core::schema::QualifiedName::new(Some("public"), "pol_items_user");
    let pol = model.policies.get(&key).expect("policy must be populated");
    assert_eq!(pol.target.schema, "public");
    assert_eq!(pol.target.name, "items");
}

#[test]
fn walk_ddl_populates_index() {
    let sql = concat!(
        "CREATE TABLE public.items (id bigint NOT NULL, name text);",
        "CREATE INDEX idx_items_name ON public.items (name);",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let key = psql_scip::core::schema::QualifiedName::new(Some("public"), "idx_items_name");
    let idx = model.indexes.get(&key).expect("index must be populated");
    assert_eq!(idx.target.schema, "public");
    assert_eq!(idx.target.name, "items");
}

#[test]
fn column_with_enum_type_has_type_ref() {
    let sql = concat!(
        "CREATE TYPE public.item_color AS ENUM ('red', 'green');",
        "CREATE TABLE public.items (id bigint NOT NULL, color public.item_color NOT NULL);",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let table_key = psql_scip::core::schema::QualifiedName::new(Some("public"), "items");
    let col = &model.tables[&table_key].columns["color"];
    let type_ref = col
        .type_ref
        .as_ref()
        .expect("color column must have type_ref");
    assert_eq!(type_ref.schema, "public");
    assert_eq!(type_ref.name, "item_color");
}

#[test]
fn column_without_custom_type_has_no_type_ref() {
    let sql = "CREATE TABLE public.items (id bigint NOT NULL, name text);";
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let key = psql_scip::core::schema::QualifiedName::new(Some("public"), "items");
    // `id` has type bigint — pg_catalog prefix stripped → type_ref = None
    // (`text` skipped: no type entry in model, so emission gated regardless)
    let col = &model.tables[&key].columns["id"];
    assert!(
        col.type_ref.is_none(),
        "builtin column must not have type_ref"
    );
}

#[test]
fn column_with_inline_fk_has_fk_target() {
    let sql = concat!(
        "CREATE TABLE public.items (id bigint NOT NULL);",
        "CREATE TABLE public.orders (id bigint NOT NULL, item_id bigint NOT NULL REFERENCES public.items(id));",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let key = psql_scip::core::schema::QualifiedName::new(Some("public"), "orders");
    let col = &model.tables[&key].columns["item_id"];
    let (ref_table, ref_col) = col.fk_target.as_ref().expect("item_id must have fk_target");
    assert_eq!(ref_table.schema, "public");
    assert_eq!(ref_table.name, "items");
    assert_eq!(ref_col, "id");
}

#[test]
fn alter_table_add_fk_populates_fk_target() {
    let sql = concat!(
        "CREATE TABLE public.items (id bigint NOT NULL);",
        "CREATE TABLE public.orders (id bigint NOT NULL, item_id bigint NOT NULL);",
        "ALTER TABLE public.orders ADD CONSTRAINT orders_item_id_fkey FOREIGN KEY (item_id) REFERENCES public.items(id);",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let key = psql_scip::core::schema::QualifiedName::new(Some("public"), "orders");
    let col = &model.tables[&key].columns["item_id"];
    let (ref_table, ref_col) = col.fk_target.as_ref().expect("item_id must have fk_target");
    assert_eq!(ref_table.name, "items");
    assert_eq!(ref_col, "id");
}

#[test]
fn function_with_custom_return_type_has_return_type() {
    let sql = concat!(
        "CREATE TYPE public.item_color AS ENUM ('red', 'green');",
        "CREATE FUNCTION public.get_color(p bigint) RETURNS public.item_color LANGUAGE sql AS $$ SELECT 'red'::public.item_color $$;",
    );
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let fn_key = psql_scip::core::schema::QualifiedName::new(Some("public"), "get_color");
    let func = &model.functions[&fn_key];
    let rt = func
        .return_type
        .as_ref()
        .expect("get_color must have return_type");
    assert_eq!(rt.schema, "public");
    assert_eq!(rt.name, "item_color");
}

#[test]
fn function_with_builtin_return_type_has_no_return_type() {
    let sql = "CREATE FUNCTION public.f(p bigint) RETURNS bigint LANGUAGE sql AS $$ SELECT p $$;";
    let (stmts, _) = split_and_parse(sql).expect("must parse");
    let mut model = SchemaModel::default();
    walk_ddl(&stmts, &mut model);
    let fn_key = psql_scip::core::schema::QualifiedName::new(Some("public"), "f");
    let func = &model.functions[&fn_key];
    assert!(
        func.return_type.is_none(),
        "builtin return type must not set return_type"
    );
}

#[test]
fn trigger_symbol_has_kind_event() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let si = find_sym(doc, "trg_items_audit");
    assert_eq!(
        si.kind.enum_value(),
        Ok(Kind::Event),
        "trigger must have Kind::Event"
    );
}

#[test]
fn policy_symbol_has_kind_attribute() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let si = find_sym(doc, "pol_items_user");
    assert_eq!(
        si.kind.enum_value(),
        Ok(Kind::Attribute),
        "policy must have Kind::Attribute"
    );
}

#[test]
fn index_symbol_has_kind_property() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let si = find_sym(doc, "idx_items_name");
    assert_eq!(
        si.kind.enum_value(),
        Ok(Kind::Property),
        "index must have Kind::Property"
    );
}

#[test]
fn column_with_enum_type_has_type_definition_relationship() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let col_si = find_sym(doc, "public/items#color");
    let enum_sym = find_sym(doc, "item_color").symbol.clone();
    assert!(
        col_si
            .relationships
            .iter()
            .any(|r| r.symbol == enum_sym && r.is_type_definition),
        "color column must have is_type_definition relationship to item_color; got {:?}",
        col_si.relationships,
    );
}

#[test]
fn fk_column_has_reference_relationship() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let col_si = find_sym(doc, "public/orders#item_id");
    let items_id_sym = find_sym(doc, "public/items#id").symbol.clone();
    assert!(
        col_si
            .relationships
            .iter()
            .any(|r| r.symbol == items_id_sym && r.is_reference && r.is_type_definition),
        "item_id column must have is_reference+is_type_definition relationship to items#id; got {:?}",
        col_si.relationships,
    );
}

#[test]
fn function_with_custom_return_type_has_relationship() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let fn_si = find_sym(doc, "get_color");
    let enum_sym = find_sym(doc, "item_color").symbol.clone();
    assert!(
        fn_si
            .relationships
            .iter()
            .any(|r| r.symbol == enum_sym && r.is_type_definition),
        "get_color must have is_type_definition relationship to item_color; got {:?}",
        fn_si.relationships,
    );
}

#[test]
fn trigger_has_reference_relationship_to_table() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let trig_si = find_sym(doc, "trg_items_audit");
    let table_sym = find_sym(doc, "public/items.").symbol.clone();
    assert!(
        trig_si
            .relationships
            .iter()
            .any(|r| r.symbol == table_sym && r.is_reference),
        "trigger must have is_reference relationship to items table; got {:?}",
        trig_si.relationships,
    );
}

#[test]
fn policy_has_reference_relationship_to_table() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let pol_si = find_sym(doc, "pol_items_user");
    let table_sym = find_sym(doc, "public/items.").symbol.clone();
    assert!(
        pol_si
            .relationships
            .iter()
            .any(|r| r.symbol == table_sym && r.is_reference),
        "policy must have is_reference relationship to items table; got {:?}",
        pol_si.relationships,
    );
}

#[test]
fn index_has_reference_relationship_to_table() {
    let index = build_batch2_index();
    let doc = schema_doc(&index);
    let idx_si = find_sym(doc, "idx_items_name");
    let table_sym = find_sym(doc, "public/items.").symbol.clone();
    assert!(
        idx_si
            .relationships
            .iter()
            .any(|r| r.symbol == table_sym && r.is_reference),
        "index must have is_reference relationship to items table; got {:?}",
        idx_si.relationships,
    );
}
