## Property-Based Test: Manifest Hash Determinism
##
## **Feature:** 01-nip-unified-storage-and-formats
## **Property 9:** Manifest Hash Determinism
## **Validates:** Requirements 6.4, 7.5
##
## **Property Statement:**
## For any manifest, calculating the hash twice SHALL produce identical results
##
## **Test Strategy:**
## 1. Generate random manifests with valid data (all three formats: NPK, NIP, NEXTER)
## 2. Calculate hash twice for each manifest
## 3. Verify hashes are identical (determinism)
## 4. Verify hash format is valid (xxh3-<hex>)
## 5. Verify different manifests produce different hashes (collision resistance)
## 6. Verify field order doesn't affect hash (sorted internally)

import std/[unittest, times, options, random, strutils, sets]
import nip/manifest_parser

# ============================================================================
# Test Generators
# ============================================================================

proc genSemanticVersion(): SemanticVersion =
  ## Generate random semantic version
  SemanticVersion(
    major: rand(0..10),
    minor: rand(0..20),
    patch: rand(0..50),
    prerelease: if rand(1) == 0: "alpha" & $rand(10) else: "",
    build: if rand(1) == 0: "build" & $rand(100) else: ""
  )

proc genDependencySpec(): DependencySpec =
  ## Generate random dependency specification
  DependencySpec(
    name: "dep-" & $rand(1000),
    versionConstraint: VersionConstraint(
      operator: [OpExact, OpGreater, OpGreaterEq, OpTilde, OpCaret][rand(4)],
      version: genSemanticVersion()
    ),
    optional: rand(1) == 0,
    features: if rand(1) == 0: @["feature" & $rand(5)] else: @[]
  )

proc genPackageManifest(format: FormatType): PackageManifest =
  ## Generate random package manifest
  var manifest = PackageManifest(
    format: format,
    name: "package-" & $rand(1000),
    version: genSemanticVersion(),
    license: ["MIT", "GPL-3.0", "Apache-2.0", "BSD-3-Clause"][rand(3)]
  )

  # Optional fields (randomly include)
  if rand(1) == 0:
    manifest.description = some("Description " & $rand(100))
  if rand(1) == 0:
    manifest.homepage = some("https://example.com/" & $rand(100))
  if rand(1) == 0:
    manifest.author = some("Author " & $rand(50))
  if rand(1) == 0:
    manifest.timestamp = some($now())

  # Dependencies (random count)
  let depCount = rand(0..5)
  for i in 0..<depCount:
    manifest.dependencies.add(genDependencySpec())

  # Build dependencies
  let buildDepCount = rand(0..3)
  for i in 0..<buildDepCount:
    manifest.buildDependencies.add(genDependencySpec())

  # Optional dependencies
  let optDepCount = rand(0..3)
  for i in 0..<optDepCount:
    var dep = genDependencySpec()
    dep.optional = true
    manifest.optionalDependencies.add(dep)

  # Build configuration
  if rand(1) == 0:
    manifest.buildSystem = some(["cmake", "meson", "autotools"][rand(2)])

  let flagCount = rand(0..5)
  for i in 0..<flagCount:
    manifest.buildFlags.add("--flag" & $rand(10))

  let configCount = rand(0..5)
  for i in 0..<configCount:
    manifest.configureFlags.add("--with-" & $rand(10))

  # Runtime configuration
  if rand(1) == 0:
    manifest.libc = some(["musl", "glibc"][rand(1)])
  if rand(1) == 0:
    manifest.allocator = some(["jemalloc", "tcmalloc", "default"][rand(2)])

  # Platform constraints
  let osCount = rand(1..3)
  for i in 0..<osCount:
    let os = ["linux", "freebsd", "openbsd", "netbsd"][rand(3)]
    if os notin manifest.supportedOS:
      manifest.supportedOS.add(os)

  let archCount = rand(1..3)
  for i in 0..<archCount:
    let arch = ["x86_64", "aarch64", "riscv64"][rand(2)]
    if arch notin manifest.supportedArchitectures:
      manifest.supportedArchitectures.add(arch)

  let capCount = rand(0..3)
  for i in 0..<capCount:
    manifest.requiredCapabilities.add("cap-" & $rand(10))

  # Integrity hashes
  manifest.buildHash = "xxh3-" & $rand(high(int))
  manifest.sourceHash = "xxh3-" & $rand(high(int))
  manifest.artifactHash = "xxh3-" & $rand(high(int))

  # Metadata
  let tagCount = rand(0..5)
  for i in 0..<tagCount:
    manifest.tags.add("tag" & $rand(20))

  let maintainerCount = rand(0..3)
  for i in 0..<maintainerCount:
    manifest.maintainers.add("maintainer" & $rand(50))

  # UTCP support
  if rand(1) == 0:
    manifest.utcpEndpoint = some("https://utcp.example.com/" & $rand(100))
  if rand(1) == 0:
    manifest.utcpVersion = some("1.0." & $rand(10))

  return manifest

