///|
/// Query Code Generator — P0-009.
/// Generates MoonBit query function definitions from IR queries.
/// Produces FnDef AST nodes with db: DB first param, typed params, and SQL body.
//
// 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.
//

///|
/// Get the result struct type name for a query.
fn get_result_type_name(
  query : InternalQuery,
  initialisms : Array[String],
) -> String {
  escape_keyword(snake_to_pascal(query.name, initialisms) + "Row")
}

///|
/// Convert an InternalParameter to a ParamDef, mapping the type through the type layer.
fn param_to_param_def(
  p : InternalParameter,
  overrides : Array[(String, String)],
  nullable_overrides : Array[String],
) -> ParamDef {
  let moon_type = map_internal_type_nullable(
    p.ty,
    p.not_null,
    overrides,
    nullable_overrides,
    [],
  )
  { name: escape_keyword(p.name), ty: moon_type }
}

///|
/// Build a single Value constructor expression for a parameter.
fn param_to_value_expr(
  p : InternalParameter,
  overrides : Array[(String, String)],
) -> Expr {
  let ctor_name = type_to_value_constructor(p.ty, overrides)
  let from_fn = value_ctor_to_from_fn(ctor_name)
  let escaped_name = escape_keyword(p.name)
  if p.not_null {
    Call(from_fn, [Ident(escaped_name)])
  } else {
    Match(Ident(escaped_name), [
      ("None", Call(runtime_null_value_fn(), [])),
      ("Some(" + escaped_name + ")", Call(from_fn, [Ident(escaped_name)])),
    ])
  }
}

///|
/// Build the Array[Value] params expression from query parameters.
fn build_params_expr(
  params : Array[InternalParameter],
  overrides : Array[(String, String)],
  _nullable_overrides : Array[String],
) -> Expr {
  let value_exprs = params.map(fn(p) { param_to_value_expr(p, overrides) })
  ArrLit(value_exprs)
}

///|
/// Build the return TypeExpr for a query based on its cardinality, result type,
/// and result shape. ExecResult/ExecCount with Rows (e.g. RETURNING *) produce
/// Result[T, DBError] (same as OneRow), while without rows produce Result[Int64, DBError].
fn build_return_ty(
  cmd : QueryCardinality,
  result_type : String,
  result_shape : InternalResultShape,
) -> TypeExpr {
  let err_ty = TypeExpr::Named(qual_runtime_name("DBError"))
  match cmd {
    OneRow => Parameterized("Result", [TypeExpr::Named(result_type), err_ty])
    ManyRows =>
      Parameterized("Result", [
        Parameterized("Array", [TypeExpr::Named(result_type)]),
        err_ty,
      ])
    ExecResult =>
      match result_shape {
        Rows(_) =>
          Parameterized("Result", [TypeExpr::Named(result_type), err_ty])
        None =>
          Parameterized("Result", [
            TypeExpr::Named(qual_runtime_name("ExecResult")),
            err_ty,
          ])
      }
    ExecCount =>
      match result_shape {
        Rows(_) =>
          Parameterized("Result", [TypeExpr::Named(result_type), err_ty])
        None => Parameterized("Result", [TypeExpr::Named("Int64"), err_ty])
      }
  }
}

