// Copyright 2019 The etcd Authors
// Copyright 2026 Leo Cheng
//
// 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.
//
// The three benchmarks upstream ships, ported case for case:
// `BenchmarkStatus` and `BenchmarkRawNode` from `rawnode_test.go`, and
// `BenchmarkMajorityConfig_CommittedIndex` from `quorum/bench_test.go`. MoonBit
// ships no `go test -bench`, so this executable times the same operations with
// core/bench's monotonic clock and prints ns/op per sub-benchmark; the figures
// feed the README.

///|
/// Answer one request the way an idle peer would: grant the vote, or accept the
/// append at its last index. Other traffic is left alone.
#coverage.skip
fn answer(rn : @raft.RawNode, m : @raft.Message) -> Unit {
  match m.payload {
    Vote(a) =>
      rn.step(
        @raft.Message::new(
          m.to,
          m.from,
          VoteResp({ term: a.term, vote_granted: true, }),
        ),
      )
    Append(a) => {
      let match_index = if a.entries.is_empty() {
        a.prev_log_index
      } else {
        a.entries[a.entries.length() - 1].index
      }
      rn.step(
        @raft.Message::new(
          m.to,
          m.from,
          AppendResp({
            term: a.term,
            success: true,
            match_index,
            conflict_index: 0UL,
            conflict_term: 0UL,
            reject_index: 0UL,
          }),
        ),
      )
    }
    _ => ()
  }
}

///|
/// Drive every pending Ready of a node to quiescence, answering the peers it
/// writes to (upstream's `stabilize` closure in `benchmarkRawNodeImpl`). Returns
/// the last index handed to the application.
#coverage.skip
fn stabilize(rn : @raft.RawNode) -> UInt64 {
  let mut applied = 0UL
  while rn.has_ready() {
    let rd = rn.ready()
    let n = rd.committed_entries.length()
    if n > 0 {
      applied = rd.committed_entries[n - 1].index
    }
    rn.store(rd)
    for m in rd.messages {
      answer(rn, m)
    }
    rn.advance(rd)
  }
  applied
}

///|
/// Build a node with `members` voters (this one is server 1) and drive it to
/// leader, answering the election traffic the way a peer would.
#coverage.skip
fn leader_with(members : Int32) -> @raft.RawNode {
  let peers : Array[String] = []
  for i = 2; i <= members; i = i + 1 {
    peers.push(i.to_string())
  }
  let rn = @raft.RaftNode::new("1", peers, pre_vote=false).raw()
  rn.campaign()
  stabilize(rn) |> ignore
  rn
}

///|
/// Time `body` for `n` iterations and report ns/op. The returned accumulator
/// keeps `body`'s result alive so the optimizer cannot delete the measured work.
#coverage.skip
fn bench(name : String, n : Int32, body : () -> UInt64) -> UInt64 {
  for _warm = 0; _warm < 1000; _warm = _warm + 1 {
    ignore(body())
  }
  let t0 = @bench.monotonic_clock_start()
  let mut acc = 0UL
  for _i = 0; _i < n; _i = _i + 1 {
    acc = acc + body()
  }
  let us = @bench.monotonic_clock_end(t0)
  let ns_per_op = us * 1000.0 / n.to_double()
  println("\{name}: \{ns_per_op} ns/op  (\{n} ops)")
  acc
}

///|
/// `BenchmarkStatus`: the cost of reading a node's status for a cluster of
/// 1, 3, 5 and 100 members, through each accessor upstream times (`Status`, the
/// documented sum-the-matches example, `BasicStatus`, and `WithProgress` with
/// and without its example visitor).
#coverage.skip
fn bench_status() -> UInt64 {
  let mut sink = 0UL
  for members in [1, 3, 5, 100] {
    let rn = leader_with(members)
    let tag = "members=\{members}"
    sink = sink ^ bench("\{tag}/Status", 20000, fn() { rn.status().term })
    sink = sink ^
      bench("\{tag}/Status-example", 20000, fn() {
        // Upstream's `Status-example` sums the per-follower match indexes out of
        // the one struct; this port splits the progress view out of `RaftStatus`,
        // so the example sums it from `full_status`.
        let s = rn.full_status()
        let mut n = 0UL
        for p in s.progress {
          n = n + p.match_index
        }
        n
      })
    sink = sink ^
      bench("\{tag}/BasicStatus", 20000, fn() { rn.basic_status().term })
    sink = sink ^
      bench("\{tag}/WithProgress", 20000, fn() {
        rn.with_progress(fn(_id, _p) { () })
        0UL
      })
    sink = sink ^
      bench("\{tag}/WithProgress-example", 20000, fn() {
        let mut n = 0UL
        rn.with_progress(fn(_id, p) { n = n + p.match_index })
        n
      })
  }
  sink
}

///|
/// `BenchmarkRawNode`: one propose -> persist -> commit -> apply turn, for a
/// single-voter and a two-voter group, with the same applied-everything check
/// upstream makes.
#coverage.skip
fn bench_raw_node() -> UInt64 {
  let mut sink = 0UL
  for tc in [(1, "single-voter"), (2, "two-voters")] {
    let (members, name) = tc
    let rn = leader_with(members)
    let iterations = 5000
    let mut applied = 0UL
    let t0 = @bench.monotonic_clock_start()
    for _i = 0; _i < iterations; _i = _i + 1 {
      rn.propose(b"foo")
      applied = stabilize(rn)
    }
    let us = @bench.monotonic_clock_end(t0)
    let ns_per_op = us * 1000.0 / iterations.to_double()
    println("BenchmarkRawNode/\{name}: \{ns_per_op} ns/op  (\{iterations} ops)")
    if applied < iterations.to_uint64() {
      abort(
        "BenchmarkRawNode/\{name}: did not apply everything: \{applied} < \{iterations}",
      )
    }
    sink = sink ^ applied
  }
  sink
}

///|
/// `BenchmarkMajorityConfig_CommittedIndex`: the commit index of a simple
/// majority over 1, 3, 5, 7, 9 and 11 voters with assorted acknowledgement
/// indexes (upstream draws these at random; a fixed spread keeps runs
/// comparable while still mixing the acked and the lagging half).
#coverage.skip
fn bench_committed_index() -> UInt64 {
  let mut sink = 0UL
  let spread = [0, 7, 3, 11, 5, 13, 1, 9, 2, 15, 4]
  for n in [1, 3, 5, 7, 9, 11] {
    let ids : Array[String] = []
    let acked : Map[String, UInt64] = Map([])
    for i = 0; i < n; i = i + 1 {
      let id = (i + 1).to_string()
      ids.push(id)
      acked[id] = spread[i].to_uint64()
    }
    sink = sink ^
      bench("BenchmarkMajorityConfig_CommittedIndex/voters=\{n}", 200000, fn() {
        @raft.committed_index(ids, [], acked)
      })
  }
  sink
}

///|
#coverage.skip
fn main {
  let mut sink = 0UL
  sink = sink ^ bench_status()
  sink = sink ^ bench_raw_node()
  sink = sink ^ bench_committed_index()
  ignore(sink)
}