## Test suite for Merkle Tree implementation

import unittest
import std/[options]
import ../src/nimpak/merkle_tree

suite "Merkle Tree Building":
  test "Build tree from empty file list":
    let files: seq[FileEntry] = @[]
    let result = buildTreeFromFiles(files, "xxh3")

    check result.isOk
    let tree = result.get()
    check tree.nodeCount == 1
    check tree.leafCount == 1
    check tree.root.hash.len > 0

  test "Build tree from single file":
    let files = @[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ]
    let result = buildTreeFromFiles(files, "xxh3")

    check result.isOk
    let tree = result.get()
    check tree.nodeCount == 1
    check tree.leafCount == 1
    check tree.root.path == "file1.txt"
    check tree.root.hash == "xxh3-abc123"

  test "Build tree from multiple files":
    let files = @[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200),
      FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
    ]
    let result = buildTreeFromFiles(files, "xxh3")

    check result.isOk
    let tree = result.get()
    check tree.leafCount == 3
    check tree.nodeCount > 3  # Should have internal nodes
    check tree.root.hash.len > 0

  test "Tree structure is deterministic":
    let files = @[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ]

    let result1 = buildTreeFromFiles(files, "xxh3")
    let result2 = buildTreeFromFiles(files, "xxh3")

    check result1.isOk
    check result2.isOk
    check result1.get().root.hash == result2.get().root.hash

  test "Files are sorted by path":
    let files = @[
      FileEntry(path: "zzz.txt", hash: "xxh3-zzz", size: 100),
      FileEntry(path: "aaa.txt", hash: "xxh3-aaa", size: 200),
      FileEntry(path: "mmm.txt", hash: "xxh3-mmm", size: 300)
    ]

    let result = buildTreeFromFiles(files, "xxh3")
    check result.isOk

    let tree = result.get()
    let leaves = getAllLeaves(tree)
    check leaves[0].path == "aaa.txt"
    check leaves[1].path == "mmm.txt"
    check leaves[2].path == "zzz.txt"

suite "Merkle Tree Verification":
  test "Verify valid tree":
    let files = @[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ]

    let buildResult = buildTreeFromFiles(files, "xxh3")
    check buildResult.isOk

    let tree = buildResult.get()
    let verifyResult = verifyTree(tree)
    check verifyResult.isOk
    check verifyResult.get() == true

  test "Get verification statistics":
    let files = @[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200),
      FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
    ]

    let buildResult = buildTreeFromFiles(files, "xxh3")
    check buildResult.isOk

    let tree = buildResult.get()
    let statsResult = verifyTreeWithStats(tree)
    check statsResult.isOk

    let stats = statsResult.get()
    check stats.totalNodes == tree.nodeCount
    check stats.verifiedNodes > 0
    check stats.failedNodes == 0

suite "Merkle Tree Incremental Updates":
  test "Add file to tree":
    var tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let oldRootHash = tree.root.hash
    let result = tree.addFile("file2.txt", "xxh3-def456", 200)

    check result.isOk
    let newRootHash = result.get()
    check newRootHash != oldRootHash
    check tree.leafCount == 2

  test "Update file in tree":
    var tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ], "xxh3").get()

    let oldRootHash = tree.root.hash
    let result = tree.updateFile("file1.txt", "xxh3-newHash", 150)

    check result.isOk
    let newRootHash = result.get()
    check newRootHash != oldRootHash
    check tree.leafCount == 2

  test "Remove file from tree":
    var tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ], "xxh3").get()

    let oldRootHash = tree.root.hash
    let result = tree.removeFile("file1.txt")

    check result.isOk
    let newRootHash = result.get()
    check newRootHash != oldRootHash
    check tree.leafCount == 1

  test "Apply multiple changes":
    var tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let changes = @[
      FileChange(path: "file2.txt", changeType: Added, newHash: some("xxh3-def456"), newSize: some(200'i64)),
      FileChange(path: "file3.txt", changeType: Added, newHash: some("xxh3-ghi789"), newSize: some(300'i64)),
      FileChange(path: "file1.txt", changeType: Modified, newHash: some("xxh3-modified"), newSize: some(150'i64))
    ]

    let result = tree.applyChanges(changes)
    check result.isOk
    check tree.leafCount == 3

  test "Get update statistics":
    var tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let changes = @[
      FileChange(path: "file2.txt", changeType: Added, newHash: some("xxh3-def456"), newSize: some(200'i64))
    ]

    let result = tree.applyChangesWithStats(changes)
    check result.isOk

    let stats = result.get()
    check stats.changesApplied == 1
    check stats.oldRootHash != stats.newRootHash

suite "Merkle Tree Diffing":
  test "Diff identical trees":
    let files = @[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ]

    let tree1 = buildTreeFromFiles(files, "xxh3").get()
    let tree2 = buildTreeFromFiles(files, "xxh3").get()

    let result = diffTrees(tree1, tree2)
    check result.isOk
    check result.get().len == 0

  test "Diff trees with added file":
    let tree1 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let tree2 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ], "xxh3").get()

    let result = diffTrees(tree1, tree2)
    check result.isOk

    let diffs = result.get()
    check diffs.len == 1
    check diffs[0].path == "file2.txt"
    check diffs[0].diffType == OnlyInSecond

  test "Diff trees with removed file":
    let tree1 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ], "xxh3").get()

    let tree2 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let result = diffTrees(tree1, tree2)
    check result.isOk

    let diffs = result.get()
    check diffs.len == 1
    check diffs[0].path == "file2.txt"
    check diffs[0].diffType == OnlyInFirst

  test "Diff trees with modified file":
    let tree1 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let tree2 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-modified", size: 150)
    ], "xxh3").get()

    let result = diffTrees(tree1, tree2)
    check result.isOk

    let diffs = result.get()
    check diffs.len == 1
    check diffs[0].path == "file1.txt"
    check diffs[0].diffType == Different

  test "Get diff statistics":
    let tree1 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ], "xxh3").get()

    let tree2 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-modified", size: 150),
      FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
    ], "xxh3").get()

    let result = getDiffStats(tree1, tree2)
    check result.isOk

    let stats = result.get()
    check stats.onlyInFirst == 1  # file2.txt
    check stats.onlyInSecond == 1  # file3.txt
    check stats.different == 1  # file1.txt modified
    check stats.identical == 0

  test "Quick change detection":
    let tree1 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let tree2 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let tree3 = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-modified", size: 150)
    ], "xxh3").get()

    check not hasChanges(tree1, tree2)  # Identical
    check hasChanges(tree1, tree3)  # Different

suite "Merkle Tree Helper Functions":
  test "Find leaf by path":
    let tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
    ], "xxh3").get()

    let leaf = findLeafInTree(tree, "file1.txt")
    check leaf.isSome
    check leaf.get().path == "file1.txt"
    check leaf.get().hash == "xxh3-abc123"

  test "Find non-existent leaf":
    let tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let leaf = findLeafInTree(tree, "nonexistent.txt")
    check leaf.isNone

  test "Get all leaves":
    let tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
      FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200),
      FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
    ], "xxh3").get()

    let leaves = getAllLeaves(tree)
    check leaves.len == 3

  test "Get root hash":
    let tree = buildTreeFromFiles(@[
      FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
    ], "xxh3").get()

    let rootHash = getRootHash(tree)
    check rootHash.len > 0
    check rootHash == tree.root.hash