# ============================================================================
# Property-Based Tests
# ============================================================================

suite "Manifest Hash Determinism Property Tests":
  test "Property 9: Hash Determinism - Same manifest produces same hash":
    ## **Feature:** 01-nip-unified-storage-and-formats, Property 9: Manifest Hash Determinism
    ## **Validates:** Requirements 6.4, 7.5
    ##
    ## For any manifest, calculating the hash twice should produce identical results.

    var passCount = 0
    var failCount = 0
    var errors: seq[string] = @[]

    # Test all three format types
    for format in [NPK, NIP, NEXTER]:
      for i in 0..<33:  # 33 iterations per format = 99 total (close to 100)
        try:
          # Generate random manifest
          let manifest = genPackageManifest(format)

          # Calculate hash twice
          let hash1 = calculateManifestHash(manifest)
          let hash2 = calculateManifestHash(manifest)

          # Verify they're identical
          if hash1 == hash2:
            passCount.inc()
          else:
            failCount.inc()
            errors.add($format & " iteration " & $i & ": Hashes differ: " & hash1 & " vs " & hash2)
        except CatchableError as e:
          failCount.inc()
          errors.add($format & " iteration " & $i & ": " & e.msg)

    # Report results
    echo "\nProperty Test Results (Determinism):"
    echo "  Passed: ", passCount, "/99"
    echo "  Failed: ", failCount, "/99"

    if errors.len > 0:
      echo "\nFirst 5 errors:"
      for i in 0..<min(5, errors.len):
        echo "  ", errors[i]

    # Property should hold for all cases
    check passCount == 99

  test "Property 9: Hash Format - Valid xxh3 format":
    ## Verify that calculated hashes have valid xxh3 format

    var passCount = 0
    var failCount = 0

    for format in [NPK, NIP, NEXTER]:
      for i in 0..<33:
        try:
          let manifest = genPackageManifest(format)
          let hash = calculateManifestHash(manifest)

          # Check format
          if hash.startsWith("xxh3-") and hash.len > 5:
            passCount.inc()
          else:
            failCount.inc()
            echo $format, " iteration ", i, ": Invalid hash format: ", hash
        except CatchableError as e:
          failCount.inc()
          echo $format, " iteration ", i, ": ", e.msg

    echo "\nHash Format Test Results:"
    echo "  Passed: ", passCount, "/99"
    echo "  Failed: ", failCount, "/99"

    check passCount == 99

  test "Property 9: Collision Resistance - Different manifests produce different hashes":
    ## Verify that different manifests produce different hashes
    ## (collision resistance property)

    var hashes: HashSet[string]
    var collisionCount = 0
    var totalCount = 0

    for format in [NPK, NIP, NEXTER]:
      for i in 0..<33:
        let manifest = genPackageManifest(format)
        let hash = calculateManifestHash(manifest)

        if hash in hashes:
          collisionCount.inc()
          echo "Collision detected: ", hash
        else:
          hashes.incl(hash)

        totalCount.inc()

    echo "\nCollision Resistance Test Results:"
    echo "  Total manifests: ", totalCount
    echo "  Unique hashes: ", hashes.len
    echo "  Collisions: ", collisionCount

    # With 99 random manifests, we should have 99 unique hashes
    # (collision probability with xxh3-128 is < 2^-100)
    check collisionCount == 0
    check hashes.len == totalCount

  test "Property 9: Field Order Independence - Sorted fields produce consistent hash":
    ## Verify that field order doesn't affect hash
    ## (internal sorting ensures determinism)

    var passCount = 0
    var failCount = 0

    for i in 0..<100:
      try:
        # Create manifest with dependencies in random order
        var manifest1 = genPackageManifest(NPK)
        manifest1.dependencies = @[
          DependencySpec(name: "dep-c", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
          DependencySpec(name: "dep-a", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
          DependencySpec(name: "dep-b", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[])
        ]

        # Create same manifest with dependencies in different order
        var manifest2 = manifest1
        manifest2.dependencies = @[
          DependencySpec(name: "dep-a", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
          DependencySpec(name: "dep-b", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[]),
          DependencySpec(name: "dep-c", versionConstraint: VersionConstraint(operator: OpAny, version: SemanticVersion()), optional: false, features: @[])
        ]

        # Hashes should be identical (sorted internally)
        let hash1 = calculateManifestHash(manifest1)
        let hash2 = calculateManifestHash(manifest2)

        if hash1 == hash2:
          passCount.inc()
        else:
          failCount.inc()
          echo "Iteration ", i, ": Field order affected hash"
      except CatchableError as e:
        failCount.inc()
        echo "Iteration ", i, ": ", e.msg

    echo "\nField Order Independence Test Results:"
    echo "  Passed: ", passCount, "/100"
    echo "  Failed: ", failCount, "/100"

    check passCount == 100

  test "Property 9: Hash Verification - verifyManifestHash works correctly":
    ## Test the hash verification function

    var passCount = 0
    var failCount = 0

    for format in [NPK, NIP, NEXTER]:
      for i in 0..<33:
        try:
          let manifest = genPackageManifest(format)
          let hash = calculateManifestHash(manifest)

          # Verify with correct hash
          if verifyManifestHash(manifest, hash):
            passCount.inc()
          else:
            failCount.inc()
            echo $format, " iteration ", i, ": Verification failed for correct hash"

          # Verify with incorrect hash (should fail)
          let wrongHash = "xxh3-wrong-hash"
          if not verifyManifestHash(manifest, wrongHash):
            passCount.inc()
          else:
            failCount.inc()
            echo $format, " iteration ", i, ": Verification passed for wrong hash"
        except CatchableError as e:
          failCount.inc()
          echo $format, " iteration ", i, ": ", e.msg

    echo "\nHash Verification Test Results:"
    echo "  Passed: ", passCount, "/198"  # 99 correct + 99 incorrect
    echo "  Failed: ", failCount, "/198"

    check passCount == 198

  test "Property 9: Minimal Manifest - Hash works with minimal required fields":
    ## Test that hash calculation works with only required fields

    var passCount = 0
    var failCount = 0

    for format in [NPK, NIP, NEXTER]:
      for i in 0..<33:
        try:
          # Create minimal manifest (only required fields)
          let manifest = PackageManifest(
            format: format,
            name: "minimal-" & $i,
            version: SemanticVersion(major: 1, minor: 0, patch: 0),
            license: "MIT",
            buildHash: "xxh3-build",
            sourceHash: "xxh3-source",
            artifactHash: "xxh3-artifact"
          )

          # Calculate hash twice
          let hash1 = calculateManifestHash(manifest)
          let hash2 = calculateManifestHash(manifest)

          if hash1 == hash2 and hash1.startsWith("xxh3-"):
            passCount.inc()
          else:
            failCount.inc()
            echo $format, " iteration ", i, ": Minimal manifest hash failed"
        except CatchableError as e:
          failCount.inc()
          echo $format, " iteration ", i, ": ", e.msg

    echo "\nMinimal Manifest Test Results:"
    echo "  Passed: ", passCount, "/99"
    echo "  Failed: ", failCount, "/99"

    check passCount == 99

when isMainModule:
  # Run tests
  randomize()
  echo "Running Manifest Hash Determinism Property Tests..."
  echo "Testing Property 9: Manifest Hash Determinism"
  echo "Validates: Requirements 6.4, 7.5"
  echo ""
