///|
/// Models declared with `#orm` annotations. `models.g.mbt` is generated from
/// `models.mbt` by `cmd/orm`; a project wires the generator as a dev_build rule
/// (see the README). The program checks that the generated `Hero::model()`
/// renders and binds exactly as the hand-written model in `hand.mbt`, then runs a
/// round trip through a `Session`.
///
///     moon run src/examples/22-annotations --target native
fn render(params : Array[@moonorm.Value]) -> String {
  params.map(v => v.to_string()).join(", ")
}

///|
fn[T : Show] shown(x : T?) -> String {
  match x {
    Some(v) => v.to_string()
    None => "NULL"
  }
}

///|
fn same(what : String, generated : String, hand : String) -> Unit {
  if generated != hand {
    abort("\{what} differs:\n  generated: \{generated}\n  by hand:   \{hand}")
  }
}

///|
fn main {
  let dialects : Array[@moonorm.Dialect] = [Sqlite, Postgres, Mysql]
  println("-- CREATE TABLE, per dialect")
  for d in dialects {
    let sql = Team::model().create_table_sql_for(d)
    println(sql)
  }
  for d in dialects {
    let sql = Hero::model().create_table_sql_for(d)
    same("DDL", sql, hand_heroes.create_table_sql_for(d))
    println(sql)
    let indexes = Hero::model().create_indexes_sql_for(d, if_not_exists=true)
    same(
      "indexes",
      indexes.join("; "),
      hand_heroes.create_indexes_sql_for(d, if_not_exists=true).join("; "),
    )
    for statement in indexes {
      println(statement)
    }
  }
  let nova : Team = { id: Some(1), name: "Nova Corps", }
  let (team_sql, team_params) = Hero::team()
    .query({
      id: None,
      name: "",
      secret: "",
      age: None,
      power: 0,
      team_id: Some(1),
      email: Email("x@y"),
      visits: 0,
    })
    .build()
  let (roster_sql, roster_params) = Team::heroes().query(nova).build()
  same(
    "belongs_to",
    team_sql + render(team_params),
    {
      let (sql, params) = hand_team
        .query({
          id: None,
          name: "",
          secret: "",
          age: None,
          power: 0,
          team_id: Some(1),
          email: Email("x@y"),
          visits: 0,
        })
        .build()
      sql + render(params)
    },
  )
  same(
    "has_many",
    roster_sql + render(roster_params),
    {
      let (sql, params) = hand_roster.query(nova).build()
      sql + render(params)
    },
  )
  println("")
  println("-- relations")
  println(team_sql + "  [\{render(team_params)}]")
  println(roster_sql + "  [\{render(roster_params)}]")
  let fresh : Hero = {
    id: None,
    name: "Nova",
    secret: "Ana",
    age: None,
    power: 9.5,
    team_id: Some(1),
    email: Email("nova@example.com"),
    visits: 3,
  }
  let stored = { ..fresh, id: Some(7), age: Some(31), }
  println("")
  println("-- INSERT; a key the database assigns is left out")
  for h in [fresh, stored] {
    let (sql, params) = Hero::model().insert_of(h).build()
    let (hand_sql, hand_params) = hand_heroes.insert_of(h).build()
    same("INSERT", sql, hand_sql)
    same("parameters", render(params), render(hand_params))
    println(sql)
    println("  [\{render(params)}]")
  }
  println("")
  println("-- round trip through a Session")
  try {
    let sess = @moonorm.Session::new(@moondb.MockDriver::new())
    sess.insert_record(Hero::model(), stored) |> ignore
    let back = sess.all(Hero::model())
    let hand = sess.all(hand_heroes)
    for h in back {
      println(
        "  #\{shown(h.id)} \{h.name} age=\{shown(h.age)} team=\{shown(h.team_id)} email=\{h.email.0} visits=\{h.visits}",
      )
    }
    let bound = (h : Hero) => render((hand_heroes.to_columns)(h).map(p => p.1))
    same("rows", back.map(bound).join("; "), hand.map(bound).join("; "))
    sess.close()
  } catch {
    e => abort("db error: " + e.to_string())
  }
  println("")
  println("generated and hand-written models agree")
}