///|
/// Type Code Generator — P0-008.
/// Generates MoonBit struct/enum AST nodes from IR types and catalog data.
/// Handles nullable strategy (Option[T]) and naming conversion (snake_case → PascalCase).
//
// Copyright 2026 Mairzzcllo
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

///|
/// Generate a StructDef from a list of InternalFields.
/// Each field is mapped through the type mapping layer with nullability handling.
pub fn generate_struct_from_fields(
  type_name : String,
  fields : Array[InternalField],
  doc : String?,
  is_pub : Bool,
  emit_json_tags : Bool,
  emit_db_tags : Bool,
  overrides : Array[(String, String)],
  nullable_overrides : Array[String],
  json_tags_case_style : String,
  _initialisms : Array[String],
) -> StructDef {
  let field_defs = fields.map(fn(f) {
    let moon_type = qual_runtime_type(
      map_internal_type_nullable(
        f.ty,
        f.not_null,
        overrides,
        nullable_overrides,
        [],
      ),
    )
    let field_doc : String? = if f.comment == "" {
      None
    } else {
      Some(f.comment)
    }
    let field_name = match f.rename_to {
      Some(r) => escape_keyword(r)
      None => escape_keyword(f.name)
    }
    // json_tag/db_tag use original column name (database column name), not renamed
    let tag_value = apply_case_style(f.name, json_tags_case_style)
    let json_tag : String? = if emit_json_tags { Some(tag_value) } else { None }
    let db_tag : String? = if emit_db_tags { Some(tag_value) } else { None }
    {
      name: field_name,
      col_name: f.name,
      ty: moon_type,
      doc: field_doc,
      json_tag,
      db_tag,
    }
  })
  { name: type_name, fields: field_defs, doc, is_pub }
}

///|
/// Generate a StructDef from a catalog table.
/// When emit_exact_table_names=false (default), singularizes the table name before PascalCase.
pub fn generate_struct_from_table(
  table_name : String,
  columns : Array[AdaptColumn],
  comment : String,
  emit_json_tags : Bool,
  emit_db_tags : Bool,
  overrides : Array[(String, String)],
  column_overrides : Array[ColumnOverride],
  nullable_overrides : Array[String],
  renames : Array[(String, String)],
  emit_exact_table_names : Bool,
  json_tags_case_style : String,
  initialisms : Array[String],
  enum_renames : Array[(String, String)],
) -> StructDef {
  let type_name = table_struct_type_name(
    table_name, emit_exact_table_names, initialisms,
  )
  let doc : String? = if comment == "" { None } else { Some(comment) }
  let fields = columns.map(fn(c) {
    let ir_type = if c.is_array {
      Arr(Named(c.ty.name))
    } else {
      Named(c.ty.name)
    }
    let moon_type = qual_runtime_type(
      match lookup_column_override(table_name, c.name, column_overrides) {
        Some(mapped) => {
          let base = TypeExpr::Named(mapped)
          if c.not_null {
            if is_nullable_override(c.ty.name, nullable_overrides) {
              Option(base)
            } else {
              base
            }
          } else {
            Option(base)
          }
        }
        None =>
          map_internal_type_nullable(
            ir_type,
            c.not_null,
            overrides,
            nullable_overrides,
            enum_renames,
          )
      },
    )
    let field_doc : String? = if c.comment == "" {
      None
    } else {
      Some(c.comment)
    }
    let rename_to = lookup_rename(c.name, renames)
    let field_name = match rename_to {
      Some(r) => escape_keyword(r)
      None => escape_keyword(c.name)
    }
    // json_tag/db_tag use original column name (database column name), not renamed
    let tag_value = apply_case_style(c.name, json_tags_case_style)
    let json_tag : String? = if emit_json_tags { Some(tag_value) } else { None }
    let db_tag : String? = if emit_db_tags { Some(tag_value) } else { None }
    {
      name: field_name,
      col_name: c.name,
      ty: moon_type,
      doc: field_doc,
      json_tag,
      db_tag,
    }
  })
  { name: type_name, fields, doc, is_pub: true }
}