///|
/// Build the function body expressions for a query.
/// ExecResult/ExecCount with Rows (e.g. RETURNING *) generate decode paths
/// instead of exec/execrows calls.
/// When emit_empty_slices is true, :many queries return an empty array
/// instead of an error when no rows are returned.
fn build_body(
  query : InternalQuery,
  result_type : String,
  conn_name : String,
  result_shape : InternalResultShape,
  overrides : Array[(String, String)],
  nullable_overrides : Array[String],
  emit_empty_slices : Bool,
) -> Array[Expr] {
  let sql_let = Let("sql", StrLit(query.text))
  let params_expr = build_params_expr(
    query.params,
    overrides,
    nullable_overrides,
  )
  let params_let = Let("params", params_expr)
  match query.cmd {
    OneRow =>
      [
        sql_let,
        params_let,
        Match(Call(conn_name + ".query", [Ident("sql"), Ident("params")]), [
          (
            "Ok(rows)",
            Match(Call("rows.collect_limited", [IntLit(2)]), [
              (
                "Ok(collected)",
                Match(Call("collected.length", []), [
                  ("0", Call("Err", [Call(runtime_db_err_no_rows_fn(), [])])),
                  (
                    "1",
                    Call(result_type + "::decode", [
                      Index(Ident("collected"), IntLit(0)),
                    ]),
                  ),
                  (
                    "_",
                    Call("Err", [
                      Call(runtime_db_err_too_many_rows_fn(), [
                        Call("collected.length", []),
                        Ident("sql"),
                      ]),
                    ]),
                  ),
                ]),
              ),
              (
                "Err(TooManyRows(n, _))",
                Call("Err", [
                  Call(runtime_db_err_too_many_rows_fn(), [
                    Ident("n"),
                    Ident("sql"),
                  ]),
                ]),
              ),
              ("Err(e)", Call("Err", [Ident("e")])),
            ]),
          ),
          ("Err(e)", Call("Err", [Ident("e")])),
        ]),
      ]
    ManyRows => {
      let ok_arm = (
        "Ok(rows)",
        Block([
          Let("result", ArrLit([])),
          ForLoop("row", Ident("rows"), [
            Match(Call(result_type + "::decode", [Ident("row")]), [
              ("Ok(v)", Call("result.push", [Ident("v")])),
              ("Err(e)", Return(Call("Err", [Ident("e")]))),
            ]),
          ]),
          Call("Ok", [Ident("result")]),
        ]),
      )
      let err_arm = ("Err(e)", Call("Err", [Ident("e")]))
      let collect_arms : Array[(String, Expr)] = if emit_empty_slices {
        [ok_arm, ("Err(NoRows)", Call("Ok", [ArrLit([])])), err_arm]
      } else {
        [ok_arm, err_arm]
      }
      [
        sql_let,
        params_let,
        Match(Call(conn_name + ".query", [Ident("sql"), Ident("params")]), [
          ("Ok(iter)", Match(Call("iter.collect", []), collect_arms)),
          ("Err(e)", Call("Err", [Ident("e")])),
        ]),
      ]
    }
    ExecResult =>
      match result_shape {
        // RETURNING * — use query_row + decode (single row)
        Rows(_) =>
          [
            sql_let,
            params_let,
            Match(
              Call(conn_name + ".query_row", [Ident("sql"), Ident("params")]),
              [
                ("Ok(row)", Call(result_type + "::decode", [Ident("row")])),
                ("Err(e)", Call("Err", [Ident("e")])),
              ],
            ),
          ]
        None => {
          let method_name = match query.raw_cmd {
            CopyFrom => ".copyfrom"
            Batch => ".batch"
            ExecLastId => ".execlastid"
            _ => ".exec"
          }
          let conn_call = Call(conn_name + method_name, [
            Ident("sql"),
            Ident("params"),
          ])
          let (zero_or_n, one_or_n) = match query.raw_cmd {
            ExecLastId => ("n", "1L")
            _ => ("0L", "n")
          }
          [
            sql_let,
            params_let,
            Match(conn_call, [
              (
                "Ok(n)",
                Call("Ok", [
                  Call(runtime_exec_result_new_fn(), [
                    Ident(zero_or_n),
                    Ident(one_or_n),
                  ]),
                ]),
              ),
              ("Err(e)", Call("Err", [Ident("e")])),
            ]),
          ]
        }
      }
    ExecCount =>
      match result_shape {
        // RETURNING * — use query + collect + decode (multiple rows)
        Rows(_) => {
          let ok_arm = (
            "Ok(rows)",
            Block([
              Let("result", ArrLit([])),
              ForLoop("row", Ident("rows"), [
                Match(Call(result_type + "::decode", [Ident("row")]), [
                  ("Ok(v)", Call("result.push", [Ident("v")])),
                  ("Err(e)", Return(Call("Err", [Ident("e")]))),
                ]),
              ]),
              Call("Ok", [Ident("result")]),
            ]),
          )
          let err_arm = ("Err(e)", Call("Err", [Ident("e")]))
          let collect_arms : Array[(String, Expr)] = if emit_empty_slices {
            [ok_arm, ("Err(NoRows)", Call("Ok", [ArrLit([])])), err_arm]
          } else {
            [ok_arm, err_arm]
          }
          [
            sql_let,
            params_let,
            Match(Call(conn_name + ".query", [Ident("sql"), Ident("params")]), [
              ("Ok(iter)", Match(Call("iter.collect", []), collect_arms)),
              ("Err(e)", Call("Err", [Ident("e")])),
            ]),
          ]
        }
        None =>
          [
            sql_let,
            params_let,
            Call(conn_name + ".execrows", [Ident("sql"), Ident("params")]),
          ]
      }
  }
}

///|
/// Convert PascalCase or camelCase to snake_case.
/// Handles: "GetUser" → "get_user", "getUser" → "get_user", "list_users" → "list_users"
fn to_snake_case(s : String) -> String {
  let mut result = ""
  let mut i = 0
  while i < s.length() {
    if s[i] >= 'A' && s[i] <= 'Z' {
      if i > 0 {
        if s[i - 1] >= 'a' && s[i - 1] <= 'z' {
          result = result + "_"
        } else if i + 1 < s.length() && s[i + 1] >= 'a' && s[i + 1] <= 'z' {
          result = result + "_"
        }
      }
    }
    result = result + s[i:i + 1].to_owned()
    i = i + 1
  }
  result.to_lower()
}

