use text_size::TextRange;

use super::util::parse_name;

use indexmap::IndexMap;

use crate::core::schema::{QualifiedName, SchemaModel, TypeDef, TypeKind};

use super::tables::{build_column_def, range_var_to_key};

pub(super) fn handle_create_enum(
    stmt: &pg_query::protobuf::CreateEnumStmt,
    range: TextRange,
    model: &mut SchemaModel,
) {
    let Some((schema, name)) = parse_name(&stmt.type_name) else {
        tracing::warn!("CreateEnumStmt has no parseable name, skipping");
        return;
    };
    let key = QualifiedName::new(schema.as_deref(), &name);
    let labels = extract_enum_labels(&stmt.vals, &key);
    model.types.insert(
        key,
        TypeDef {
            kind: TypeKind::Enum,
            range,
            doc: None,
            fields: IndexMap::new(),
            labels,
        },
    );
}

fn extract_enum_labels(vals: &[pg_query::protobuf::Node], owner: &QualifiedName) -> Vec<String> {
    use pg_query::NodeEnum;
    let mut labels = Vec::with_capacity(vals.len());
    for node in vals {
        match node.node.as_ref() {
            Some(NodeEnum::String(s)) => labels.push(s.sval.clone()),
            other => {
                tracing::warn!(
                    enum = %owner,
                    "CreateEnumStmt vals entry is not a String node ({other:?}), skipping label",
                );
            }
        }
    }
    labels
}

pub(super) fn handle_composite_type(
    stmt: &pg_query::protobuf::CompositeTypeStmt,
    range: TextRange,
    model: &mut SchemaModel,
) {
    let Some(typevar) = &stmt.typevar else {
        tracing::warn!("CompositeTypeStmt has no typevar, skipping");
        return;
    };
    let key = range_var_to_key(typevar);
    let fields = extract_composite_fields(&stmt.coldeflist, range.start());
    model.types.insert(
        key,
        TypeDef {
            kind: TypeKind::Composite,
            range,
            doc: None,
            fields,
            labels: Vec::new(),
        },
    );
}

fn extract_composite_fields(
    coldeflist: &[pg_query::protobuf::Node],
    stmt_start: text_size::TextSize,
) -> IndexMap<String, crate::core::schema::ColumnDef> {
    use pg_query::NodeEnum;
    let mut fields = IndexMap::new();
    for node in coldeflist {
        let Some(NodeEnum::ColumnDef(col)) = node.node.as_ref() else {
            continue;
        };
        let (name, col_def) = build_column_def(col, stmt_start);
        fields.insert(name, col_def);
    }
    fields
}

#[cfg(test)]
mod tests {
    use crate::{
        core::schema::{QualifiedName, SchemaModel, TypeKind},
        parse::split_and_parse,
    };

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

    #[test]
    fn create_enum_inserts_type_def() {
        let model = walk("CREATE TYPE public.mood AS ENUM ('happy', 'sad');");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "mood"))
            .expect("mood must exist");
        assert_eq!(td.kind, TypeKind::Enum);
    }

    #[test]
    fn create_enum_has_no_fields() {
        let model = walk("CREATE TYPE public.mood AS ENUM ('happy', 'sad');");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "mood"))
            .expect("mood must exist");
        assert!(td.fields.is_empty(), "enum types must have no fields");
    }

    #[test]
    fn create_enum_captures_labels_in_declaration_order() {
        let model = walk(
            "CREATE TYPE public.order_status AS ENUM ('pending','flagged','suspended','closed');",
        );
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "order_status"))
            .expect("order_status must exist");
        assert_eq!(
            td.labels,
            vec![
                "pending".to_owned(),
                "flagged".to_owned(),
                "suspended".to_owned(),
                "closed".to_owned(),
            ],
            "labels must preserve declaration order verbatim",
        );
    }

    #[test]
    fn create_enum_with_single_label() {
        let model = walk("CREATE TYPE public.singleton AS ENUM ('only');");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "singleton"))
            .expect("singleton must exist");
        assert_eq!(td.labels, vec!["only".to_owned()]);
    }

    #[test]
    fn composite_type_has_empty_labels() {
        let model = walk("CREATE TYPE public.p AS (x bigint, y text);");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "p"))
            .expect("p must exist");
        assert!(
            td.labels.is_empty(),
            "composite types must have no labels; got {:?}",
            td.labels,
        );
    }

    #[test]
    fn create_composite_inserts_type_def() {
        let model =
            walk("CREATE TYPE public.point_2d AS (x double precision, y double precision);");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "point_2d"))
            .expect("point_2d must exist");
        assert_eq!(td.kind, TypeKind::Composite);
    }

    #[test]
    fn composite_type_fields_are_populated() {
        let model = walk("CREATE TYPE public.p AS (x bigint, y text);");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "p"))
            .expect("p must exist");
        assert!(td.fields.contains_key("x"), "field x must be present");
        assert!(td.fields.contains_key("y"), "field y must be present");
        assert_eq!(td.fields.len(), 2);
    }

    #[test]
    fn composite_field_range_is_nonzero() {
        let model = walk("CREATE TYPE public.p AS (x bigint, y text);");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "p"))
            .expect("p must exist");
        let x = &td.fields["x"];
        assert!(
            x.range.start() > text_size::TextSize::from(0u32),
            "composite field x range start must reflect actual source position, got {:?}",
            x.range,
        );
    }

    #[test]
    fn composite_field_with_user_type_sets_type_ref() {
        let model = walk(concat!(
            "CREATE TYPE public.my_status AS ENUM ('active', 'inactive');",
            "CREATE TYPE public.my_comp AS (id bigint, status public.my_status);",
        ));
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "my_comp"))
            .expect("my_comp must exist");
        let status_field = &td.fields["status"];
        let tr = status_field
            .type_ref
            .as_ref()
            .expect("status field must have a type_ref");
        assert_eq!(tr.schema, "public");
        assert_eq!(tr.name, "my_status");
    }

    #[test]
    fn composite_field_pg_catalog_type_has_no_type_ref() {
        // bigint resolves through pg_catalog.int8 — type_name_to_qn returns None for pg_catalog.
        let model = walk("CREATE TYPE public.p AS (x bigint, y bigint);");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "p"))
            .expect("p must exist");
        assert!(
            td.fields["x"].type_ref.is_none(),
            "pg_catalog type must have no type_ref; got {:?}",
            td.fields["x"].type_ref,
        );
    }

    #[test]
    fn composite_field_default_expr_is_none_when_absent() {
        // Postgres rejects DEFAULT in CREATE TYPE bodies — default_expr always None for composite fields
        let model = walk("CREATE TYPE public.p AS (id bigint, ts timestamptz);");
        let td = model
            .types
            .get(&QualifiedName::new(Some("public"), "p"))
            .expect("p must exist");
        assert_eq!(td.fields["id"].default_expr, None);
        assert_eq!(td.fields["ts"].default_expr, None);
    }
}
