## Property-Based Test: NIP Manifest Roundtrip
##
## **Feature:** 01-nip-unified-storage-and-formats
## **Property 3:** Manifest Roundtrip
## **Validates:** Requirements 6.4
##
## **Property Statement:**
## For any NIP manifest, parsing and regenerating SHALL produce semantically equivalent KDL
##
## **Test Strategy:**
## 1. Generate random NIP manifests with valid data
## 2. Convert manifest to KDL string
## 3. Parse KDL string back to manifest
## 4. Verify semantic equivalence (all fields match)
## 5. Verify determinism (same manifest = same KDL)

import std/[unittest, times, options, random, strutils]
import nip/nip_manifest
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)
  )

proc genAppInfo(): AppInfo =
  ## Generate random application metadata
  AppInfo(
    description: "Test application " & $rand(1000),
    homepage: some("https://example.com/" & $rand(1000)),
    license: ["MIT", "GPL-3.0", "Apache-2.0", "BSD-3-Clause"][rand(3)],
    author: some("Test Author " & $rand(100)),
    maintainer: some("Test Maintainer " & $rand(100)),
    tags: @["test", "example", "app" & $rand(10)],
    category: some(["Graphics", "Network", "Development", "Utility"][rand(3)])
  )

proc genProvenanceInfo(): ProvenanceInfo =
  ## Generate random provenance information
  ProvenanceInfo(
    source: "https://github.com/test/repo" & $rand(1000),
    sourceHash: "xxh3-" & $rand(high(int)),
    upstream: some("https://upstream.example.com/" & $rand(1000)),
    buildTimestamp: now(),
    builder: some("builder-" & $rand(100))
  )

proc genBuildConfiguration(): BuildConfiguration =
  ## Generate random build configuration
  BuildConfiguration(
    configureFlags: @["--enable-feature" & $rand(10), "--with-lib" & $rand(5)],
    compilerFlags: @["-O2", "-march=native", "-flto"],
    compilerVersion: "gcc-" & $rand(10..13) & ".0.0",
    targetArchitecture: ["x86_64", "aarch64", "riscv64"][rand(2)],
    libc: ["musl", "glibc"][rand(1)],
    allocator: ["jemalloc", "tcmalloc", "default"][rand(2)],
    buildSystem: ["cmake", "meson", "autotools"][rand(2)]
  )

proc genChunkReference(): ChunkReference =
  ## Generate random CAS chunk reference
  ChunkReference(
    hash: "xxh3-" & $rand(high(int)),
    size: rand(1024..1048576).int64,
    chunkType: [Binary, Library, Runtime, Config, Data][rand(4)],
    path: "bin/app" & $rand(10)
  )

proc genDesktopFileSpec(): DesktopFileSpec =
  ## Generate random desktop file specification
  DesktopFileSpec(
    name: "Test App " & $rand(100),
    genericName: some("Generic App " & $rand(100)),
    comment: some("A test application"),
    exec: "/usr/bin/testapp" & $rand(10),
    icon: "testapp" & $rand(10),
    terminal: rand(1) == 0,
    categories: @["Graphics", "Utility"],
    keywords: @["test", "example", "app"]
  )

proc genIconSpec(): IconSpec =
  ## Generate random icon specification
  IconSpec(
    size: [16, 32, 48, 64, 128, 256][rand(5)],
    path: "icons/icon" & $rand(10) & ".png",
    format: ["png", "svg"][rand(1)]
  )

proc genDesktopMetadata(): DesktopMetadata =
  ## Generate random desktop metadata
  DesktopMetadata(
    desktopFile: genDesktopFileSpec(),
    icons: @[genIconSpec(), genIconSpec()],
    mimeTypes: @["text/plain", "application/json"],
    appId: "org.example.testapp" & $rand(100)
  )

proc genFilesystemAccess(): FilesystemAccess =
  ## Generate random filesystem access permission
  FilesystemAccess(
    path: ["/home", "/tmp", "/var/cache"][rand(2)],
    mode: [ReadOnly, ReadWrite, Create][rand(2)]
  )

proc genDBusAccess(): DBusAccess =
  ## Generate random D-Bus access permissions
  DBusAccess(
    session: @["org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"],
    system: @["org.freedesktop.NetworkManager"],
    own: @["org.example.TestApp" & $rand(100)]
  )