///|
/// Generate a single query function FnDef.
/// emit_methods_with_db_argument is accepted for API compatibility with sqlc's other plugins.
/// MoonBit 0.1 always generates standalone functions with DB as first param (see generate_query_fns).
pub fn generate_query_fn(
  query : InternalQuery,
  conn_name : String,
  conn_ty : TypeExpr,
  overrides : Array[(String, String)],
  nullable_overrides : Array[String],
  emit_sql_as_comment : Bool,
  _emit_methods_with_db_argument : Bool,
  emit_empty_slices : Bool,
  initialisms : Array[String],
) -> FnDef {
  let fn_name = if conn_name == "tx" {
    "query_" + to_snake_case(query.name) + "_tx"
  } else {
    "query_" + to_snake_case(query.name)
  }
  let result_type = get_result_type_name(query, initialisms)
  // First param is connection (db: @runtime.DB or tx: @runtime.Transaction)
  let conn_param : ParamDef = {
    name: conn_name,
    ty: qual_runtime_type(conn_ty),
  }
  // Convert IR params to AST params
  let query_params = query.params.map(fn(p) {
    let pd = param_to_param_def(p, overrides, nullable_overrides)
    { name: pd.name, ty: qual_runtime_type(pd.ty) }
  })
  let all_params = [conn_param] + query_params
  // Return type (respects RETURNING * for ExecResult/ExecCount)
  let return_ty = qual_runtime_type(
    build_return_ty(query.cmd, result_type, query.result_shape),
  )
  // Body (respects RETURNING * for ExecResult/ExecCount)
  let body = build_body(
    query,
    result_type,
    conn_name,
    query.result_shape,
    overrides,
    nullable_overrides,
    emit_empty_slices,
  )
  // Doc comment — if emit_sql_as_comment is true, include SQL text in doc comment
  let doc = if query.comments.length() > 0 {
    let comment = query.comments[0]
    if emit_sql_as_comment {
      Some(comment + "\n\n" + query.text)
    } else {
      Some(comment)
    }
  } else if emit_sql_as_comment {
    Some(query.text)
  } else {
    None
  }
  { name: fn_name, params: all_params, return_ty, body, doc, is_pub: true }
}

///|
/// Whether a raw QueryCmd is supported on the Transaction struct.
/// Transaction only has exec/execrows/query/query_row — no copyfrom/batch/execlastid.
fn supports_transaction(cmd : QueryCmd) -> Bool {
  match cmd {
    One | Many | Exec | ExecRows => true
    CopyFrom | Batch | ExecLastId => false
  }
}

///|
/// Generate all query function TopLevel items from IR queries.
/// Always generates a `db: DB` overload. Only generates a `tx: Transaction` overload
/// for queries whose `raw_cmd` is supported by Transaction (One, Many, Exec, ExecRows).
///
/// emit_methods_with_db_argument:
///   The current codegen strategy always produces standalone functions with `db: DB` as
///   the first parameter (e.g., `pub fn query_list_users(db: DB) -> ...`). This is the
///   only feasible approach in MoonBit 0.1 since trait objects and method dispatch are
///   not fully supported. Both `true` and `false` produce identical output — standalone
///   functions with a DB argument. A future version might use `fn DB::query_list_users()`
///   method receiver syntax when MoonBit supports it, at which point `false` could mean
///   "emit as method on DB type". For now, the option is accepted for API compatibility
///   with sqlc's other plugins but is a no-op.
pub fn generate_query_fns(
  queries : Array[InternalQuery],
  overrides : Array[(String, String)],
  nullable_overrides : Array[String],
  emit_sql_as_comment : Bool,
  emit_methods_with_db_argument : Bool,
  emit_empty_slices : Bool,
  initialisms : Array[String],
) -> Array[TopLevel] {
  let items : Array[TopLevel] = []
  for q in queries {
    items.push(
      Fn(
        generate_query_fn(
          q,
          "db",
          Named("DB"),
          overrides,
          nullable_overrides,
          emit_sql_as_comment,
          emit_methods_with_db_argument,
          emit_empty_slices,
          initialisms,
        ),
      ),
    )
    if supports_transaction(q.raw_cmd) {
      items.push(
        Fn(
          generate_query_fn(
            q,
            "tx",
            Named("Transaction"),
            overrides,
            nullable_overrides,
            emit_sql_as_comment,
            emit_methods_with_db_argument,
            emit_empty_slices,
            initialisms,
          ),
        ),
      )
    }
  }
  items
}

