use std::fmt::Write;

use anyhow::Result;
use serde::Serialize;

use crate::manifest::{
    BackendAdapterManifest, EntityFieldManifest, ExpectedSchemaManifest,
    SchemaAdditionalPropertiesManifest, SchemaDescriptorManifest, SchemaNodeManifest,
};

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct GeneratedSchemaFile<'a> {
    expected_schema: &'a ExpectedSchemaManifest,
}

pub fn export_expected_schema(manifest: &BackendAdapterManifest) -> Result<String> {
    Ok(format!(
        "{}\n",
        serde_json::to_string_pretty(&GeneratedSchemaFile {
            expected_schema: &manifest.database.expected_schema,
        })?
    ))
}

pub fn export_typescript_client_bindings(manifest: &BackendAdapterManifest) -> Result<String> {
    let expected_schema_json = serde_json::to_string_pretty(&manifest.database.expected_schema)?;
    let auth_endpoints_json = serde_json::to_string_pretty(&manifest.auth.rest.paths)?;
    let mut output = String::new();

    writeln!(&mut output, "// Generated by e2ee-backend-adapter. Do not edit manually.")?;
    writeln!(&mut output, "// Manifest: {}", manifest.name)?;
    writeln!(&mut output)?;
    writeln!(&mut output, "import {{")?;
    writeln!(&mut output, "  createGraphqlCrudAdapterFromExpectedSchemaEntity,")?;
    writeln!(&mut output, "  createGraphqlPasswordAuthConfigFromExpectedSchema,")?;
    writeln!(&mut output, "  createGraphqlTransportFromExpectedSchema,")?;
    writeln!(&mut output, "  createRestTransportFromExpectedSchema,")?;
    writeln!(&mut output, "  createEntitySchemaFromExpectedSchemaEntity,")?;
    writeln!(&mut output, "  createRestCrudAdapterFromExpectedSchemaEntity,")?;
    writeln!(&mut output, "  createRestPasswordAuthConfig,")?;
    writeln!(&mut output, "  defineClientModel,")?;
    writeln!(&mut output, "  type BackendAdapterExpectedSchemaManifest,")?;
    writeln!(&mut output, "  type ClientModelDefinition,")?;
    writeln!(&mut output, "  type CreateGeneratedGraphqlAuthOptions,")?;
    writeln!(&mut output, "  type CreateGeneratedGraphqlTransportOptions,")?;
    writeln!(&mut output, "  type CreateGeneratedRestTransportOptions,")?;
    writeln!(&mut output, "  type CreateGeneratedEntitySchemaOptions,")?;
    writeln!(&mut output, "  type EncryptedFieldValue,")?;
    writeln!(&mut output, "  type EntitySchema,")?;
    writeln!(&mut output, "  type GraphqlCrudAdapter,")?;
    writeln!(&mut output, "  type GraphqlPasswordAuthConfig,")?;
    writeln!(&mut output, "  type GraphqlTransport,")?;
    writeln!(&mut output, "  type FetchRestTransport,")?;
    writeln!(&mut output, "  type RestCrudAdapter,")?;
    writeln!(&mut output, "  type RestPasswordAuthConfig,")?;
    writeln!(&mut output, "  type RestTransport,")?;
    writeln!(&mut output, "}} from \"e2ee-client-backend\";")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export interface SessionUser {{")?;
    writeln!(&mut output, "  email: string;")?;
    writeln!(&mut output, "  id: string;")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    for entity in &manifest.database.expected_schema.entities {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "export type {alias_base}Entity = {};",
            typescript_object_type_for_fields(&entity.fields, false),
        )?;
        writeln!(
            &mut output,
            "export type {alias_base}RemoteRecord = {};",
            typescript_object_type_for_fields(&entity.fields, true),
        )?;
        writeln!(&mut output, "export type {alias_base}Id = {};", resolve_id_type(entity))?;
        writeln!(&mut output)?;
    }

    writeln!(
        &mut output,
        "export const expectedSchema = {expected_schema_json} as unknown as BackendAdapterExpectedSchemaManifest;",
    )?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export const restAuthEndpoints = {auth_endpoints_json} as const;")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export function createRestTransport(")?;
    writeln!(&mut output, "  options: CreateGeneratedRestTransportOptions = {{}},")?;
    writeln!(&mut output, "): FetchRestTransport {{")?;
    writeln!(&mut output, "  return createRestTransportFromExpectedSchema(expectedSchema, options);")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export function createRestAuthConfig(")?;
    writeln!(&mut output, "  transport: RestTransport = createRestTransport(),")?;
    writeln!(&mut output, "): RestPasswordAuthConfig<SessionUser> {{")?;
    writeln!(&mut output, "  return createRestPasswordAuthConfig<SessionUser>({{")?;
    writeln!(&mut output, "    endpoints: restAuthEndpoints,")?;
    writeln!(&mut output, "    transport,")?;
    writeln!(&mut output, "  }});")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export function createGraphqlTransport(")?;
    writeln!(&mut output, "  options: CreateGeneratedGraphqlTransportOptions = {{}},")?;
    writeln!(&mut output, "): GraphqlTransport {{")?;
    writeln!(&mut output, "  return createGraphqlTransportFromExpectedSchema(expectedSchema, options);")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export function createGraphqlAuthConfig(")?;
    writeln!(&mut output, "  transport: GraphqlTransport = createGraphqlTransport(),")?;
    writeln!(&mut output, "  options: CreateGeneratedGraphqlAuthOptions = {{}},")?;
    writeln!(&mut output, "): GraphqlPasswordAuthConfig<SessionUser> {{")?;
    writeln!(&mut output, "  return createGraphqlPasswordAuthConfigFromExpectedSchema<SessionUser>(expectedSchema, transport, options);")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export interface GeneratedSchemaOptions {{")?;
    for entity in &manifest.database.expected_schema.entities {
        writeln!(
            &mut output,
            "  {}?: CreateGeneratedEntitySchemaOptions;",
            typescript_property_name(&entity.name),
        )?;
    }
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export interface GeneratedEntitySchemas {{")?;
    for entity in &manifest.database.expected_schema.entities {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "  {}: EntitySchema<{}Entity, {}RemoteRecord, {}Id>;",
            typescript_property_name(&entity.name),
            alias_base,
            alias_base,
            alias_base,
        )?;
    }
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export function createEntitySchemas(")?;
    writeln!(&mut output, "  options: GeneratedSchemaOptions = {{}},")?;
    writeln!(&mut output, "): GeneratedEntitySchemas {{")?;
    writeln!(&mut output, "  return {{")?;
    for (index, entity) in manifest.database.expected_schema.entities.iter().enumerate() {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "    {}: createEntitySchemaFromExpectedSchemaEntity(",
            typescript_property_name(&entity.name),
        )?;
        writeln!(&mut output, "      expectedSchema.entities[{index}]!,")?;
        writeln!(
            &mut output,
            "      {},",
            typescript_member_access("options", &entity.name),
        )?;
        writeln!(
            &mut output,
            "    ) as unknown as EntitySchema<{}Entity, {}RemoteRecord, {}Id>,",
            alias_base,
            alias_base,
            alias_base,
        )?;
    }
    writeln!(&mut output, "  }};")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export interface GeneratedRestCrudAdapters {{")?;
    for entity in &manifest.database.expected_schema.entities {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "  {}: RestCrudAdapter<{}RemoteRecord, {}Id>;",
            typescript_property_name(&entity.name),
            alias_base,
            alias_base,
        )?;
    }
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export interface GeneratedGraphqlCrudAdapters {{")?;
    for entity in &manifest.database.expected_schema.entities {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "  {}: GraphqlCrudAdapter<{}RemoteRecord, {}Id>;",
            typescript_property_name(&entity.name),
            alias_base,
            alias_base,
        )?;
    }
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export interface CreateRestModelsOptions {{")?;
    writeln!(&mut output, "  schemaOptions?: GeneratedSchemaOptions;")?;
    writeln!(&mut output, "  transport?: RestTransport;")?;
    writeln!(&mut output, "  transportOptions?: CreateGeneratedRestTransportOptions;")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export interface CreateGraphqlModelsOptions {{")?;
    writeln!(&mut output, "  authOptions?: CreateGeneratedGraphqlAuthOptions;")?;
    writeln!(&mut output, "  schemaOptions?: GeneratedSchemaOptions;")?;
    writeln!(&mut output, "  transport?: GraphqlTransport;")?;
    writeln!(&mut output, "  transportOptions?: CreateGeneratedGraphqlTransportOptions;")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export interface GeneratedRestModels {{")?;
    for entity in &manifest.database.expected_schema.entities {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "  {}: ClientModelDefinition<EntitySchema<{}Entity, {}RemoteRecord, {}Id>>;",
            typescript_property_name(&entity.table_name),
            alias_base,
            alias_base,
            alias_base,
        )?;
    }
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export interface GeneratedGraphqlModels {{")?;
    for entity in &manifest.database.expected_schema.entities {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "  {}: ClientModelDefinition<EntitySchema<{}Entity, {}RemoteRecord, {}Id>>;",
            typescript_property_name(&entity.table_name),
            alias_base,
            alias_base,
            alias_base,
        )?;
    }
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export function createRestModels(")?;
    writeln!(&mut output, "  options: CreateRestModelsOptions = {{}},")?;
    writeln!(&mut output, "): GeneratedRestModels {{")?;
    writeln!(&mut output, "  const transport = options.transport ?? createRestTransport(options.transportOptions ?? {{}});")?;
    writeln!(&mut output, "  const schemas = createEntitySchemas(options.schemaOptions ?? {{}});")?;
    writeln!(&mut output, "  const adapters = createRestCrudAdapters(transport);")?;
    writeln!(&mut output, "  return {{")?;
    for entity in &manifest.database.expected_schema.entities {
        writeln!(
            &mut output,
            "    {}: defineClientModel({{",
            typescript_property_name(&entity.table_name),
        )?;
        writeln!(
            &mut output,
            "      adapter: {},",
            typescript_member_access("adapters", &entity.name),
        )?;
        writeln!(
            &mut output,
            "      schema: {},",
            typescript_member_access("schemas", &entity.name),
        )?;
        writeln!(&mut output, "    }}),")?;
    }
    writeln!(&mut output, "  }};")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export function createGraphqlModels(")?;
    writeln!(&mut output, "  options: CreateGraphqlModelsOptions = {{}},")?;
    writeln!(&mut output, "): GeneratedGraphqlModels {{")?;
    writeln!(&mut output, "  const transport = options.transport ?? createGraphqlTransport(options.transportOptions ?? {{}});")?;
    writeln!(&mut output, "  const schemas = createEntitySchemas(options.schemaOptions ?? {{}});")?;
    writeln!(&mut output, "  const adapters = createGraphqlCrudAdapters(transport);")?;
    writeln!(&mut output, "  return {{")?;
    for entity in &manifest.database.expected_schema.entities {
        writeln!(
            &mut output,
            "    {}: defineClientModel({{",
            typescript_property_name(&entity.table_name),
        )?;
        writeln!(
            &mut output,
            "      adapter: {},",
            typescript_member_access("adapters", &entity.name),
        )?;
        writeln!(
            &mut output,
            "      schema: {},",
            typescript_member_access("schemas", &entity.name),
        )?;
        writeln!(&mut output, "    }}),")?;
    }
    writeln!(&mut output, "  }};")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;

    writeln!(&mut output, "export function createRestCrudAdapters(")?;
    writeln!(&mut output, "  transport: RestTransport = createRestTransport(),")?;
    writeln!(&mut output, "): GeneratedRestCrudAdapters {{")?;
    writeln!(&mut output, "  return {{")?;
    for (index, entity) in manifest.database.expected_schema.entities.iter().enumerate() {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "    {}: createRestCrudAdapterFromExpectedSchemaEntity<{}RemoteRecord, {}Id>(",
            typescript_property_name(&entity.name),
            alias_base,
            alias_base,
        )?;
        writeln!(&mut output, "      expectedSchema.entities[{index}]!,")?;
        writeln!(&mut output, "      transport,")?;
        writeln!(&mut output, "    ),")?;
    }
    writeln!(&mut output, "  }};")?;
    writeln!(&mut output, "}}")?;
    writeln!(&mut output)?;
    writeln!(&mut output, "export function createGraphqlCrudAdapters(")?;
    writeln!(&mut output, "  transport: GraphqlTransport = createGraphqlTransport(),")?;
    writeln!(&mut output, "): GeneratedGraphqlCrudAdapters {{")?;
    writeln!(&mut output, "  return {{")?;
    for (index, entity) in manifest.database.expected_schema.entities.iter().enumerate() {
        let alias_base = pascal_case(&entity.name);
        writeln!(
            &mut output,
            "    {}: createGraphqlCrudAdapterFromExpectedSchemaEntity<{}RemoteRecord, {}Id>(",
            typescript_property_name(&entity.name),
            alias_base,
            alias_base,
        )?;
        writeln!(&mut output, "      expectedSchema.entities[{index}]!,")?;
        writeln!(&mut output, "      transport,")?;
        writeln!(&mut output, "    ),")?;
    }
    writeln!(&mut output, "  }};")?;
    writeln!(&mut output, "}}")?;

    Ok(format!("{output}\n"))
}

fn resolve_id_type(entity: &crate::manifest::ExpectedSchemaEntityManifest) -> String {
    entity
        .fields
        .iter()
        .find(|field| field.entity_path == entity.id_path)
        .map(|field| typescript_scalar_type(&field.entity_type).to_owned())
        .unwrap_or_else(|| "string".to_owned())
}

fn typescript_object_type_for_fields(fields: &[EntityFieldManifest], remote: bool) -> String {
    let fragments = fields
        .iter()
        .map(|field| {
            let type_expr = if remote {
                typescript_field_type(field, true)
            } else {
                typescript_field_type(field, false)
            };
            let path = if remote {
                field.remote_path.as_str()
            } else {
                field.entity_path.as_str()
            };

            typescript_path_fragment(path, &type_expr, field.optional)
        })
        .collect::<Vec<_>>();

    if fragments.is_empty() {
        "Record<string, never>".to_owned()
    } else {
        fragments.join(" & ")
    }
}

fn typescript_field_type(field: &EntityFieldManifest, remote: bool) -> String {
    let base = if remote && field.encrypted {
        "EncryptedFieldValue".to_owned()
    } else if remote {
        field.remote_schema.as_ref().map(typescript_type_for_node)
            .unwrap_or_else(|| typescript_scalar_type(&field.remote_type).to_owned())
    } else if remote {
        typescript_scalar_type(&field.remote_type).to_owned()
    } else {
        field.entity_schema.as_ref().map(typescript_type_for_node)
            .unwrap_or_else(|| typescript_scalar_type(&field.entity_type).to_owned())
    };

    if field.nullable {
        format!("{base} | null")
    } else {
        base
    }
}

fn typescript_type_for_node(node: &SchemaNodeManifest) -> String {
    let base = typescript_type_for_descriptor(&node.schema);
    let nullable = node.nullable == Some(true);
    let optional = node.optional == Some(true);

    match (nullable, optional) {
        (true, true) => format!("{base} | null | undefined"),
        (true, false) => format!("{base} | null"),
        (false, true) => format!("{base} | undefined"),
        (false, false) => base,
    }
}

fn typescript_type_for_descriptor(descriptor: &SchemaDescriptorManifest) -> String {
    match descriptor {
        SchemaDescriptorManifest::Array { items } => {
            format!("{}[]", parenthesize_typescript_array_item_type(items))
        }
        SchemaDescriptorManifest::Boolean => "boolean".to_owned(),
        SchemaDescriptorManifest::DiscriminatedUnion { options, .. }
        | SchemaDescriptorManifest::Union { options } => options
            .iter()
            .map(typescript_type_for_node)
            .collect::<Vec<_>>()
            .join(" | "),
        SchemaDescriptorManifest::Enum { values } => values
            .iter()
            .map(|value| format!("\"{}\"", value.replace('"', "\\\"")))
            .collect::<Vec<_>>()
            .join(" | "),
        SchemaDescriptorManifest::Literal { value } => {
            if let Some(text) = value.as_str() {
                format!("\"{}\"", text.replace('"', "\\\""))
            } else if let Some(number) = value.as_i64() {
                number.to_string()
            } else if let Some(number) = value.as_u64() {
                number.to_string()
            } else if let Some(number) = value.as_f64() {
                number.to_string()
            } else if let Some(flag) = value.as_bool() {
                flag.to_string()
            } else {
                "null".to_owned()
            }
        }
        SchemaDescriptorManifest::Number { .. } => "number".to_owned(),
        SchemaDescriptorManifest::Object {
            additional_properties,
            properties,
        } => {
            let property_fragments = properties
                .iter()
                .flat_map(|properties| properties.iter())
                .map(|(name, schema)| {
                    let optional_marker = if schema.optional == Some(true) { "?" } else { "" };
                    format!(
                        "{}{}: {}",
                        typescript_property_name(name),
                        optional_marker,
                        typescript_type_for_node_without_optional(schema),
                    )
                })
                .collect::<Vec<_>>();
            let object_fragment = if property_fragments.is_empty() {
                "Record<string, never>".to_owned()
            } else {
                format!("{{ {} }}", property_fragments.join("; "))
            };

            match additional_properties {
                Some(SchemaAdditionalPropertiesManifest::Boolean(false)) | None => object_fragment,
                Some(SchemaAdditionalPropertiesManifest::Boolean(true)) => {
                    format!("{} & Record<string, unknown>", object_fragment)
                }
                Some(SchemaAdditionalPropertiesManifest::Schema(schema)) => format!(
                    "{} & Record<string, {}>",
                    object_fragment,
                    typescript_type_for_node(schema),
                ),
            }
        }
        SchemaDescriptorManifest::Record { values } => {
            format!("Record<string, {}>", typescript_type_for_node(values))
        }
        SchemaDescriptorManifest::String => "string".to_owned(),
        SchemaDescriptorManifest::Unknown => "unknown".to_owned(),
    }
}

fn parenthesize_typescript_array_item_type(items: &SchemaNodeManifest) -> String {
    let item_type = typescript_type_for_node(items);

    if item_type.contains(" | ") || item_type.contains(" & ") {
        format!("({item_type})")
    } else {
        item_type
    }
}

fn typescript_type_for_node_without_optional(node: &SchemaNodeManifest) -> String {
    let mut node = node.clone();
    node.optional = None;
    typescript_type_for_node(&node)
}

fn typescript_scalar_type(schema_type: &str) -> &'static str {
    match schema_type {
        "array" => "unknown[]",
        "boolean" => "boolean",
        "number" => "number",
        "object" => "Record<string, unknown>",
        "string" => "string",
        "json" | "unknown" => "unknown",
        _ => "unknown",
    }
}

fn typescript_path_fragment(path: &str, type_expr: &str, optional: bool) -> String {
    let segments = path.split('.').collect::<Vec<_>>();
    typescript_nested_fragment(&segments, type_expr, optional)
}

fn typescript_nested_fragment(segments: &[&str], type_expr: &str, optional: bool) -> String {
    let property = typescript_property_name(segments[0]);
    let optional_marker = if optional { "?" } else { "" };

    if segments.len() == 1 {
        return format!("{{ {property}{optional_marker}: {type_expr} }}");
    }

    let inner = typescript_nested_fragment(&segments[1..], type_expr, optional);
    format!("{{ {property}{optional_marker}: {inner} }}")
}

fn typescript_property_name(name: &str) -> String {
    if is_typescript_identifier(name) {
        name.to_owned()
    } else {
        format!("\"{}\"", name.replace('"', "\\\""))
    }
}

fn typescript_property_access(name: &str) -> String {
    if is_typescript_identifier(name) {
        name.to_owned()
    } else {
        format!("[\"{}\"]", name.replace('"', "\\\""))
    }
}

fn typescript_member_access(target: &str, name: &str) -> String {
    let property_access = typescript_property_access(name);

    if property_access.starts_with('[') {
        format!("{target}{property_access}")
    } else {
        format!("{target}.{property_access}")
    }
}

fn is_typescript_identifier(value: &str) -> bool {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return false;
    };

    if !(first == '_' || first == '$' || first.is_ascii_alphabetic()) {
        return false;
    }

    chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
}

fn pascal_case(value: &str) -> String {
    let mut output = String::new();
    let mut capitalize = true;

    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() {
            if capitalize {
                output.push(ch.to_ascii_uppercase());
                capitalize = false;
            } else {
                output.push(ch);
            }
        } else {
            capitalize = true;
        }
    }

    if output.is_empty() {
        "GeneratedEntity".to_owned()
    } else {
        output
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::{export_expected_schema, export_typescript_client_bindings};
    use crate::manifest::{
        AuthManifest, BackendAdapterManifest, DatabaseManifest, EntityFieldManifest,
        EntityGraphqlManifest, EntityManifest, EntityRestManifest, ExpectedEntityColumnManifest,
        ExpectedEntityTableManifest, ExpectedSchemaApiManifest, ExpectedSchemaEntityApiManifest,
        ExpectedSchemaRestApiManifest,
        ExpectedSchemaEntityManifest, ExpectedSchemaManifest, RestAuthManifest,
        RestAuthPaths, SchemaDescriptorManifest, SessionCookieNames, SessionManifest,
    };

    fn manifest() -> BackendAdapterManifest {
        BackendAdapterManifest {
            auth: AuthManifest {
                mode: "password-session".to_owned(),
                rest: RestAuthManifest {
                    paths: RestAuthPaths {
                        get_kdf_salt: "/auth/kdf-salt".to_owned(),
                        login: "/auth/login".to_owned(),
                        logout: "/auth/logout".to_owned(),
                        refresh: "/auth/refresh".to_owned(),
                        register_begin: "/auth/register-begin".to_owned(),
                        register_complete: "/auth/register-complete".to_owned(),
                    },
                },
                session: SessionManifest {
                    cookie_names: SessionCookieNames {
                        refresh: "refresh".to_owned(),
                        session: "session".to_owned(),
                    },
                    refresh_duration_seconds: 3600,
                    session_duration_seconds: 600,
                },
            },
            database: DatabaseManifest {
                engine: "postgres".to_owned(),
                expected_schema: ExpectedSchemaManifest {
                    api: ExpectedSchemaApiManifest {
                        graphql: None,
                        rest: Some(ExpectedSchemaRestApiManifest {
                            base_url: "/api".to_owned(),
                            default_headers: None,
                        }),
                        api_type: "rest".to_owned(),
                    },
                    auth_tables: vec!["users".to_owned(), "sessions".to_owned()],
                    entities: vec![ExpectedSchemaEntityManifest {
                        api: ExpectedSchemaEntityApiManifest {
                            graphql: None,
                            rest: Some(EntityRestManifest {
                                allow_create: true,
                                allow_delete: true,
                                allow_get_by_id: true,
                                allow_list: true,
                                allow_update: true,
                                base_path: "/notes".to_owned(),
                            }),
                            api_type: "rest".to_owned(),
                        },
                        fields: vec![
                            EntityFieldManifest {
                                encrypted: true,
                                entity_schema: None,
                                entity_path: "content".to_owned(),
                                entity_type: "string".to_owned(),
                                nullable: false,
                                optional: false,
                                remote_path: "content".to_owned(),
                                remote_schema: None,
                                remote_type: "string".to_owned(),
                                strategy_id: Some("aes-256-gcm".to_owned()),
                            },
                            EntityFieldManifest {
                                encrypted: false,
                                entity_schema: None,
                                entity_path: "id".to_owned(),
                                entity_type: "string".to_owned(),
                                nullable: false,
                                optional: false,
                                remote_path: "id".to_owned(),
                                remote_schema: None,
                                remote_type: "string".to_owned(),
                                strategy_id: None,
                            },
                            EntityFieldManifest {
                                encrypted: false,
                                entity_schema: None,
                                entity_path: "metadata.tags".to_owned(),
                                entity_type: "array".to_owned(),
                                nullable: true,
                                optional: true,
                                remote_path: "metadata.tags".to_owned(),
                                remote_schema: None,
                                remote_type: "array".to_owned(),
                                strategy_id: None,
                            },
                        ],
                        id_path: "id".to_owned(),
                        name: "note".to_owned(),
                        primary_key: "id".to_owned(),
                        table_name: "notes".to_owned(),
                    }],
                    entity_tables: vec![ExpectedEntityTableManifest {
                        columns: vec![
                            ExpectedEntityColumnManifest {
                                column_name: "content".to_owned(),
                                nullable: false,
                                sql_type: "TEXT".to_owned(),
                            },
                            ExpectedEntityColumnManifest {
                                column_name: "id".to_owned(),
                                nullable: false,
                                sql_type: "TEXT".to_owned(),
                            },
                            ExpectedEntityColumnManifest {
                                column_name: "metadata.tags".to_owned(),
                                nullable: true,
                                sql_type: "JSONB".to_owned(),
                            },
                        ],
                        primary_key: "id".to_owned(),
                        table_name: "notes".to_owned(),
                    }],
                },
            },
            entities: vec![EntityManifest {
                fields: vec![EntityFieldManifest {
                    encrypted: false,
                    entity_schema: None,
                    entity_path: "id".to_owned(),
                    entity_type: "string".to_owned(),
                    nullable: false,
                    optional: false,
                    remote_path: "id".to_owned(),
                    remote_schema: None,
                    remote_type: "string".to_owned(),
                    strategy_id: None,
                }],
                graphql: EntityGraphqlManifest {
                    allow_create: true,
                    allow_delete: true,
                    allow_get_by_id: true,
                    allow_list: true,
                    allow_update: true,
                    create_mutation: "createNote".to_owned(),
                    delete_mutation: "deleteNote".to_owned(),
                    get_by_id_query: "note".to_owned(),
                    list_query: "notes".to_owned(),
                    update_mutation: "updateNote".to_owned(),
                },
                id_path: "id".to_owned(),
                name: "note".to_owned(),
                rest: EntityRestManifest {
                    allow_create: true,
                    allow_delete: true,
                    allow_get_by_id: true,
                    allow_list: true,
                    allow_update: true,
                    base_path: "/notes".to_owned(),
                },
                table_name: "notes".to_owned(),
            }],
            name: "notes-service".to_owned(),
            realtime: None,
            version: crate::manifest::MANIFEST_VERSION,
        }
    }

    #[test]
    fn export_expected_schema_wraps_expected_schema() {
        let json = export_expected_schema(&manifest()).expect("expected schema should export");

        assert!(json.contains("\"expectedSchema\""));
        assert!(json.contains("\"api\""));
    }

    #[test]
    fn export_typescript_client_bindings_includes_auth_and_entity_types() {
        let source = export_typescript_client_bindings(&manifest())
            .expect("typescript bindings should export");

        assert!(source.contains("export interface SessionUser"));
        assert!(source.contains("export function createRestTransport"));
        assert!(source.contains("export type NoteEntity = { content: string } & { id: string } & { metadata?: { tags?: unknown[] | null } }"));
        assert!(source.contains("export type NoteRemoteRecord = { content: EncryptedFieldValue } & { id: string } & { metadata?: { tags?: unknown[] | null } }"));
        assert!(source.contains("export function createRestAuthConfig"));
        assert!(source.contains("export function createGraphqlTransport"));
        assert!(source.contains("export function createGraphqlAuthConfig"));
        assert!(source.contains("export function createEntitySchemas"));
        assert!(source.contains("export function createRestModels"));
        assert!(source.contains("export function createGraphqlModels"));
        assert!(source.contains("export function createRestCrudAdapters"));
        assert!(source.contains("export function createGraphqlCrudAdapters"));
        assert!(source.contains("createRestTransportFromExpectedSchema(expectedSchema, options)"));
        assert!(source.contains("createGraphqlTransportFromExpectedSchema(expectedSchema, options)"));
        assert!(source.contains("\"/auth/login\""));
    }

    #[test]
    fn export_typescript_client_bindings_parenthesizes_union_array_items() {
        let item_type = super::typescript_type_for_descriptor(&SchemaDescriptorManifest::Array {
            items: Box::new(crate::manifest::SchemaNodeManifest {
                nullable: None,
                optional: None,
                schema: SchemaDescriptorManifest::DiscriminatedUnion {
                    discriminator: "type".to_owned(),
                    options: vec![
                        crate::manifest::SchemaNodeManifest {
                            nullable: None,
                            optional: None,
                            schema: SchemaDescriptorManifest::Object {
                                additional_properties: None,
                                properties: Some(BTreeMap::from([( 
                                    "type".to_owned(),
                                    crate::manifest::SchemaNodeManifest {
                                        nullable: None,
                                        optional: None,
                                        schema: SchemaDescriptorManifest::Literal {
                                            value: serde_json::Value::String("a".to_owned()),
                                        },
                                    },
                                )])),
                            },
                        },
                        crate::manifest::SchemaNodeManifest {
                            nullable: None,
                            optional: None,
                            schema: SchemaDescriptorManifest::Object {
                                additional_properties: None,
                                properties: Some(BTreeMap::from([( 
                                    "type".to_owned(),
                                    crate::manifest::SchemaNodeManifest {
                                        nullable: None,
                                        optional: None,
                                        schema: SchemaDescriptorManifest::Literal {
                                            value: serde_json::Value::String("b".to_owned()),
                                        },
                                    },
                                )])),
                            },
                        },
                    ],
                },
            }),
        });

        assert_eq!(item_type, "({ type: \"a\" } | { type: \"b\" })[]");
    }
}
