// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A dependency-free, in-memory reference [`Driver`]. It exists for two reasons:
/// to *prove the interface is implementable* end to end, and to give query layers
/// built on moondb (moonorm and friends) a real test double they can run against
/// with no database and no native backend — it compiles on every target.
///
/// It is a deliberately naive echo store, **not** a SQL engine: it does not parse
/// `sql`. Each `execute` appends one [`Row`] built from the bound `params` (columns
/// named `c0`, `c1`, …) and hands back an [`ExecResult`] with a monotonically
/// increasing `last_insert_id`; each `query` returns every stored row. What it
/// *does* model faithfully is the transaction bracket: `begin` snapshots the store,
/// `rollback` restores it, `commit` keeps the changes — so a test can assert real
/// rollback semantics against it.
pub struct MockDriver {
  mut rows : Array[Row]
  mut next_id : Int64
  savepoints : Array[Int32]
  // Named savepoints, each with the store length it must restore to. Kept apart from
  // `savepoints` because `begin` nests anonymously while `Txn` names its levels.
  named_savepoints : Array[(String, Int32)]
  mut closed : Bool
}

///|
/// A fresh, empty mock connection.
pub fn MockDriver::new() -> MockDriver {
  { rows: [], next_id: 0, savepoints: [], named_savepoints: [], closed: false, }
}

///|
/// Apply one of the three savepoint statements. `ROLLBACK TO` truncates the store to
/// the length recorded when the savepoint was taken and keeps the savepoint alive;
/// `RELEASE` drops it.
fn MockDriver::run_savepoint(self : MockDriver, sql : String) -> Unit {
  let head = sql.trim_start().to_upper()
  if head.has_prefix("SAVEPOINT ") {
    let name = sql.trim_start()[10:].to_owned().trim().to_owned()
    self.named_savepoints.push((name, self.rows.length()))
  } else if head.has_prefix("RELEASE SAVEPOINT ") {
    let name = sql.trim_start()[18:].to_owned().trim().to_owned()
    let mut i = self.named_savepoints.length() - 1
    while i >= 0 {
      if self.named_savepoints[i].0 == name {
        self.named_savepoints.remove(i) |> ignore
        return
      }
      i -= 1
    }
  } else if head.has_prefix("ROLLBACK TO SAVEPOINT ") {
    let name = sql.trim_start()[22:].to_owned().trim().to_owned()
    let mut i = self.named_savepoints.length() - 1
    while i >= 0 {
      if self.named_savepoints[i].0 == name {
        self.rows = self.rows[:self.named_savepoints[i].1].to_owned()
        return
      }
      i -= 1
    }
  }
}

///|
/// Whether `sql` is one of the three savepoint statements `Txn` issues. The mock does
/// not parse SQL; it recognises exactly the statements its own nesting helper sends
/// so a nested transaction does not turn into stored rows.
fn is_savepoint_statement(sql : String) -> Bool {
  let head = sql.trim_start().to_upper()
  head.has_prefix("SAVEPOINT ") ||
  head.has_prefix("RELEASE SAVEPOINT ") ||
  head.has_prefix("ROLLBACK TO SAVEPOINT ")
}

///|
/// How many rows the store currently holds. A test-facing helper, not part of the
/// [`Driver`] contract.
pub fn MockDriver::row_count(self : MockDriver) -> Int32 {
  self.rows.length()
}

///|
/// Whether the connection has been closed.
pub fn MockDriver::is_closed(self : MockDriver) -> Bool {
  self.closed
}

///|
/// Whether a transaction is currently open (at least one un-committed `begin`).
pub fn MockDriver::in_transaction(self : MockDriver) -> Bool {
  self.savepoints.length() > 0
}

///|
/// Record the statement's parameters as a row and report one row affected, with an
/// incrementing last-insert id. The SQL text is ignored on purpose: this driver
/// exists to prove the ORM issued a statement, not to interpret it.
pub impl Driver for MockDriver with fn execute(
  self : MockDriver,
  sql : String,
  params : Array[Value],
) -> ExecResult raise DbError {
  if self.closed {
    raise Closed
  }
  if is_savepoint_statement(sql) {
    // Transaction control is modeled here, not stored as a row: `Txn` drives its
    // nesting through `execute`, and the savepoint must undo exactly the rows written
    // after it — that scope is the thing a test asserts on.
    self.run_savepoint(sql)
    return { rows_affected: 0, last_insert_id: 0, }
  }
  let columns = []
  for i in 0.. Array[Row] raise DbError {
  if self.closed {
    raise Closed
  }
  self.rows[:].to_owned()
}

///|
/// Open a transaction, or a SAVEPOINT when one is already open, so nesting depth is
/// observable to a test.
pub impl Driver for MockDriver with fn begin(self : MockDriver) -> Unit raise DbError {
  if self.closed {
    raise Closed
  }
  self.savepoints.push(self.rows.length())
}

///|
/// Close the innermost transaction or release the innermost savepoint.
pub impl Driver for MockDriver with fn commit(self : MockDriver) -> Unit raise DbError {
  if self.closed {
    raise Closed
  }
  if self.savepoints.pop() is None {
    raise QueryError("no transaction to commit")
  }
}

///|
/// Undo back to the innermost savepoint, or abandon the transaction.
pub impl Driver for MockDriver with fn rollback(self : MockDriver) -> Unit raise DbError {
  if self.closed {
    raise Closed
  }
  match self.savepoints.pop() {
    Some(n) => self.rows = self.rows[:n].to_owned()
    None => raise QueryError("no transaction to roll back")
  }
}

///|
/// Mark the connection closed; every later call raises `Closed`.
pub impl Driver for MockDriver with fn close(self : MockDriver) -> Unit {
  self.closed = true
}

///|
pub extend MockDriver with Driver::{
  execute,
  begin,
  ping,
  close,
  query_stream,
  commit,
  query,
  rollback,
  prepare,
  dialect,
  capabilities,
}