// === Tests ===

///|
test "get_result_type_name: basic" {
  let q : InternalQuery = {
    name: "get_user",
    text: "",
    cmd: OneRow,
    raw_cmd: One,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let name = get_result_type_name(q, [])
  inspect(name, content="GetUserRow")
}

///|
test "param_to_param_def: not_null param" {
  let p : InternalParameter = {
    number: 1,
    name: "id",
    ty: Named("int4"),
    not_null: true,
  }
  let pd = param_to_param_def(p, [], [])
  inspect(pd.name, content="id")
  inspect(pd.ty, content="Named(\"Int64\")")
}

///|
test "param_to_param_def: nullable param" {
  let p : InternalParameter = {
    number: 1,
    name: "email",
    ty: Named("text"),
    not_null: false,
  }
  let pd = param_to_param_def(p, [], [])
  inspect(pd.ty, content="Option(Named(\"String\"))")
}

///|
test "build_return_ty: OneRow returns Result[T, DBError]" {
  let t = build_return_ty(OneRow, "User", None)
  inspect(
    t,
    content="Parameterized(\"Result\", [Named(\"User\"), Named(\"@runtime.DBError\")])",
  )
}

///|
test "build_return_ty: ManyRows returns Result[Array[T], DBError]" {
  let t = build_return_ty(ManyRows, "User", None)
  inspect(
    t,
    content="Parameterized(\"Result\", [Parameterized(\"Array\", [Named(\"User\")]), Named(\"@runtime.DBError\")])",
  )
}

///|
test "build_return_ty: ExecResult returns Result[ExecResult, DBError]" {
  let t = build_return_ty(ExecResult, "User", None)
  inspect(
    t,
    content="Parameterized(\"Result\", [Named(\"@runtime.ExecResult\"), Named(\"@runtime.DBError\")])",
  )
}

///|
test "build_return_ty: ExecCount returns Result[Int64, DBError]" {
  let t = build_return_ty(ExecCount, "User", None)
  inspect(
    t,
    content="Parameterized(\"Result\", [Named(\"Int64\"), Named(\"@runtime.DBError\")])",
  )
}

///|
test "build_return_ty: ExecResult with Rows returns Result[T, DBError]" {
  let t = build_return_ty(
    ExecResult,
    "CreateUserRow",
    (
      Rows(
        Array::make(0, {
          name: "",
          ty: Named(""),
          not_null: false,
          comment: "",
          rename_to: (None : String?),
        }),
      ) : InternalResultShape),
  )
  inspect(
    t,
    content="Parameterized(\"Result\", [Named(\"CreateUserRow\"), Named(\"@runtime.DBError\")])",
  )
}

///|
test "build_return_ty: ExecCount with Rows returns Result[T, DBError]" {
  let t = build_return_ty(
    ExecCount,
    "UpdateUserRow",
    (
      Rows(
        Array::make(0, {
          name: "",
          ty: Named(""),
          not_null: false,
          comment: "",
          rename_to: (None : String?),
        }),
      ) : InternalResultShape),
  )
  inspect(
    t,
    content="Parameterized(\"Result\", [Named(\"UpdateUserRow\"), Named(\"@runtime.DBError\")])",
  )
}

///|
test "generate_query_fn: :one query has db param and result return" {
  let params : Array[InternalParameter] = [
    { number: 1, name: "id", ty: Named("int4"), not_null: true },
  ]
  let q : InternalQuery = {
    name: "get_user",
    text: "SELECT * FROM users WHERE id = $1",
    cmd: OneRow,
    raw_cmd: One,
    params,
    result_shape: (None : InternalResultShape),
    comments: ["Get a user by ID"],
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    false,
    false,
    false,
    [],
  )
  inspect(fn_def.name, content="query_get_user")
  inspect(fn_def.params.length(), content="2")
  inspect(fn_def.params[0].name, content="db")
  inspect(fn_def.params[0].ty, content="Named(\"@runtime.DB\")")
  inspect(fn_def.params[1].name, content="id")
  inspect(fn_def.params[1].ty, content="Named(\"Int64\")")
  inspect(
    fn_def.return_ty,
    content="Parameterized(\"Result\", [Named(\"GetUserRow\"), Named(\"@runtime.DBError\")])",
  )
  debug_inspect(fn_def.doc, content="Some(\"Get a user by ID\")")
  inspect(fn_def.is_pub, content="true")
}

///|
test "generate_query_fn: :many query has no params" {
  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: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  inspect(fn_def.name, content="query_list_users")
  inspect(fn_def.params.length(), content="1")
  inspect(fn_def.params[0].name, content="db")
  inspect(
    fn_def.return_ty,
    content="Parameterized(\"Result\", [Parameterized(\"Array\", [Named(\"ListUsersRow\")]), Named(\"@runtime.DBError\")])",
  )
}

///|
test "generate_query_fn: :exec query returns Result[ExecResult, DBError]" {
  let q : InternalQuery = {
    name: "delete_user",
    text: "DELETE FROM users WHERE id = $1",
    cmd: ExecResult,
    raw_cmd: Exec,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  inspect(fn_def.name, content="query_delete_user")
  inspect(
    fn_def.return_ty,
    content="Parameterized(\"Result\", [Named(\"@runtime.ExecResult\"), Named(\"@runtime.DBError\")])",
  )
  inspect(fn_def.body.length(), content="3")
  let sql_text = match fn_def.body[0] {
    Let(_, StrLit(s)) => s
    _ => ""
  }
  inspect(sql_text, content="DELETE FROM users WHERE id = $1")
  // body[2] is now a Match wrapping the db.exec call
  let is_match = match fn_def.body[2] {
    Match(_, _) => true
    _ => false
  }
  inspect(is_match, content="true")
}

///|
test "generate_query_fn: :execrows query returns Result[Int64, DBError]" {
  let q : InternalQuery = {
    name: "update_user_email",
    text: "UPDATE users SET email = $1 WHERE id = $2",
    cmd: ExecCount,
    raw_cmd: ExecRows,
    params: [
      { number: 1, name: "email", ty: Named("text"), not_null: true },
      { number: 2, name: "id", ty: Named("int4"), not_null: true },
    ],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  inspect(
    fn_def.return_ty,
    content="Parameterized(\"Result\", [Named(\"Int64\"), Named(\"@runtime.DBError\")])",
  )
  inspect(fn_def.params.length(), content="3")
  inspect(fn_def.params[1].name, content="email")
  inspect(fn_def.params[2].name, content="id")
}

///|
test "generate_query_fn: full function emission matches expected pattern" {
  let q : InternalQuery = {
    name: "get_user",
    text: "SELECT id, name FROM users WHERE id = $1",
    cmd: OneRow,
    raw_cmd: One,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: ["Get a user by ID"],
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  let sf : SourceFile = { items: [Fn(fn_def)] }
  let result = emit_source_file(sf)
  // P0-062: :one queries now use db.query() + collect_limited(2) for TooManyRows detection
  inspect(result.contains("match db.query(sql, params)"), content="true")
  inspect(result.contains("match rows.collect_limited(2)"), content="true")
  inspect(result.contains("TooManyRows"), content="true")
  inspect(result.contains("Err(e) => Err(e)"), content="true")
}

///|
test "to_snake_case: PascalCase GetUser converts to get_user" {
  let result = to_snake_case("GetUser")
  inspect(result, content="get_user")
}

///|
test "to_snake_case: camelCase getUser converts to get_user" {
  let result = to_snake_case("getUser")
  inspect(result, content="get_user")
}

///|
test "to_snake_case: already snake_case unchanged" {
  let result = to_snake_case("get_user")
  inspect(result, content="get_user")
}

///|
test "to_snake_case: PascalCase with acronym GetUserByID" {
  let result = to_snake_case("GetUserByID")
  inspect(result, content="get_user_by_id")
}

///|
test "build_body: CopyFrom raw_cmd uses conn.copyfrom" {
  let q : InternalQuery = {
    name: "copy_users",
    text: "COPY users FROM stdin",
    cmd: ExecResult,
    raw_cmd: CopyFrom,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "CopyUsersRow", "db", None, [], [], false)
  let call_name = match body[2] {
    Match(Call(name, _), _) => name
    _ => ""
  }
  inspect(call_name, content="db.copyfrom")
}

///|
test "build_body: Batch raw_cmd uses conn.batch" {
  let q : InternalQuery = {
    name: "batch_insert",
    text: "INSERT INTO t VALUES ($1)",
    cmd: ExecResult,
    raw_cmd: Batch,
    params: [{ number: 1, name: "val", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "BatchInsertRow", "db", None, [], [], false)
  let call_name = match body[2] {
    Match(Call(name, _), _) => name
    _ => ""
  }
  inspect(call_name, content="db.batch")
}

///|
test "build_body: ExecLastId raw_cmd uses conn.execlastid" {
  let q : InternalQuery = {
    name: "insert_user",
    text: "INSERT INTO users (name) VALUES ($1)",
    cmd: ExecResult,
    raw_cmd: ExecLastId,
    params: [{ number: 1, name: "name", ty: Named("text"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "InsertUserRow", "db", None, [], [], false)
  let call_name = match body[2] {
    Match(Call(name, _), _) => name
    _ => ""
  }
  inspect(call_name, content="db.execlastid")
}

///|
test "build_body: Exec raw_cmd uses conn.exec (default)" {
  let q : InternalQuery = {
    name: "delete_user",
    text: "DELETE FROM users WHERE id = $1",
    cmd: ExecResult,
    raw_cmd: Exec,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "DeleteUserRow", "db", None, [], [], false)
  let call_name = match body[2] {
    Match(Call(name, _), _) => name
    _ => ""
  }
  inspect(call_name, content="db.exec")
}

///|
test "build_body: ExecResult+None Exec constructs ExecResult::new(0L, n) in Match arm" {
  let q : InternalQuery = {
    name: "delete_user",
    text: "DELETE FROM users WHERE id = $1",
    cmd: ExecResult,
    raw_cmd: Exec,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "DeleteUserRow", "db", None, [], [], false)
  let (arm0_pat, arm0_arg0, arm0_arg1) = match body[2] {
    Match(
      _,
      [
        (
          "Ok(n)",
          Call(
            "Ok",
            [Call("@runtime.ExecResult::new", [Ident(zero), Ident(one)])]
          ),
        ),
        ("Err(e)", Call("Err", [Ident("e")])),
      ]
    ) => ("Ok(n)", zero, one)
    _ => ("", "", "")
  }
  inspect(arm0_pat, content="Ok(n)")
  inspect(arm0_arg0, content="0L")
  inspect(arm0_arg1, content="n")
}

///|
test "build_body: ExecResult+None CopyFrom constructs ExecResult::new(0L, n) in Match arm" {
  let q : InternalQuery = {
    name: "copy_users",
    text: "COPY users FROM stdin",
    cmd: ExecResult,
    raw_cmd: CopyFrom,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "CopyUsersRow", "db", None, [], [], false)
  let (arm0_pat, arm0_arg0, arm0_arg1) = match body[2] {
    Match(
      _,
      [
        (
          "Ok(n)",
          Call(
            "Ok",
            [Call("@runtime.ExecResult::new", [Ident(zero), Ident(one)])]
          ),
        ),
        ("Err(e)", Call("Err", [Ident("e")])),
      ]
    ) => ("Ok(n)", zero, one)
    _ => ("", "", "")
  }
  inspect(arm0_pat, content="Ok(n)")
  inspect(arm0_arg0, content="0L")
  inspect(arm0_arg1, content="n")
}

///|
test "build_body: ExecResult+None Batch constructs ExecResult::new(0L, n) in Match arm" {
  let q : InternalQuery = {
    name: "batch_insert",
    text: "INSERT INTO t VALUES ($1)",
    cmd: ExecResult,
    raw_cmd: Batch,
    params: [{ number: 1, name: "val", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "BatchInsertRow", "db", None, [], [], false)
  let (arm0_pat, arm0_arg0, arm0_arg1) = match body[2] {
    Match(
      _,
      [
        (
          "Ok(n)",
          Call(
            "Ok",
            [Call("@runtime.ExecResult::new", [Ident(zero), Ident(one)])]
          ),
        ),
        ("Err(e)", Call("Err", [Ident("e")])),
      ]
    ) => ("Ok(n)", zero, one)
    _ => ("", "", "")
  }
  inspect(arm0_pat, content="Ok(n)")
  inspect(arm0_arg0, content="0L")
  inspect(arm0_arg1, content="n")
}

///|
test "build_body: ExecResult+None ExecLastId constructs ExecResult::new(n, 1L) in Match arm" {
  let q : InternalQuery = {
    name: "insert_user",
    text: "INSERT INTO users (name) VALUES ($1)",
    cmd: ExecResult,
    raw_cmd: ExecLastId,
    params: [{ number: 1, name: "name", ty: Named("text"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "InsertUserRow", "db", None, [], [], false)
  let (arm0_pat, arm0_arg0, arm0_arg1) = match body[2] {
    Match(
      _,
      [
        (
          "Ok(n)",
          Call(
            "Ok",
            [Call("@runtime.ExecResult::new", [Ident(zero), Ident(one)])]
          ),
        ),
        ("Err(e)", Call("Err", [Ident("e")])),
      ]
    ) => ("Ok(n)", zero, one)
    _ => ("", "", "")
  }
  inspect(arm0_pat, content="Ok(n)")
  inspect(arm0_arg0, content="n")
  inspect(arm0_arg1, content="1L")
}

///|
test "build_body: CopyFrom with Rows uses query_row + decode (result_shape > raw_cmd)" {
  let q : InternalQuery = {
    name: "copy_users",
    text: "COPY users FROM stdin RETURNING id",
    cmd: ExecResult,
    raw_cmd: CopyFrom,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (
      Rows([
        {
          name: "id",
          ty: Named("int4"),
          not_null: true,
          comment: "",
          rename_to: (None : String?),
        },
      ]) : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let body = build_body(q, "CopyUsersRow", "db", q.result_shape, [], [], false)
  let call_name = match body[2] {
    Match(Call(name, _), _) => name
    _ => ""
  }
  inspect(call_name, content="db.query_row")
}

///|
test "generate_query_fn: PascalCase query name converted to snake_case fn name" {
  let params : Array[InternalParameter] = [
    { number: 1, name: "id", ty: Named("int4"), not_null: true },
  ]
  let q : InternalQuery = {
    name: "GetUser",
    text: "SELECT * FROM users WHERE id = $1",
    cmd: OneRow,
    raw_cmd: One,
    params,
    result_shape: (None : InternalResultShape),
    comments: ["Get a user by ID"],
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  inspect(fn_def.name, content="query_get_user")
}

///|
test "supports_transaction: One returns true" {
  inspect(supports_transaction(One), content="true")
}

///|
test "supports_transaction: Many returns true" {
  inspect(supports_transaction(Many), content="true")
}

///|
test "supports_transaction: Exec returns true" {
  inspect(supports_transaction(Exec), content="true")
}

///|
test "supports_transaction: ExecRows returns true" {
  inspect(supports_transaction(ExecRows), content="true")
}

///|
test "supports_transaction: CopyFrom returns false" {
  inspect(supports_transaction(CopyFrom), content="false")
}

///|
test "supports_transaction: Batch returns false" {
  inspect(supports_transaction(Batch), content="false")
}

///|
test "supports_transaction: ExecLastId returns false" {
  inspect(supports_transaction(ExecLastId), content="false")
}

///|
test "generate_query_fns: CopyFrom only generates 1 fn (no tx overload)" {
  let q : InternalQuery = {
    name: "copy_users",
    text: "COPY users FROM stdin",
    cmd: ExecResult,
    raw_cmd: CopyFrom,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="1")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
}

///|
test "generate_query_fns: Batch only generates 1 fn (no tx overload)" {
  let q : InternalQuery = {
    name: "batch_insert",
    text: "INSERT INTO t VALUES ($1)",
    cmd: ExecResult,
    raw_cmd: Batch,
    params: [{ number: 1, name: "val", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="1")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
}

///|
test "generate_query_fns: ExecLastId only generates 1 fn (no tx overload)" {
  let q : InternalQuery = {
    name: "insert_user",
    text: "INSERT INTO users (name) VALUES ($1)",
    cmd: ExecResult,
    raw_cmd: ExecLastId,
    params: [{ number: 1, name: "name", ty: Named("text"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="1")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
}

///|
test "generate_query_fns: One still generates 2 fns (db + tx)" {
  let q : InternalQuery = {
    name: "get_user",
    text: "SELECT * FROM users WHERE id = $1",
    cmd: OneRow,
    raw_cmd: One,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="2")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  let param1_name = match items[1] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
  inspect(param1_name, content="tx")
}

///|
test "generate_query_fns: Exec still generates 2 fns (db + tx)" {
  let q : InternalQuery = {
    name: "delete_user",
    text: "DELETE FROM users WHERE id = $1",
    cmd: ExecResult,
    raw_cmd: Exec,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="2")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  let param1_name = match items[1] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
  inspect(param1_name, content="tx")
}

///|
test "generate_query_fns: Many still generates 2 fns (db + tx)" {
  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: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="2")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  let param1_name = match items[1] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
  inspect(param1_name, content="tx")
}

///|
test "generate_query_fns: ExecRows still generates 2 fns (db + tx)" {
  let q : InternalQuery = {
    name: "update_email",
    text: "UPDATE users SET email = $1 WHERE id = $2",
    cmd: ExecCount,
    raw_cmd: ExecRows,
    params: [
      { number: 1, name: "email", ty: Named("text"), not_null: true },
      { number: 2, name: "id", ty: Named("int4"), not_null: true },
    ],
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let items = generate_query_fns([q], [], [], true, false, false, [])
  inspect(items.length(), content="2")
  let param0_name = match items[0] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  let param1_name = match items[1] {
    Fn(f) => f.params[0].name
    _ => ""
  }
  inspect(param0_name, content="db")
  inspect(param1_name, content="tx")
}

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

///|
test "P0-065: parameter name 'type' is escaped to 'ty'" {
  let p : InternalParameter = {
    number: 1,
    name: "type",
    ty: Named("text"),
    not_null: true,
  }
  let pd = param_to_param_def(p, [], [])
  inspect(pd.name, content="ty")
}

///|
test "P0-065: parameter name 'fn' is escaped to 'fn_'" {
  let p : InternalParameter = {
    number: 1,
    name: "fn",
    ty: Named("int4"),
    not_null: true,
  }
  let pd = param_to_param_def(p, [], [])
  inspect(pd.name, content="fn_")
}

///|
test "P0-065: non-keyword parameter name passes through" {
  let p : InternalParameter = {
    number: 1,
    name: "user_id",
    ty: Named("int4"),
    not_null: true,
  }
  let pd = param_to_param_def(p, [], [])
  inspect(pd.name, content="user_id")
}

///|
test "P0-065: get_result_type_name: query named 'type' produces TypeRow" {
  let q : InternalQuery = {
    name: "type",
    text: "",
    cmd: OneRow,
    raw_cmd: One,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let name = get_result_type_name(q, [])
  inspect(name, content="TypeRow")
}

///|
test "P0-065: get_result_type_name: query named 'int' produces IntRow" {
  let q : InternalQuery = {
    name: "int",
    text: "",
    cmd: OneRow,
    raw_cmd: One,
    params: Array::make(0, {
      number: 0,
      name: "",
      ty: Named(""),
      not_null: false,
    }),
    result_shape: (None : InternalResultShape),
    comments: Array::make(0, ""),
    filename: "",
  }
  let name = get_result_type_name(q, [])
  inspect(name, content="IntRow")
}

// === P0-062: :one query TooManyRows detection tests ===

///|
/// Helper to generate and emit a :one query function for string-based assertions.
fn emit_one_query_body() -> String {
  let q : InternalQuery = {
    name: "get_user",
    text: "SELECT id, name FROM users WHERE id = $1",
    cmd: OneRow,
    raw_cmd: One,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: ["Get a user by ID"],
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "db",
    Named("DB"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  let sf : SourceFile = { items: [Fn(fn_def)] }
  emit_source_file(sf)
}

///|
/// Helper to generate and emit a :one query function with tx connection for string-based assertions.
fn emit_one_query_tx_body() -> String {
  let q : InternalQuery = {
    name: "get_user",
    text: "SELECT id, name FROM users WHERE id = $1",
    cmd: OneRow,
    raw_cmd: One,
    params: [{ number: 1, name: "id", ty: Named("int4"), not_null: true }],
    result_shape: (None : InternalResultShape),
    comments: ["Get a user by ID"],
    filename: "q.sql",
  }
  let fn_def = generate_query_fn(
    q,
    "tx",
    Named("Transaction"),
    [],
    [],
    true,
    false,
    false,
    [],
  )
  let sf : SourceFile = { items: [Fn(fn_def)] }
  emit_source_file(sf)
}

///|
test "P0-062: :one query uses db.query() instead of db.query_row()" {
  let result = emit_one_query_body()
  inspect(result.contains("match db.query(sql, params)"), content="true")
  inspect(result.contains("db.query_row"), content="false")
}

///|
test "P0-062: :one query uses collect_limited(2) for row count guard" {
  let result = emit_one_query_body()
  inspect(result.contains("collect_limited(2)"), content="true")
}

///|
test "P0-062: :one query has Err(NoRows) arm for empty result" {
  let result = emit_one_query_body()
  inspect(
    result.contains("0 => Err(@runtime.db_err_no_rows())"),
    content="true",
  )
}

///|
test "P0-062: :one query decodes collected[0] for single row" {
  let result = emit_one_query_body()
  inspect(result.contains("GetUserRow::decode(collected[0])"), content="true")
}

///|
test "P0-062: :one query has Err(TooManyRows) arm for 2+ rows" {
  let result = emit_one_query_body()
  inspect(result.contains("Err(TooManyRows"), content="true")
}

///|
test "P0-062: :one query handles TooManyRows from collect_limited (3+ rows)" {
  let result = emit_one_query_body()
  inspect(result.contains("Err(TooManyRows(n, _))"), content="true")
  inspect(
    result.contains("Err(@runtime.db_err_too_many_rows(n, sql))"),
    content="true",
  )
}

///|
test "P0-062: :one query general error propagation preserved" {
  let result = emit_one_query_body()
  inspect(result.contains("Err(e) => Err(e)"), content="true")
}

///|
test "P0-062: :one query tx overload also uses tx.query() with TooManyRows detection" {
  let result = emit_one_query_tx_body()
  inspect(result.contains("match tx.query(sql, params)"), content="true")
  inspect(result.contains("tx.query_row"), content="false")
  inspect(result.contains("collect_limited(2)"), content="true")
}