mod defs;

use indexmap::IndexMap;

pub use defs::{
    ColumnDef, FunctionDef, IndexDef, IndexTarget, PolicyDef, TableDef, TriggerDef, TypeDef,
    TypeKind, ViewDef,
};

/// Schema-qualified object name. Bare names resolve to `("public", name)`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QualifiedName {
    /// Schema name, defaults to `"public"` for unqualified identifiers.
    pub schema: String,
    /// Object name within the schema.
    pub name: String,
}

impl QualifiedName {
    /// `None` schema resolves to `"public"`.
    #[must_use]
    pub fn new(schema: Option<&str>, name: &str) -> Self {
        Self {
            schema: schema.unwrap_or("public").to_owned(),
            name: name.to_owned(),
        }
    }

    /// Parse from the internal `"schema/name"` or `"name"` symbol string format.
    #[must_use]
    pub fn from_symbol_str(s: &str) -> Self {
        match s.split_once('/') {
            Some((schema, name)) => Self::new(Some(schema), name),
            None => Self::new(None, s),
        }
    }
}

impl std::fmt::Display for QualifiedName {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}/{}", self.schema, self.name)
    }
}

/// All schema objects extracted from a DDL pass over `schema.sql`.
#[derive(Debug, Default)]
pub struct SchemaModel {
    /// Tables keyed by qualified name.
    pub tables: IndexMap<QualifiedName, TableDef>,
    /// Functions keyed by qualified name.
    pub functions: IndexMap<QualifiedName, FunctionDef>,
    /// Views keyed by qualified name.
    pub views: IndexMap<QualifiedName, ViewDef>,
    /// User-defined types (enums and composites) keyed by qualified name.
    pub types: IndexMap<QualifiedName, TypeDef>,
    /// Triggers keyed by qualified name.
    pub triggers: IndexMap<QualifiedName, TriggerDef>,
    /// Row-level security policies keyed by qualified name.
    pub policies: IndexMap<QualifiedName, PolicyDef>,
    /// Indexes keyed by qualified name.
    pub indexes: IndexMap<QualifiedName, IndexDef>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use text_size::TextRange;

    #[test]
    fn qualified_name_defaults_schema_to_public() {
        let qn = QualifiedName::new(None, "users");
        assert_eq!(qn.schema, "public");
        assert_eq!(qn.name, "users");
    }

    #[test]
    fn qualified_name_uses_provided_schema() {
        let qn = QualifiedName::new(Some("myschema"), "orders");
        assert_eq!(qn.schema, "myschema");
        assert_eq!(qn.name, "orders");
    }

    #[test]
    fn qualified_name_hash_eq_consistent() {
        use std::collections::HashMap;
        let mut map: HashMap<QualifiedName, u32> = HashMap::new();
        let key = QualifiedName::new(Some("public"), "foo");
        map.insert(key.clone(), 42);
        assert_eq!(map.get(&key), Some(&42));
    }

    #[test]
    fn schema_model_default_is_empty() {
        let model = SchemaModel::default();
        assert!(model.tables.is_empty());
        assert!(model.functions.is_empty());
        assert!(model.views.is_empty());
        assert!(model.types.is_empty());
        assert!(model.triggers.is_empty());
        assert!(model.policies.is_empty());
        assert!(model.indexes.is_empty());
    }

    #[test]
    fn from_symbol_str_with_schema() {
        let qn = QualifiedName::from_symbol_str("public/users");
        assert_eq!(qn.schema, "public");
        assert_eq!(qn.name, "users");
    }

    #[test]
    fn from_symbol_str_bare_name_defaults_to_public() {
        let qn = QualifiedName::from_symbol_str("orders");
        assert_eq!(qn.schema, "public");
        assert_eq!(qn.name, "orders");
    }

    #[test]
    fn from_symbol_str_roundtrips_display() {
        let original = QualifiedName::new(Some("myschema"), "items");
        let display = original.to_string();
        let parsed = QualifiedName::from_symbol_str(&display);
        assert_eq!(parsed, original);
    }

    #[test]
    fn column_def_default_type_ref_is_none() {
        let col = ColumnDef {
            ty: "bigint".into(),
            nullable: false,
            range: TextRange::default(),
            doc: None,
            type_ref: None,
            fk_target: None,
            default_expr: None,
        };
        assert!(col.type_ref.is_none());
        assert!(col.fk_target.is_none());
    }

    #[test]
    fn function_def_default_return_type_is_none() {
        let func = FunctionDef {
            range: TextRange::default(),
            doc: None,
            return_type: None,
            param_types: Vec::new(),
            params: Vec::new(),
        };
        assert!(func.return_type.is_none());
        assert!(func.param_types.is_empty());
    }

    #[test]
    fn view_def_default_source_tables_is_empty() {
        let view = ViewDef {
            range: TextRange::default(),
            doc: None,
            source_tables: Vec::new(),
        };
        assert!(view.source_tables.is_empty());
    }

    #[test]
    fn column_def_display_doc_not_null_no_comment() {
        let col = ColumnDef {
            ty: "bigint".into(),
            nullable: false,
            range: TextRange::default(),
            doc: None,
            type_ref: None,
            fk_target: None,
            default_expr: None,
        };
        assert_eq!(col.display_doc(), "bigint NOT NULL");
    }

    #[test]
    fn column_def_display_doc_nullable_with_comment() {
        let col = ColumnDef {
            ty: "text".into(),
            nullable: true,
            range: TextRange::default(),
            doc: Some("the label".into()),
            type_ref: None,
            fk_target: None,
            default_expr: None,
        };
        assert_eq!(col.display_doc(), "text — the label");
    }

    #[test]
    fn column_def_display_doc_not_null_with_comment() {
        let col = ColumnDef {
            ty: "integer".into(),
            nullable: false,
            range: TextRange::default(),
            doc: Some("primary key".into()),
            type_ref: None,
            fk_target: None,
            default_expr: None,
        };
        assert_eq!(col.display_doc(), "integer NOT NULL — primary key");
    }
}