proc genMount(): Mount =
  ## Generate random mount specification
  Mount(
    source: "/host/path" & $rand(10),
    target: "/app/path" & $rand(10),
    mountType: [Bind, Tmpfs, Devtmpfs][rand(2)],
    readOnly: rand(1) == 0
  )

proc genNamespaceConfig(): NamespaceConfig =
  ## Generate random namespace configuration
  NamespaceConfig(
    namespaceType: ["user", "strict", "none"][rand(2)],
    permissions: Permissions(
      network: rand(1) == 0,
      gpu: rand(1) == 0,
      audio: rand(1) == 0,
      camera: rand(1) == 0,
      microphone: rand(1) == 0,
      filesystem: @[genFilesystemAccess(), genFilesystemAccess()],
      dbus: genDBusAccess()
    ),
    mounts: @[genMount(), genMount()]
  )

proc genSignatureInfo(): SignatureInfo =
  ## Generate random signature information
  SignatureInfo(
    algorithm: "ed25519",
    keyId: "key-" & $rand(1000),
    signature: "sig-" & $rand(high(int))
  )

proc genNIPManifest(): NIPManifest =
  ## Generate random NIP manifest
  NIPManifest(
    name: "testapp" & $rand(1000),
    version: genSemanticVersion(),
    buildDate: now(),
    metadata: genAppInfo(),
    provenance: genProvenanceInfo(),
    buildConfig: genBuildConfiguration(),
    casChunks: @[genChunkReference(), genChunkReference(), genChunkReference()],
    desktop: genDesktopMetadata(),
    namespace: genNamespaceConfig(),
    buildHash: "xxh3-" & $rand(high(int)),
    signature: genSignatureInfo()
  )

# ============================================================================
# Semantic Equivalence Checks
# ============================================================================

proc checkTimestampsClose(a, b: DateTime, toleranceSeconds: int = 2): bool =
  ## Check if two timestamps are within tolerance (for parsing precision)
  let diff = abs((a - b).inSeconds)
  return diff <= toleranceSeconds

proc checkAppInfoEqual(a, b: AppInfo): bool =
  ## Check if two AppInfo objects are semantically equivalent
  result = a.description == b.description and
           a.homepage == b.homepage and
           a.license == b.license and
           a.author == b.author and
           a.maintainer == b.maintainer and
           a.tags == b.tags and
           a.category == b.category

proc checkProvenanceEqual(a, b: ProvenanceInfo): bool =
  ## Check if two ProvenanceInfo objects are semantically equivalent
  result = a.source == b.source and
           a.sourceHash == b.sourceHash and
           a.upstream == b.upstream and
           a.builder == b.builder and
           checkTimestampsClose(a.buildTimestamp, b.buildTimestamp)

proc checkBuildConfigEqual(a, b: BuildConfiguration): bool =
  ## Check if two BuildConfiguration objects are semantically equivalent
  result = a.configureFlags == b.configureFlags and
           a.compilerFlags == b.compilerFlags and
           a.compilerVersion == b.compilerVersion and
           a.targetArchitecture == b.targetArchitecture and
           a.libc == b.libc and
           a.allocator == b.allocator and
           a.buildSystem == b.buildSystem

proc checkChunkRefEqual(a, b: ChunkReference): bool =
  ## Check if two ChunkReference objects are semantically equivalent
  result = a.hash == b.hash and
           a.size == b.size and
           a.chunkType == b.chunkType and
           a.path == b.path

proc checkDesktopFileEqual(a, b: DesktopFileSpec): bool =
  ## Check if two DesktopFileSpec objects are semantically equivalent
  result = a.name == b.name and
           a.genericName == b.genericName and
           a.comment == b.comment and
           a.exec == b.exec and
           a.icon == b.icon and
           a.terminal == b.terminal and
           a.categories == b.categories and
           a.keywords == b.keywords

proc checkIconEqual(a, b: IconSpec): bool =
  ## Check if two IconSpec objects are semantically equivalent
  result = a.size == b.size and
           a.path == b.path and
           a.format == b.format

