## tests/test_hash_verifier.nim
## Comprehensive unit tests for hash verification module
##
## Includes golden test vectors and edge case testing

import std/[unittest, os, streams, strutils, tempfiles, options]
import ../src/nimpak/security/hash_verifier

suite "Hash Algorithm Detection":
  test "detect BLAKE2b hash formats":
    check detectHashAlgorithm("blake2b-abc123") == HashBlake2b
    check detectHashAlgorithm("a".repeat(128)) == HashBlake2b  # 128 hex chars = BLAKE2b-512

  test "detect BLAKE3 hash formats":
    # BLAKE3 is detected but falls back to BLAKE2b
    check detectHashAlgorithm("blake3-abc123") == HashBlake3

  test "detect SHA256 hash formats":
    check detectHashAlgorithm("sha256-abc123") == HashSha256
    check detectHashAlgorithm("a".repeat(64)) == HashSha256  # 64 hex chars = SHA256

  test "invalid hash formats":
    expect ValueError:
      discard detectHashAlgorithm("invalid-hash")
    expect ValueError:
      discard detectHashAlgorithm("abc")  # Too short

suite "Hash String Parsing":
  test "parse BLAKE2b hash strings":
    let (alg1, digest1) = parseHashString("blake2b-abc123def456")
    check alg1 == HashBlake2b
    check digest1 == "abc123def456"

    # Test with a valid 128-character BLAKE2b hash (without prefix)
    let validBlake2bHash = "a".repeat(128)
    let (alg2, digest2) = parseHashString(validBlake2bHash)
    check alg2 == HashBlake2b
    check digest2 == validBlake2bHash

  test "format hash strings":
    check formatHashString(HashBlake2b, "abc123") == "blake2b-abc123"
    check formatHashString(HashBlake3, "def456") == "blake3-def456"
    check formatHashString(HashSha256, "789abc") == "sha256-789abc"