///|
/// Generate an EnumDef from a catalog enum with an optional pre-resolved type name.
pub fn generate_enum_from_enum(
  _name : String,
  vals : Array[String],
  comment : String,
  initialisms : Array[String],
  type_name : String,
) -> EnumDef {
  let doc : String? = if comment == "" { None } else { Some(comment) }
  let variants = vals.map(fn(v) {
    {
      name: escape_keyword(snake_to_pascal(v, initialisms)),
      doc: (None : String?),
    }
  })
  { name: type_name, variants, doc, is_pub: true }
}

///|
/// P1-060: pick a non-colliding enum type name when a struct already uses `base_pascal`.
fn disambiguate_enum_type_name(
  base_pascal : String,
  struct_names : Array[String],
) -> String {
  if !struct_names.contains(base_pascal) {
    return base_pascal
  }
  let with_type = base_pascal + "Type"
  if !struct_names.contains(with_type) {
    return with_type
  }
  for n in 1..<100 {
    let candidate = base_pascal + n.to_string()
    if !struct_names.contains(candidate) {
      return candidate
    }
  }
  with_type
}

///|
/// P1-060: build PG enum name → disambiguated MoonBit type name map for a schema.
fn build_enum_renames(
  schema : AdaptSchema,
  struct_names : Array[String],
  initialisms : Array[String],
) -> Array[(String, String)] {
  let renames : Array[(String, String)] = []
  for e in schema.enums {
    let pascal = escape_keyword(snake_to_pascal(e.name, initialisms))
    let resolved = disambiguate_enum_type_name(pascal, struct_names)
    if resolved != pascal {
      renames.push((e.name.to_lower(), resolved))
    }
  }
  renames
}

///|
/// Generate all type definitions from an AdaptCatalog.
/// Produces StructDef for each table (with decode method) and EnumDef for every enum.
/// When omit_unused_structs is true and referenced_tables is non-empty, only tables
/// whose names appear in the referenced_tables list are included.
pub fn generate_types_from_catalog(
  catalog : AdaptCatalog,
  emit_json_tags : Bool,
  emit_db_tags : Bool,
  overrides : Array[(String, String)],
  column_overrides : Array[ColumnOverride],
  nullable_overrides : Array[String],
  renames : Array[(String, String)],
  omit_unused_structs : Bool,
  referenced_tables : Array[String],
  emit_exact_table_names : Bool,
  json_tags_case_style : String,
  initialisms : Array[String],
) -> Array[TopLevel] {
  catalog.schemas
  .map(fn(schema) {
    let tables_to_use = if omit_unused_structs && referenced_tables.length() > 0 {
      schema.tables.filter(fn(t) { referenced_tables.contains(t.name) })
    } else {
      schema.tables
    }
    let struct_names : Array[String] = []
    for table in tables_to_use {
      struct_names.push(
        table_struct_type_name(table.name, emit_exact_table_names, initialisms),
      )
    }
    let enum_renames = build_enum_renames(schema, struct_names, initialisms)
    let table_items = tables_to_use
      .map(fn(table) {
        let sd = generate_struct_from_table(
          table.name,
          table.columns,
          table.comment,
          emit_json_tags,
          emit_db_tags,
          overrides,
          column_overrides,
          nullable_overrides,
          renames,
          emit_exact_table_names,
          json_tags_case_style,
          initialisms,
          enum_renames,
        )
        [Struct(sd), Fn(generate_decode_fn(sd))]
      })
      .flatten()
    let enum_items = schema.enums.map(fn(e) {
      let pascal = escape_keyword(snake_to_pascal(e.name, initialisms))
      let resolved = disambiguate_enum_type_name(pascal, struct_names)
      Enum(
        generate_enum_from_enum(
          e.name,
          e.vals,
          e.comment,
          initialisms,
          resolved,
        ),
      )
    })
    table_items + enum_items
  })
  .flatten()
}

