# Nimbus
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Licensed under either of
#  * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
#    http://www.apache.org/licenses/LICENSE-2.0)
#  * MIT license ([LICENSE-MIT](LICENSE-MIT) or
#    http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.

import
  std/[os, sequtils, strformat, strutils, times],
  ./replay/[pp, gunzip],
  ../nimbus/core/[pow, pow/pow_cache, pow/pow_dataset],
  eth/[common],
  stew/endians2,
  unittest2

const
  baseDir = [".", "tests", ".." / "tests", $DirSep] # path containg repo
  repoDir = ["replay"]                              # alternative repos

  specsDump = "mainspecs2k.txt.gz"

# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------

proc say*(noisy = false; pfx = "***"; args: varargs[string, `$`]) =
  if noisy:
    if args.len == 0:
      echo "*** ", pfx
    elif 0 < pfx.len and pfx[^1] != ' ':
      echo pfx, " ", args.toSeq.join
    else:
      echo pfx, args.toSeq.join

proc findFilePath(file: string): string =
  result = "?unknown?" / file
  for dir in baseDir:
    for repo in repoDir:
      let path = dir / repo / file
      if path.fileExists:
        return path

# ------------------------------------------------------------------------------
# Test Runners
# ------------------------------------------------------------------------------

proc runPowTests(noisy = true; file = specsDump;
                 nVerify = int.high; nFakeMiner = 0, nRealMiner = 0) =
  let
    filePath = file.findFilePath
    fileInfo = file.splitFile.name.split(".")[0]

    powCache = PowCacheRef.new # so we can inspect the LRU caches
    powDataset = PowDatasetRef.new(cache = powCache)
    pow = PowRef.new(powCache, powDataset)

  var
    specsList: seq[PowSpecs]

  suite &"PoW: Header test specs from {fileInfo} capture":
    block:
      test "Loading from capture":
        for (lno,line) in gunzipLines(filePath):
          let specs = line.undumpPowSpecs
          if 0 < specs.blockNumber:
            specsList.add specs
            check line == specs.dumpPowSpecs
        noisy.say "***", " block range #",
          specsList[0].blockNumber, " .. #", specsList[^1].blockNumber

    # Adjust number of tests
    let
      startVerify = max(0, specsList.len - nVerify)
      startFakeMiner = max(0, specsList.len - nFakeMiner)
      startRealMiner = max(0, specsList.len - nRealMiner)

      nDoVerify = specsList.len - startVerify
      nDoFakeMiner = specsList.len - startFakeMiner
      nDoRealMiner = specsList.len - startRealMiner

      backStep = 1u64 shl 11

    block:
      test &"Running single getPowDigest() to fill the cache":
        if nVerify <= 0:
          skip()
        else:
          noisy.showElapsed(&"first getPowDigest() instance"):
            let p = specsList[startVerify]
            check pow.getPowDigest(p).mixDigest == p.mixDigest


      test &"Running getPowDigest() on {nDoVerify} specs records":
        if nVerify <= 0:
          skip()
        else:
          noisy.showElapsed(&"all {nDoVerify} getPowDigest() instances"):
            for n in startVerify ..< specsList.len:
              let p = specsList[n]
              check pow.getPowDigest(p).mixDigest == p.mixDigest


      test &"Generate PoW mining dataset (slow proocess)":
        if nDoFakeMiner <= 0 and nRealMiner <= 0:
          skip()
        else:
          noisy.showElapsed "generate PoW dataset":
            pow.generatePowDataset(specsList[startFakeMiner].blockNumber)


      test &"Running getNonce() on {nDoFakeMiner} instances with start" &
          &" nonce {backStep} before result":
        if nDoFakeMiner <= 0:
          skip()
        else:
          noisy.showElapsed &"all {nDoFakeMiner} getNonce() instances":
            for n in startFakeMiner ..< specsList.len:
              let
                p = specsList[n]
                nonce = toBytesBE(uint64.fromBytesBE(p.nonce) - backStep)
              check pow.getNonce(
                p.blockNumber, p.miningHash, p.difficulty, nonce) == p.nonce


      test &"Running getNonce() mining function" &
          &" on {nDoRealMiner} specs records":
        if nRealMiner <= 0:
          skip()
        else:
          for n in startRealMiner ..< specsList.len:
            let p = specsList[n]
            noisy.say "***", " #", p.blockNumber, " needs ", p.nonce.pp
            noisy.showElapsed("getNonce()"):
              let nonce = pow.getNonce(p)
              noisy.say "***", " got ", nonce.pp,
                " after ", pow.nGetNonce, " attempts"
              if nonce != p.nonce:
                var q = p
                q.nonce =  nonce
                check pow.getPowDigest(q).mixDigest == p.mixDigest

# ------------------------------------------------------------------------------
# Main function(s)
# ------------------------------------------------------------------------------

proc powMain*(noisy = defined(debug)) =
  noisy.runPowTests(nVerify = 100)

when isMainModule:
  # Note:
  #   0 < nFakeMiner: allow ~20 minuntes for building lookup table
  #   0 < nRealMiner: takes days/months/years ...
  true.runPowTests(nVerify = 200, nFakeMiner = 200, nRealMiner = 5)

# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------