proc checkDesktopMetadataEqual(a, b: DesktopMetadata, verbose: bool = false): bool =
  ## Check if two DesktopMetadata objects are semantically equivalent
  if not checkDesktopFileEqual(a.desktopFile, b.desktopFile):
    if verbose: echo "  Desktop file mismatch"
    return false
  if a.icons.len != b.icons.len:
    if verbose: echo "  Icons length mismatch: ", a.icons.len, " vs ", b.icons.len
    return false
  for i in 0..<a.icons.len:
    if not checkIconEqual(a.icons[i], b.icons[i]):
      if verbose: echo "  Icon ", i, " mismatch"
      return false
  if a.mimeTypes != b.mimeTypes:
    if verbose: echo "  MIME types mismatch: ", a.mimeTypes, " vs ", b.mimeTypes
    return false
  if a.appId != b.appId:
    if verbose: echo "  App ID mismatch: ", a.appId, " vs ", b.appId
    return false
  return true

proc checkFilesystemAccessEqual(a, b: FilesystemAccess): bool =
  ## Check if two FilesystemAccess objects are semantically equivalent
  result = a.path == b.path and a.mode == b.mode

proc checkDBusAccessEqual(a, b: DBusAccess): bool =
  ## Check if two DBusAccess objects are semantically equivalent
  result = a.session == b.session and
           a.system == b.system and
           a.own == b.own

proc checkMountEqual(a, b: Mount): bool =
  ## Check if two Mount objects are semantically equivalent
  result = a.source == b.source and
           a.target == b.target and
           a.mountType == b.mountType and
           a.readOnly == b.readOnly

proc checkNamespaceConfigEqual(a, b: NamespaceConfig): bool =
  ## Check if two NamespaceConfig objects are semantically equivalent
  if a.namespaceType != b.namespaceType:
    return false
  if a.permissions.network != b.permissions.network or
     a.permissions.gpu != b.permissions.gpu or
     a.permissions.audio != b.permissions.audio or
     a.permissions.camera != b.permissions.camera or
     a.permissions.microphone != b.permissions.microphone:
    return false
  if a.permissions.filesystem.len != b.permissions.filesystem.len:
    return false
  for i in 0..<a.permissions.filesystem.len:
    if not checkFilesystemAccessEqual(a.permissions.filesystem[i], b.permissions.filesystem[i]):
      return false
  if not checkDBusAccessEqual(a.permissions.dbus, b.permissions.dbus):
    return false
  if a.mounts.len != b.mounts.len:
    return false
  for i in 0..<a.mounts.len:
    if not checkMountEqual(a.mounts[i], b.mounts[i]):
      return false
  result = true

proc checkSignatureEqual(a, b: SignatureInfo): bool =
  ## Check if two SignatureInfo objects are semantically equivalent
  result = a.algorithm == b.algorithm and
           a.keyId == b.keyId and
           a.signature == b.signature

proc checkManifestsEqual(a, b: NIPManifest, verbose: bool = false): bool =
  ## Check if two NIP manifests are semantically equivalent

  if a.name != b.name:
    if verbose: echo "Name mismatch: ", a.name, " vs ", b.name
    return false

  if a.version != b.version:
    if verbose: echo "Version mismatch: ", a.version, " vs ", b.version
    return false

  if not checkTimestampsClose(a.buildDate, b.buildDate):
    if verbose: echo "BuildDate mismatch: ", a.buildDate, " vs ", b.buildDate
    return false

  if not checkAppInfoEqual(a.metadata, b.metadata):
    if verbose: echo "Metadata mismatch"
    return false

  if not checkProvenanceEqual(a.provenance, b.provenance):
    if verbose: echo "Provenance mismatch"
    return false

  if not checkBuildConfigEqual(a.buildConfig, b.buildConfig):
    if verbose: echo "BuildConfig mismatch"
    return false

  if not checkDesktopMetadataEqual(a.desktop, b.desktop, verbose):
    if verbose: echo "Desktop metadata mismatch"
    return false

  if not checkNamespaceConfigEqual(a.namespace, b.namespace):
    if verbose: echo "Namespace config mismatch"
    return false

  if a.buildHash != b.buildHash:
    if verbose: echo "BuildHash mismatch: ", a.buildHash, " vs ", b.buildHash
    return false

  if not checkSignatureEqual(a.signature, b.signature):
    if verbose: echo "Signature mismatch"
    return false

  # Check CAS chunks
  if a.casChunks.len != b.casChunks.len:
    if verbose: echo "CAS chunks length mismatch: ", a.casChunks.len, " vs ", b.casChunks.len
    return false

  for i in 0..<a.casChunks.len:
    if not checkChunkRefEqual(a.casChunks[i], b.casChunks[i]):
      if verbose: echo "CAS chunk ", i, " mismatch"
      return false

  return true

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