///|
/// Generate struct definitions for query result types.
/// Produces a StructDef (with decode method) for each query that has a Rows result shape.
/// The struct is named `Row` in PascalCase.
pub fn generate_types_from_queries(
  queries : Array[InternalQuery],
  emit_json_tags : Bool,
  emit_db_tags : Bool,
  overrides : Array[(String, String)],
  nullable_overrides : Array[String],
  json_tags_case_style : String,
  initialisms : Array[String],
) -> Array[TopLevel] {
  queries
  .map(fn(q) {
    match q.result_shape {
      Rows(fields) => {
        let type_name = escape_keyword(
          snake_to_pascal(q.name, initialisms) + "Row",
        )
        let doc : String? = if q.comments.length() > 0 {
          Some(q.comments[0])
        } else {
          None
        }
        let sd = generate_struct_from_fields(
          type_name, fields, doc, true, emit_json_tags, emit_db_tags, overrides,
          nullable_overrides, json_tags_case_style, initialisms,
        )
        [Struct(sd), Fn(generate_decode_fn(sd))]
      }
      None => []
    }
  })
  .flatten()
}

///|
/// Collect all table names referenced by query columns from adapter queries.
fn collect_referenced_table_names(queries : Array[AdaptQuery]) -> Array[String] {
  let names : Array[String] = []
  for q in queries {
    for c in q.columns {
      let tbl_name = c.table.name
      if tbl_name.length() > 0 && !names.contains(tbl_name) {
        names.push(tbl_name)
      }
    }
  }
  names
}

///|
fn register_codegen_type_name(
  names : Array[String],
  sources : Array[String],
  raw_inputs : Array[String],
  pascal : String,
  raw_input : String,
  source : String,
) -> Unit raise {
  for i = 0; i < names.length(); i = i + 1 {
    if names[i] == pascal {
      if raw_inputs[i] != raw_input {
        raise Failure::Failure(
          "naming collision: '" +
          raw_inputs[i] +
          "' and '" +
          raw_input +
          "' both map to '" +
          pascal +
          "'",
        )
      }
      raise Failure::Failure(
        "duplicate type name '" + pascal + "': " + sources[i] + " and " + source,
      )
    }
  }
  names.push(pascal)
  sources.push(source)
  raw_inputs.push(raw_input)
}

///|
/// Detect duplicate or colliding MoonBit type names before codegen.
pub fn validate_codegen_type_names(
  catalog : AdaptCatalog,
  queries : Array[InternalQuery],
  adapt_queries : Array[AdaptQuery],
  omit_unused_structs : Bool,
  emit_exact_table_names : Bool,
  initialisms_str : String,
) -> Unit raise {
  let initialisms = parse_initialisms(initialisms_str)
  let referenced_tables = collect_referenced_table_names(adapt_queries)
  let names : Array[String] = []
  let sources : Array[String] = []
  let raw_inputs : Array[String] = []
  for schema in catalog.schemas {
    let tables_to_use = if omit_unused_structs && referenced_tables.length() > 0 {
      schema.tables.filter(fn(t) { referenced_tables.contains(t.name) })
    } else {
      schema.tables
    }
    let struct_names : Array[String] = []
    for table in tables_to_use {
      let base = table_struct_base_name(table.name, emit_exact_table_names)
      let pascal = table_struct_type_name(
        table.name,
        emit_exact_table_names,
        initialisms,
      )
      struct_names.push(pascal)
      register_codegen_type_name(
        names,
        sources,
        raw_inputs,
        pascal,
        base,
        "table '" + table.name + "'",
      )
    }
    for e in schema.enums {
      let pascal = escape_keyword(snake_to_pascal(e.name, initialisms))
      let resolved = disambiguate_enum_type_name(pascal, struct_names)
      register_codegen_type_name(
        names,
        sources,
        raw_inputs,
        resolved,
        e.name,
        "enum '" + e.name + "'",
      )
    }
  }
  for q in queries {
    match q.result_shape {
      Rows(_) => {
        let pascal = escape_keyword(
          snake_to_pascal(q.name, initialisms) + "Row",
        )
        register_codegen_type_name(
          names,
          sources,
          raw_inputs,
          pascal,
          q.name,
          "query '" + q.name + "'",
        )
      }
      None => ()
    }
  }
}