suite "Golden Test Vectors":
  # These are known-good test vectors for BLAKE2b-512
  const BLAKE2B_GOLDEN_VECTORS = [
    ("", "blake2b-786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"),
    ("a", "blake2b-333fcb4ee1aa7c115355ec66ceac917c8bfd815bf7587d325aec1864edd24e34d5abe2c6b1b5ee3face62fed78dbef802f2a85cb91d455a8f5249d330853cb3c"),
    ("abc", "blake2b-ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923"),
    ("The quick brown fox jumps over the lazy dog", "blake2b-a8add4bdddfd93e4877d2746e62817b116364a1fa7bc148d95090bc7333b3673f82401cf7aa2e4cb1ecd90296e3f14cb5413f8ed77be73045b13914cdcd6a918"),
    ("NimPak", "blake2b-5274ed32fc344107b69805a6e77c9ab0dfc4c9b83d94a400bce3cc70d11f9d5c06738c319dace5b81859d56667550744b240f52d493dfc2e8afbbe76b00df1ae"),
    ("1234567890".repeat(10), "blake2b-800bb78cd4da18995c8074713bb6743cd94b2b6490a693fe4000ed00833b88b7b474d94af9cfed246b1b4ce1935a76154d7ea7c410493557741d18ec3a08da75")
  ]

  # SHA256 test vectors for legacy support
  const SHA256_GOLDEN_VECTORS = [
    ("", "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
    ("a", "sha256-ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"),
    ("abc", "sha256-ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
    ("The quick brown fox jumps over the lazy dog", "sha256-d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"),
    ("NimPak", "sha256-1ba47abd74914c637330282fad2a2eb807970548b532a552128d6de4af131cbf"),
    ("1234567890".repeat(10), "sha256-b20e12a7bcf7a0bcc5150265aab9c40b1d673781c143a73be76232d81e6038ec")
  ]

  test "BLAKE2b golden vectors":
    for (input, expectedHash) in BLAKE2B_GOLDEN_VECTORS:
      let result = computeStringHash(input, HashBlake2b)
      let formattedHash = formatHashString(result.algorithm, result.digest)
      check formattedHash == expectedHash
      check result.verified == false  # Not verified yet

      # Test verification
      let verifyResult = verifyStringHash(input, expectedHash)
      check verifyResult.verified == true
      check verifyResult.digest == result.digest

  test "SHA256 golden vectors":
    for (input, expectedHash) in SHA256_GOLDEN_VECTORS:
      let result = computeStringHash(input, HashSha256)
      let formattedHash = formatHashString(result.algorithm, result.digest)
      check formattedHash == expectedHash
      check result.verified == false  # Not verified yet

      # Test verification
      let verifyResult = verifyStringHash(input, expectedHash)
      check verifyResult.verified == true
      check verifyResult.digest == result.digest

suite "Streaming Hash Computation":
  test "streaming hasher initialization":
    let hasher = initStreamingHasher(HashBlake2b)
    check hasher.algorithm == HashBlake2b
    check hasher.bytesProcessed == 0

  test "streaming hash with multiple updates":
    var hasher = initStreamingHasher(HashBlake2b)
    hasher.update("The quick ")
    hasher.update("brown fox ")
    hasher.update("jumps over ")
    hasher.update("the lazy dog")

    let result = hasher.finalize()
    check hasher.bytesProcessed == 43  # Length of the string

    # Should match the golden vector for the full string
    let expectedResult = computeStringHash("The quick brown fox jumps over the lazy dog", HashBlake2b)
    check result.digest == expectedResult.digest

  test "streaming hash with byte arrays":
    var hasher = initStreamingHasher(HashBlake2b)
    let dataStr = "test data"
    hasher.update(dataStr.toOpenArrayByte(0, dataStr.high))

    let result = hasher.finalize()
    check hasher.bytesProcessed == 9

    # Should match direct string hash
    let expectedResult = computeStringHash("test data", HashBlake2b)
    check result.digest == expectedResult.digest

suite "File Hash Computation":
  test "compute hash of existing file":
    # Create a temporary file with known content
    let (tempFile, tempPath) = createTempFile("nimpak_test_", ".txt")
    tempFile.write("Hello, NimPak!")
    tempFile.close()

    try:
      let result = computeFileHash(tempPath, HashBlake2b)
      check result.algorithm == HashBlake2b
      check result.digest.len == 128  # BLAKE2b-512 hex length
      check result.computeTime >= 0.0

      # Verify the hash
      let expectedHash = formatHashString(result.algorithm, result.digest)
      let verifyResult = verifyFileHash(tempPath, expectedHash)
      check verifyResult.verified == true

    finally:
      removeFile(tempPath)

  test "file not found error":
    expect IOError:
      discard computeFileHash("/nonexistent/file.txt")

  test "large file handling":
    # Create a larger temporary file (1MB)
    let (tempFile, tempPath) = createTempFile("nimpak_large_", ".bin")
    let chunkData = "A".repeat(1024)  # 1KB chunk

    for i in 0..<1024:  # Write 1MB total
      tempFile.write(chunkData)
    tempFile.close()

    try:
      let result = computeFileHash(tempPath, HashBlake2b)
      check result.algorithm == HashBlake2b
      check result.digest.len == 128
      check result.computeTime > 0.0

      # Check that we can format the hash rate
      let stats = getHashStatistics(result, 1024 * 1024)
      check "MB/s" in stats or "KB/s" in stats or "GB/s" in stats

    finally:
      removeFile(tempPath)

suite "Stream Hash Computation":
  test "compute hash from string stream":
    let data = "Stream test data"
    var stream = newStringStream(data)

    let result = computeStreamHash(stream, HashBlake2b)
    check result.algorithm == HashBlake2b

    # Should match direct string hash
    let expectedResult = computeStringHash(data, HashBlake2b)
    check result.digest == expectedResult.digest

    stream.close()

suite "Hash Verification":
  test "successful hash verification":
    let data = "Verification test"
    let hashResult = computeStringHash(data, HashBlake2b)
    let hashString = formatHashString(hashResult.algorithm, hashResult.digest)

    let verifyResult = verifyStringHash(data, hashString)
    check verifyResult.verified == true
    check verifyResult.digest == hashResult.digest

  test "failed hash verification":
    let data = "Verification test"
    let wrongHash = "blake2b-" & "0".repeat(128)

    expect HashVerificationError:
      discard verifyStringHash(data, wrongHash)

  test "file hash verification":
    let (tempFile, tempPath) = createTempFile("nimpak_verify_", ".txt")
    tempFile.write("File verification test")
    tempFile.close()

    try:
      # Compute correct hash
      let hashResult = computeFileHash(tempPath, HashBlake2b)
      let hashString = formatHashString(hashResult.algorithm, hashResult.digest)

      # Verify should succeed
      let verifyResult = verifyFileHash(tempPath, hashString)
      check verifyResult.verified == true

      # Wrong hash should fail
      let wrongHash = "blake2b-" & "1".repeat(128)
      expect HashVerificationError:
        discard verifyFileHash(tempPath, wrongHash)

    finally:
      removeFile(tempPath)

suite "Batch Verification":
  test "verify multiple files":
    # Create multiple temporary files
    var entries: seq[FileHashEntry] = @[]
    var tempPaths: seq[string] = @[]

    for i in 0..<3:
      let (tempFile, tempPath) = createTempFile("nimpak_batch_", ".txt")
      tempFile.write($"File content " & $i)
      tempFile.close()
      tempPaths.add(tempPath)

      # Compute correct hash
      let hashResult = computeFileHash(tempPath, HashBlake2b)
      let hashString = formatHashString(hashResult.algorithm, hashResult.digest)

      entries.add(FileHashEntry(
        filePath: tempPath,
        expectedHash: hashString,
        result: none(HashResult),
        error: ""
      ))

    try:
      let (verified, failed) = verifyMultipleFiles(entries)
      check verified == 3
      check failed == 0

      for entry in entries:
        check entry.result.isSome()
        check entry.result.get().verified == true
        check entry.error == ""

    finally:
      for path in tempPaths:
        removeFile(path)

  test "batch verification with failures":
    var entries: seq[FileHashEntry] = @[
      FileHashEntry(
        filePath: "/nonexistent/file.txt",
        expectedHash: "blake2b-" & "0".repeat(128),
        result: none(HashResult),
        error: ""
      )
    ]

    let (verified, failed) = verifyMultipleFiles(entries)
    check verified == 0
    check failed == 1
    check entries[0].result.isNone()
    check entries[0].error != ""

suite "Performance and Statistics":
  test "hash rate formatting":
    check formatHashRate(1000, 1.0) == "1.0 KB/s"
    check formatHashRate(1_000_000, 1.0) == "1.0 MB/s"
    check formatHashRate(1_000_000_000, 1.0) == "1.0 GB/s"
    check formatHashRate(500, 1.0) == "500 B/s"
    check formatHashRate(1000, 0.0) == "N/A"

  test "hash statistics formatting":
    let result = HashResult(
      algorithm: HashBlake2b,
      digest: "abc123",
      verified: true,
      computeTime: 0.5
    )

    let stats = getHashStatistics(result, 1000)
    check "blake2b" in stats.toLower()
    check "0.500s" in stats
    check "KB/s" in stats

suite "Algorithm Fallback and Support":
  test "algorithm fallback":
    check getFallbackAlgorithm(HashBlake3) == HashBlake2b
    check getFallbackAlgorithm(HashBlake2b) == HashBlake2b
    check getFallbackAlgorithm(HashSha256) == HashSha256

  test "algorithm support detection":
    check isAlgorithmSupported(HashBlake2b) == true
    check isAlgorithmSupported(HashSha256) == true
    check isAlgorithmSupported(HashBlake3) == false  # Not natively supported

  test "BLAKE3 fallback behavior":
    # BLAKE3 should fallback to BLAKE2b
    let result = computeStringHash("test", HashBlake3)
    check result.algorithm == HashBlake2b  # Should have fallen back

    # Compare with direct BLAKE2b computation
    let blake2bResult = computeStringHash("test", HashBlake2b)
    check result.digest == blake2bResult.digest

suite "Large File Handling":
  test "large file hash computation":
    # Create a file larger than the threshold for testing
    let (tempFile, tempPath) = createTempFile("nimpak_large_", ".bin")
    let chunkData = "X".repeat(1024)  # 1KB chunk

    # Write 2MB total (smaller than 1GB threshold for testing)
    for i in 0..<2048:
      tempFile.write(chunkData)
    tempFile.close()

    try:
      var progressCalls = 0
      let result = computeLargeFileHash(tempPath, HashBlake2b) do (processed: int64, total: int64):
        inc progressCalls
        check processed <= total
        check processed >= 0

      check result.algorithm == HashBlake2b
      check result.digest.len == 128
      check result.computeTime > 0.0
      check progressCalls > 0  # Progress callback should have been called

      # Verify the result matches regular file hash
      let regularResult = computeFileHash(tempPath, HashBlake2b)
      check result.digest == regularResult.digest

    finally:
      removeFile(tempPath)

  test "chunk size optimization":
    # Test that large files use larger chunk sizes
    let (tempFile, tempPath) = createTempFile("nimpak_chunk_", ".bin")
    let smallData = "small"
    tempFile.write(smallData)
    tempFile.close()

    try:
      let result = computeFileHash(tempPath, HashBlake2b)
      check result.algorithm == HashBlake2b
      check result.digest.len == 128

    finally:
      removeFile(tempPath)

suite "Utility Functions":
  test "hash string validation":
    check isValidHashString("blake2b-" & "a".repeat(128)) == true
    check isValidHashString("sha256-" & "b".repeat(64)) == true
    check isValidHashString("invalid-hash") == false
    check isValidHashString("") == false

  test "preferred algorithm":
    check getPreferredHashAlgorithm() == HashBlake2b

  test "supported algorithms":
    let supported = getSupportedAlgorithms()
    check HashBlake2b in supported
    check HashSha256 in supported
    # BLAKE3 not yet natively supported
    check HashBlake3 notin supported

suite "Edge Cases and Error Handling":
  test "empty string hash":
    let result = computeStringHash("", HashBlake2b)
    check result.digest.len == 128
    check result.algorithm == HashBlake2b

  test "very long string hash":
    let longString = "A".repeat(100_000)  # 100KB string
    let result = computeStringHash(longString, HashBlake2b)
    check result.digest.len == 128
    check result.computeTime >= 0.0

  test "unsupported algorithm error":
    # BLAKE3 falls back to BLAKE2b, so no error
    let hasher1 = initStreamingHasher(HashBlake3)
    check hasher1.algorithm == HashBlake2b

    # SHA256 is supported
    let hasher2 = initStreamingHasher(HashSha256)
    check hasher2.algorithm == HashSha256

  test "hash verification error details":
    try:
      discard verifyStringHash("test", "blake2b-wrong")
      fail()
    except HashVerificationError as e:
      check e.algorithm == HashBlake2b
      check "wrong" in e.expectedHash
      check e.actualHash != e.expectedHash