suite "NIP Manifest Roundtrip Property Tests":
  test "Property 3: Manifest Roundtrip - Generate then parse produces equivalent manifest":
    ## **Feature:** 01-nip-unified-storage-and-formats, Property 3: Manifest Roundtrip
    ## **Validates:** Requirements 6.4
    ##
    ## For any NIP manifest, generating KDL and parsing it back should produce
    ## a semantically equivalent manifest.

    # Run property test with 100 random manifests
    var passCount = 0
    var failCount = 0
    var errors: seq[string] = @[]

    for i in 0..<100:
      try:
        # Generate random manifest
        let original = genNIPManifest()

        # Generate KDL
        let kdl = generateNIPManifest(original)

        # Parse KDL back to manifest
        let parsed = parseNIPManifest(kdl)

        # Check semantic equivalence
        let verbose = (failCount == 0)  # Show details for first failure only
        if checkManifestsEqual(original, parsed, verbose):
          passCount.inc()
        else:
          failCount.inc()
          errors.add("Iteration " & $i & ": Manifests not equivalent after roundtrip")
      except CatchableError as e:
        failCount.inc()
        errors.add("Iteration " & $i & ": " & e.msg)

    # Report results
    echo "\nProperty Test Results:"
    echo "  Passed: ", passCount, "/100"
    echo "  Failed: ", failCount, "/100"

    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 == 100

  test "Property 3: Manifest Roundtrip - Determinism (same manifest = same KDL)":
    ## **Feature:** 01-nip-unified-storage-and-formats, Property 3: Manifest Roundtrip
    ## **Validates:** Requirements 6.4
    ##
    ## For any NIP manifest, generating KDL twice should produce identical output.

    var passCount = 0
    var failCount = 0

    for i in 0..<100:
      try:
        # Generate random manifest
        let manifest = genNIPManifest()

        # Generate KDL twice
        let kdl1 = generateNIPManifest(manifest)
        let kdl2 = generateNIPManifest(manifest)

        # Check they're identical
        if kdl1 == kdl2:
          passCount.inc()
        else:
          failCount.inc()
          echo "Iteration ", i, ": KDL output not deterministic"
      except CatchableError as e:
        failCount.inc()
        echo "Iteration ", i, ": ", e.msg

    echo "\nDeterminism Test Results:"
    echo "  Passed: ", passCount, "/100"
    echo "  Failed: ", failCount, "/100"

    check passCount == 100

  test "Property 3: Manifest Roundtrip - Validation catches invalid manifests":
    ## Verify that validation catches common errors

    # Test empty name
    var manifest = genNIPManifest()
    manifest.name = ""
    let issues1 = validateNIPManifest(manifest)
    check issues1.len > 0
    check "name cannot be empty" in issues1[0].toLowerAscii()

    # Test invalid hash format
    manifest = genNIPManifest()
    manifest.buildHash = "invalid-hash"
    let issues2 = validateNIPManifest(manifest)
    check issues2.len > 0
    check "xxh3" in issues2[0].toLowerAscii()

    # Test invalid namespace type
    manifest = genNIPManifest()
    manifest.namespace.namespaceType = "invalid"
    let issues3 = validateNIPManifest(manifest)
    check issues3.len > 0
    check "namespace type" in issues3[0].toLowerAscii()

    # Test empty app_id
    manifest = genNIPManifest()
    manifest.desktop.appId = ""
    let issues4 = validateNIPManifest(manifest)
    check issues4.len > 0
    check "app_id" in issues4[0].toLowerAscii()

when isMainModule:
  # Run tests
  randomize()
  echo "Running NIP Manifest Roundtrip Property Tests..."
  echo "Testing Property 3: Manifest Roundtrip"
  echo "Validates: Requirements 6.4"
  echo ""