///|
/// Generate all type definitions from both catalog and queries.
/// Each struct is followed by its decode method.
pub fn generate_types(
  catalog : AdaptCatalog,
  queries : Array[InternalQuery],
  adapt_queries : Array[AdaptQuery],
  emit_json_tags : Bool,
  emit_db_tags : Bool,
  overrides : Array[(String, String)],
  column_overrides : Array[ColumnOverride],
  nullable_overrides : Array[String],
  renames : Array[(String, String)],
  omit_unused_structs : Bool,
  emit_exact_table_names : Bool,
  json_tags_case_style : String,
  initialisms_str : String,
) -> Array[TopLevel] {
  let parsed_initialisms = parse_initialisms(initialisms_str)
  let referenced_tables = collect_referenced_table_names(adapt_queries)
  generate_types_from_catalog(
    catalog, emit_json_tags, emit_db_tags, overrides, column_overrides, nullable_overrides,
    renames, omit_unused_structs, referenced_tables, emit_exact_table_names, json_tags_case_style,
    parsed_initialisms,
  ) +
  generate_types_from_queries(
    queries, emit_json_tags, emit_db_tags, overrides, nullable_overrides, json_tags_case_style,
    parsed_initialisms,
  )
}

///|
/// Map a MoonBit struct type name to the corresponding typed getter suffix.
fn type_to_getter_suffix(type_name : String) -> String {
  let base = unqual_runtime_name(type_name)
  match base {
    "Int64" => "int64"
    "Int" => "int64"
    "String" => "string"
    "Bool" => "bool"
    "Double" => "double"
    "Bytes" => "bytes"
    "JsonValue" => "json"
    "Date" => "date"
    "DateTime" => "datetime"
    "Decimal" => "decimal"
    "Uuid" => "uuid"
    "Duration" => "duration"
    "Time" => "time"
    "TimeTZ" => "timetz"
    "IpAddr" => "ipaddr"
    _ => "string"
  }
}

///|
/// Build a getter call expression for a field at a given index.
fn field_getter_call(field : FieldDef, _index : Int) -> (String, String) {
  let (getter_name, suffix) = match field.ty {
    Named(s) => {
      let suffix = type_to_getter_suffix(s)
      ("row.get_" + suffix, suffix)
    }
    Option(inner) =>
      match inner {
        Parameterized("Array", [elem]) => {
          let inner_suffix = match elem {
            Named(n) => type_to_getter_suffix(n)
            _ => "string"
          }
          ("row.decode_nullable_array_" + inner_suffix, inner_suffix)
        }
        _ => {
          let inner_name = match inner {
            Named(n) => n
            _ => "String"
          }
          let suffix = type_to_getter_suffix(inner_name)
          ("row.get_nullable_" + suffix, suffix)
        }
      }
    Parameterized("Array", [inner]) => {
      let inner_name = match inner {
        Named(n) => n
        _ => "String"
      }
      let suffix = type_to_getter_suffix(inner_name)
      ("row.decode_array_" + suffix, suffix)
    }
    _ => ("row.get_string", "string")
  }
  (getter_name, suffix)
}

///|
/// Build the decode body using column name lookup (P0-063).
/// Each getter call uses `row.index_of("column_name")` as the index argument,
/// so column reordering does not break decoding.
pub fn build_decode_body(fields : Array[FieldDef]) -> Array[Expr] {
  [build_decode_match(fields, 0)]
}

///|
/// Build a nested match expression for decoding all fields.
/// For N fields, produces 2N levels of nested match (one for index_of, one for getter).
/// P0-063: Uses `row.index_of("col_name")` with original DB column name for index lookups.
/// P0-062/064: Handles `row.index_of` returning `Result[Int, DBError]` via nested match.
fn build_decode_match(fields : Array[FieldDef], start_index : Int) -> Expr {
  let i = start_index
  if i >= fields.length() {
    // All fields decoded — build the Ok struct literal
    let mut literal_fields = ""
    let mut first = true
    for f in fields {
      if !first {
        literal_fields = literal_fields + ", "
      }
      literal_fields = literal_fields + f.name + ": " + f.name
      first = false
    }
    Call("Ok", [Ident("{ " + literal_fields + " }")])
  } else {
    let f = fields[i]
    let (getter_name, _) = field_getter_call(f, i)
    let idx_var = "_idx_" + i.to_string()
    // Match on row.index_of(original_column_name) — returns Result[Int, DBError]
    let index_call = Call("row.index_of", [StrLit(f.col_name)])
    let index_ok_body = {
      let getter_call = Call(getter_name, [Ident(idx_var)])
      Match(getter_call, [
        ("Ok(" + f.name + ")", build_decode_match(fields, i + 1)),
        ("Err(e)", Call("Err", [Ident("e")])),
      ])
    }
    Match(index_call, [
      ("Ok(" + idx_var + ")", index_ok_body),
      ("Err(e)", Call("Err", [Ident("e")])),
    ])
  }
}

///|
/// Generate a decode function for a StructDef using typed getters.
/// Uses nested match instead of ? operator (not available in MoonBit 0.1).
/// P0-063: Uses `row.index_of("col_name")` with original DB column name.
/// Each field produces two nested match levels: one for index_of (Result[Int, DBError])
/// and one for the typed getter, so column reordering does not break decoding.
pub fn generate_decode_fn(sd : StructDef) -> FnDef {
  let params = [{ name: "row", ty: TypeExpr::Named(qual_runtime_name("Row")) }]
  let return_ty = Parameterized("Result", [
    Named(sd.name),
    Named(qual_runtime_name("DBError")),
  ])
  let body = build_decode_body(sd.fields)
  let doc = Some("Decode a Row into a " + sd.name)
  { name: sd.name + "::decode", params, return_ty, body, doc, is_pub: true }
}

// === Tests ===

///|
test "snake_to_pascal: single word" {
  inspect(snake_to_pascal("users", []), content="Users")
}

///|
test "snake_to_pascal: multi word" {
  inspect(snake_to_pascal("user_profiles", []), content="UserProfiles")
}

///|
test "snake_to_pascal: already PascalCase" {
  inspect(snake_to_pascal("Users", []), content="Users")
}

///|
test "snake_to_pascal: empty string" {
  inspect(snake_to_pascal("", []), content="UnnamedType")
}

///|
test "snake_to_pascal: single char" {
  inspect(snake_to_pascal("a", []), content="A")
}

///|
test "snake_to_pascal: leading underscore" {
  inspect(snake_to_pascal("_private", []), content="Private")
}

///|
test "snake_to_pascal: multiple underscores" {
  inspect(snake_to_pascal("blog_post_tags", []), content="BlogPostTags")
}

///|
test "generate_struct_from_fields: basic struct" {
  let fields : Array[InternalField] = [
    {
      name: "id",
      ty: Named("int4"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
    {
      name: "email",
      ty: Named("text"),
      not_null: false,
      comment: "user email",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    Some("A user"),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.name, content="User")
  inspect(sd.fields.length(), content="2")
  inspect(sd.fields[0].name, content="id")
  inspect(sd.fields[0].ty, content="Named(\"Int64\")")
  inspect(sd.fields[1].name, content="email")
  inspect(sd.fields[1].ty, content="Option(Named(\"String\"))")
}

///|
test "generate_struct_from_fields: array field" {
  let fields : Array[InternalField] = [
    {
      name: "tags",
      ty: Arr(Named("text")),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "Post",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(
    sd.fields[0].ty,
    content="Parameterized(\"Array\", [Named(\"String\")])",
  )
}

///|
test "generate_struct_from_table: snake_case table name to PascalCase" {
  let cols : Array[AdaptColumn] = [
    {
      name: "id",
      not_null: true,
      is_array: false,
      ty: { catalog: "", schema: "", name: "int4" },
      length: 0,
      comment: "",
      table: { catalog: "", schema: "public", name: "user_profiles" },
    },
  ]
  let sd = generate_struct_from_table(
    "user_profiles",
    cols,
    "User profiles table",
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    "snake",
    [],
    [],
  )
  inspect(sd.name, content="UserProfiles")
  debug_inspect(sd.doc, content="Some(\"User profiles table\")")
  inspect(sd.fields[0].name, content="id")
  inspect(sd.fields[0].ty, content="Named(\"Int64\")")
}

///|
test "generate_struct_from_table: nullable column becomes Option" {
  let cols : Array[AdaptColumn] = [
    {
      name: "nickname",
      not_null: false,
      is_array: false,
      ty: { catalog: "", schema: "", name: "text" },
      length: 0,
      comment: "",
      table: { catalog: "", schema: "public", name: "users" },
    },
  ]
  let sd = generate_struct_from_table(
    "users",
    cols,
    "",
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    "snake",
    [],
    [],
  )
  inspect(sd.fields[0].ty, content="Option(Named(\"String\"))")
}

///|
test "generate_struct_from_table: array column becomes Array[T]" {
  let cols : Array[AdaptColumn] = [
    {
      name: "tags",
      not_null: true,
      is_array: true,
      ty: { catalog: "", schema: "", name: "text" },
      length: 0,
      comment: "",
      table: { catalog: "", schema: "public", name: "posts" },
    },
  ]
  let sd = generate_struct_from_table(
    "posts",
    cols,
    "",
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    "snake",
    [],
    [],
  )
  inspect(
    sd.fields[0].ty,
    content="Parameterized(\"Array\", [Named(\"String\")])",
  )
}

///|
test "generate_enum_from_enum: basic enum" {
  let ed = generate_enum_from_enum(
    "mood",
    ["happy", "sad", "neutral"],
    "User mood",
    [],
    "Mood",
  )
  inspect(ed.name, content="Mood")
  inspect(ed.variants.length(), content="3")
  inspect(ed.variants[0].name, content="Happy")
  inspect(ed.variants[1].name, content="Sad")
}

///|
test "generate_types_from_catalog: single schema with table" {
  let col : AdaptColumn = {
    name: "id",
    not_null: true,
    is_array: false,
    ty: { catalog: "", schema: "", name: "int4" },
    length: 0,
    comment: "",
    table: { catalog: "", schema: "public", name: "users" },
  }
  let schema : AdaptSchema = {
    name: "public",
    comment: "",
    tables: [{ name: "users", schema: "public", columns: [col], comment: "" }],
    enums: [],
  }
  let catalog : AdaptCatalog = {
    name: "",
    default_schema: "public",
    schemas: [schema],
  }
  let items = generate_types_from_catalog(
    catalog,
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    [],
    false,
    "snake",
    [],
  )
  inspect(items.length(), content="2")
  let name = match items[0] {
    Struct(s) => s.name
    _ => ""
  }
  inspect(name, content="User")
  let fn_name = match items[1] {
    Fn(f) => f.name
    _ => ""
  }
  inspect(fn_name, content="User::decode")
}

///|
test "generate_types_from_catalog: table and enum" {
  let col : AdaptColumn = {
    name: "id",
    not_null: true,
    is_array: false,
    ty: { catalog: "", schema: "", name: "int4" },
    length: 0,
    comment: "",
    table: { catalog: "", schema: "public", name: "orders" },
  }
  let schema : AdaptSchema = {
    name: "public",
    comment: "",
    tables: [{ name: "orders", schema: "public", columns: [col], comment: "" }],
    enums: [{ name: "status", vals: ["pending", "shipped"], comment: "" }],
  }
  let catalog : AdaptCatalog = {
    name: "",
    default_schema: "public",
    schemas: [schema],
  }
  let items = generate_types_from_catalog(
    catalog,
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    [],
    false,
    "snake",
    [],
  )
  inspect(items.length(), content="3")
  let names = items.map(fn(item) {
    match item {
      Struct(s) => s.name
      Enum(e) => e.name
      Fn(f) => f.name
      _ => ""
    }
  })
  inspect(names[0], content="Order")
  inspect(names[1], content="Order::decode")
  inspect(names[2], content="Status")
}

///|
test "generate_types_from_queries: :many query produces Row struct" {
  let fields : Array[InternalField] = [
    {
      name: "id",
      ty: Named("int4"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
    {
      name: "name",
      ty: Named("text"),
      not_null: false,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let q : InternalQuery = {
    name: "list_users",
    text: "SELECT * FROM users",
    cmd: ManyRows,
    raw_cmd: Many,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: Rows(fields),
    comments: ["List all users"],
    filename: "query.sql",
  }
  let items = generate_types_from_queries([q], false, false, [], [], "snake", [])
  inspect(items.length(), content="2")
  let sd = match items[0] {
    Struct(s) => s
    _ => { name: "", fields: [], doc: (None : String?), is_pub: false }
  }
  inspect(sd.name, content="ListUsersRow")
  inspect(sd.fields.length(), content="2")
  inspect(sd.fields[0].name, content="id")
  inspect(sd.fields[1].name, content="name")
  let fn_name = match items[1] {
    Fn(f) => f.name
    _ => ""
  }
  inspect(fn_name, content="ListUsersRow::decode")
}

///|
test "generate_types_from_queries: :exec query produces no struct" {
  let q : InternalQuery = {
    name: "delete_user",
    text: "DELETE FROM users",
    cmd: ExecResult,
    raw_cmd: Exec,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "q.sql",
  }
  let items = generate_types_from_queries([q], false, false, [], [], "snake", [])
  inspect(items.length(), content="0")
}

///|
test "generate_types: combines catalog and query types" {
  let col : AdaptColumn = {
    name: "id",
    not_null: true,
    is_array: false,
    ty: { catalog: "", schema: "", name: "int4" },
    length: 0,
    comment: "",
    table: { catalog: "", schema: "public", name: "users" },
  }
  let schema : AdaptSchema = {
    name: "public",
    comment: "",
    tables: [{ name: "users", schema: "public", columns: [col], comment: "" }],
    enums: [],
  }
  let catalog : AdaptCatalog = {
    name: "",
    default_schema: "public",
    schemas: [schema],
  }
  let fields : Array[InternalField] = [
    {
      name: "id",
      ty: Named("int4"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let q : InternalQuery = {
    name: "get_user",
    text: "SELECT * FROM users",
    cmd: OneRow,
    raw_cmd: One,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: Rows(fields),
    comments: Array::make(0, ""),
    filename: "q.sql",
  }
  let items = generate_types(
    catalog,
    [q],
    [],
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    false,
    "snake",
    "",
  )
  inspect(items.length(), content="4")
  let first_name = match items[0] {
    Struct(s) => s.name
    _ => ""
  }
  let second_name = match items[1] {
    Fn(f) => f.name
    _ => ""
  }
  let third_name = match items[2] {
    Struct(s) => s.name
    _ => ""
  }
  let fourth_name = match items[3] {
    Fn(f) => f.name
    _ => ""
  }
  inspect(first_name, content="User")
  inspect(second_name, content="User::decode")
  inspect(third_name, content="GetUserRow")
  inspect(fourth_name, content="GetUserRow::decode")
}

///|
test "snake_to_pascal with empty initialisms: single word" {
  inspect(snake_to_pascal("hello", []), content="Hello")
}

///|
test "generate_struct_from_fields: int8 maps to Int64" {
  let fields : Array[InternalField] = [
    {
      name: "big_id",
      ty: Named("int8"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "Big",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].ty, content="Named(\"Int64\")")
}

// === P0-065: Keyword escaping tests ===

///|
test "P0-065: field name 'type' is escaped to 'ty'" {
  let fields : Array[InternalField] = [
    {
      name: "type",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].name, content="ty")
}

///|
test "P0-065: field name 'fn' is escaped to 'fn_'" {
  let fields : Array[InternalField] = [
    {
      name: "fn",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].name, content="fn_")
}

///|
test "P0-065: field name 'let' is escaped to 'let_'" {
  let fields : Array[InternalField] = [
    {
      name: "let",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].name, content="let_")
}

///|
test "P0-065: field name 'struct' is escaped to 'struct_'" {
  let fields : Array[InternalField] = [
    {
      name: "struct",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].name, content="struct_")
}

///|
test "P0-065: field name 'enum' is escaped to 'enum_'" {
  let fields : Array[InternalField] = [
    {
      name: "enum",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].name, content="enum_")
}

///|
test "P0-065: non-keyword field name passes through unchanged" {
  let fields : Array[InternalField] = [
    {
      name: "user_name",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: (None : String?),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  inspect(sd.fields[0].name, content="user_name")
}

///|
test "P0-065: rename_to non-keyword value passes through" {
  let fields : Array[InternalField] = [
    {
      name: "type",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: Some("kind"),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  // "kind" is not a MoonBit keyword, so escape_keyword returns it unchanged
  inspect(sd.fields[0].name, content="kind")
}

///|
test "P0-065: rename_to keyword value is escaped" {
  let fields : Array[InternalField] = [
    {
      name: "category",
      ty: Named("text"),
      not_null: true,
      comment: "",
      rename_to: Some("fn"),
    },
  ]
  let sd = generate_struct_from_fields(
    "User",
    fields,
    (None : String?),
    true,
    false,
    false,
    [],
    [],
    "snake",
    [],
  )
  // "fn" is a MoonBit keyword, so escape_keyword returns "fn_"
  inspect(sd.fields[0].name, content="fn_")
}

///|
test "P0-065: enum variant 'self' is escaped to 'Self_'" {
  let ed = generate_enum_from_enum(
    "status",
    ["active", "self", "pending"],
    "",
    [],
    "Status",
  )
  inspect(ed.variants[1].name, content="Self_")
}

///|
test "P0-065: struct type name that shadows built-in type is escaped" {
  let cols : Array[AdaptColumn] = [
    {
      name: "value",
      not_null: true,
      is_array: false,
      ty: { catalog: "", schema: "", name: "int4" },
      length: 0,
      comment: "",
      table: { catalog: "", schema: "public", name: "int" },
    },
  ]
  let sd = generate_struct_from_table(
    "int",
    cols,
    "",
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    "snake",
    [],
    [],
  )
  inspect(sd.name, content="Int_")
}

///|
test "P0-065: struct type name 'Struct' passes through (Struct is not a keyword in MoonBit)" {
  let cols : Array[AdaptColumn] = [
    {
      name: "value",
      not_null: true,
      is_array: false,
      ty: { catalog: "", schema: "", name: "int4" },
      length: 0,
      comment: "",
      table: { catalog: "", schema: "public", name: "struct" },
    },
  ]
  let sd = generate_struct_from_table(
    "struct",
    cols,
    "",
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    "snake",
    [],
    [],
  )
  inspect(sd.name, content="Struct")
}

///|
test "P0-065: enum type name that shadows built-in type is escaped" {
  let ed = generate_enum_from_enum("string", ["a", "b"], "", [], "String_")
  inspect(ed.name, content="String_")
}

///|
test "P1-060: table and enum same name disambiguates enum with Type suffix" {
  let col : AdaptColumn = {
    name: "id",
    not_null: true,
    is_array: false,
    ty: { catalog: "", schema: "", name: "int4" },
    length: 0,
    comment: "",
    table: { catalog: "", schema: "public", name: "status" },
  }
  let schema : AdaptSchema = {
    name: "public",
    comment: "",
    tables: [{ name: "status", schema: "public", columns: [col], comment: "" }],
    enums: [{ name: "status", vals: ["active", "inactive"], comment: "" }],
  }
  let catalog : AdaptCatalog = {
    name: "db",
    default_schema: "public",
    schemas: [schema],
  }
  let items = generate_types_from_catalog(
    catalog,
    false,
    false,
    [],
    [],
    [],
    [],
    false,
    [],
    false,
    "snake",
    [],
  )
  let mut has_struct = false
  let mut has_enum = false
  for item in items {
    match item {
      Struct(sd) => if sd.name == "Status" { has_struct = true }
      Enum(ed) => if ed.name == "StatusType" { has_enum = true }
      _ => ()
    }
  }
  inspect(has_struct, content="true")
  inspect(has_enum, content="true")
}