## Tests for Binary Serialization and Cache Key Calculation
##
## This test suite verifies:
## - Deterministic MessagePack serialization
## - Correct deserialization (round-trip)
## - Cache key determinism
## - Cache invalidation on metadata changes

import unittest
import tables
import ../src/nip/resolver/serialization
import ../src/nip/resolver/types

suite "MessagePack Serialization":
  test "Empty graph serialization":
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
      nodes: @[],
      timestamp: 1234567890
    )
    
    let binary = toMessagePack(graph)
    let reconstructed = fromMessagePack(binary)
    
    check reconstructed.rootPackage.name == "test"
    check reconstructed.rootPackage.version == "1.0"
    check reconstructed.rootPackage.variant == "default"
    check reconstructed.nodes.len == 0
    check reconstructed.timestamp == 1234567890
  
  test "Single node graph serialization":
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "ssl"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "nginx", version: "1.24.0", variant: "ssl"),
          dependencies: @[],
          buildHash: "xxh3-abc123",
          metadata: {"source": "official"}.toTable
        )
      ],
      timestamp: 1700000000
    )
    
    let binary = toMessagePack(graph)
    let reconstructed = fromMessagePack(binary)
    
    check reconstructed.nodes.len == 1
    check reconstructed.nodes[0].packageId.name == "nginx"
    check reconstructed.nodes[0].buildHash == "xxh3-abc123"
    check reconstructed.nodes[0].metadata["source"] == "official"
  
  test "Complex graph with dependencies":
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "app", version: "1.0", variant: "default"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "app", version: "1.0", variant: "default"),
          dependencies: @[
            PackageId(name: "libssl", version: "3.0", variant: "default"),
            PackageId(name: "zlib", version: "1.2.13", variant: "default")
          ],
          buildHash: "xxh3-app123",
          metadata: {"type": "application"}.toTable
        ),
        DependencyNode(
          packageId: PackageId(name: "libssl", version: "3.0", variant: "default"),
          dependencies: @[],
          buildHash: "xxh3-ssl456",
          metadata: {"type": "library"}.toTable
        ),
        DependencyNode(
          packageId: PackageId(name: "zlib", version: "1.2.13", variant: "default"),
          dependencies: @[],
          buildHash: "xxh3-zlib789",
          metadata: {"type": "library"}.toTable
        )
      ],
      timestamp: 1700000000
    )
    
    let binary = toMessagePack(graph)
    let reconstructed = fromMessagePack(binary)
    
    check reconstructed.nodes.len == 3
    check reconstructed.nodes[0].dependencies.len == 2
    
    # Verify dependencies are preserved
    let appNode = reconstructed.nodes[0]
    check appNode.dependencies[0].name in ["libssl", "zlib"]
    check appNode.dependencies[1].name in ["libssl", "zlib"]

suite "Serialization Determinism":
  test "Same graph produces identical binary":
    let graph1 = DependencyGraph(
      rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "dep1", version: "2.0", variant: "default"),
          dependencies: @[],
          buildHash: "hash1",
          metadata: {"key": "value"}.toTable
        )
      ],
      timestamp: 1234567890
    )
    
    let graph2 = graph1  # Identical graph
    
    let binary1 = toMessagePack(graph1)
    let binary2 = toMessagePack(graph2)
    
    check binary1 == binary2
  
  test "Node order doesn't affect binary (sorted)":
    let graph1 = DependencyGraph(
      rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "aaa", version: "1.0", variant: "default"),
          dependencies: @[],
          buildHash: "hash1",
          metadata: initTable[string, string]()
        ),
        DependencyNode(
          packageId: PackageId(name: "zzz", version: "1.0", variant: "default"),
          dependencies: @[],
          buildHash: "hash2",
          metadata: initTable[string, string]()
        )
      ],
      timestamp: 1234567890
    )
    
    let graph2 = DependencyGraph(
      rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "zzz", version: "1.0", variant: "default"),
          dependencies: @[],
          buildHash: "hash2",
          metadata: initTable[string, string]()
        ),
        DependencyNode(
          packageId: PackageId(name: "aaa", version: "1.0", variant: "default"),
          dependencies: @[],
          buildHash: "hash1",
          metadata: initTable[string, string]()
        )
      ],
      timestamp: 1234567890
    )
    
    let binary1 = toMessagePack(graph1)
    let binary2 = toMessagePack(graph2)
    
    # Should be identical because nodes are sorted by packageId
    check binary1 == binary2
  
  test "Dependency order doesn't affect binary (sorted)":
    let graph1 = DependencyGraph(
      rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "app", version: "1.0", variant: "default"),
          dependencies: @[
            PackageId(name: "aaa", version: "1.0", variant: "default"),
            PackageId(name: "zzz", version: "1.0", variant: "default")
          ],
          buildHash: "hash1",
          metadata: initTable[string, string]()
        )
      ],
      timestamp: 1234567890
    )
    
    let graph2 = DependencyGraph(
      rootPackage: PackageId(name: "test", version: "1.0", variant: "default"),
      nodes: @[
        DependencyNode(
          packageId: PackageId(name: "app", version: "1.0", variant: "default"),
          dependencies: @[
            PackageId(name: "zzz", version: "1.0", variant: "default"),
            PackageId(name: "aaa", version: "1.0", variant: "default")
          ],
          buildHash: "hash1",
          metadata: initTable[string, string]()
        )
      ],
      timestamp: 1234567890
    )
    
    let binary1 = toMessagePack(graph1)
    let binary2 = toMessagePack(graph2)
    
    # Should be identical because dependencies are sorted
    check binary1 == binary2

suite "Cache Key Calculation":
  test "Cache key is deterministic":
    let key1 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @["ssl", "http2"],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @["-O2", "-march=native"]
      )
    )
    
    let key2 = key1  # Identical key
    
    let hash1 = calculateCacheKey(key1)
    let hash2 = calculateCacheKey(key2)
    
    check hash1 == hash2
    check hash1.len == 32  # xxh3_128 produces 32-character hex string
  
  test "Different packages produce different keys":
    let key1 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let key2 = CacheKey(
      rootPackage: "apache",  # Different package
      rootConstraint: ">=2.4.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let hash1 = calculateCacheKey(key1)
    let hash2 = calculateCacheKey(key2)
    
    check hash1 != hash2
  
  test "Different USE flags produce different keys":
    let key1 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @["ssl"],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let key2 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @["ssl", "http2"],  # Different USE flags
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let hash1 = calculateCacheKey(key1)
    let hash2 = calculateCacheKey(key2)
    
    check hash1 != hash2
  
  test "USE flag order doesn't affect key (sorted)":
    let key1 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @["ssl", "http2", "brotli"],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let key2 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @["brotli", "http2", "ssl"],  # Different order
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let hash1 = calculateCacheKey(key1)
    let hash2 = calculateCacheKey(key2)
    
    # Should be identical because USE flags are sorted
    check hash1 == hash2
  
  test "Different repo state produces different keys":
    let key1 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-123",
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let key2 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: "repo-hash-456",  # Different repo state
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let hash1 = calculateCacheKey(key1)
    let hash2 = calculateCacheKey(key2)
    
    check hash1 != hash2

suite "Global Repo State Hash":
  test "Empty repositories produce deterministic hash":
    let repos: seq[Repository] = @[]
    
    let hash1 = calculateGlobalRepoStateHash(repos)
    let hash2 = calculateGlobalRepoStateHash(repos)
    
    check hash1 == hash2
    check hash1.len == 32  # xxh3_128 produces 32-character hex string
  
  test "Same repositories produce identical hash":
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repos2 = repos1  # Identical
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 == hash2
  
  test "Different metadata produces different hash":
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.1",  # Different version
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 != hash2
  
  test "Package order doesn't affect hash (sorted)":
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "aaa",
            version: "1.0",
            metadata: initTable[string, string]()
          ),
          PackageMetadata(
            name: "zzz",
            version: "1.0",
            metadata: initTable[string, string]()
          )
        ]
      )
    ]
    
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "zzz",
            version: "1.0",
            metadata: initTable[string, string]()
          ),
          PackageMetadata(
            name: "aaa",
            version: "1.0",
            metadata: initTable[string, string]()
          )
        ]
      )
    ]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    # Should be identical because metadata hashes are sorted
    check hash1 == hash2

suite "Variant Demand Canonicalization":
  test "Canonical form is deterministic":
    let demand1 = VariantDemand(
      useFlags: @["ssl", "http2"],
      libc: "musl",
      allocator: "jemalloc",
      targetArch: "x86_64",
      buildFlags: @["-O2", "-march=native"]
    )
    
    let demand2 = demand1  # Identical
    
    let canon1 = canonicalizeVariantDemand(demand1)
    let canon2 = canonicalizeVariantDemand(demand2)
    
    check canon1 == canon2
  
  test "USE flag order doesn't affect canonical form":
    let demand1 = VariantDemand(
      useFlags: @["ssl", "http2", "brotli"],
      libc: "musl",
      allocator: "jemalloc",
      targetArch: "x86_64",
      buildFlags: @[]
    )
    
    let demand2 = VariantDemand(
      useFlags: @["brotli", "http2", "ssl"],  # Different order
      libc: "musl",
      allocator: "jemalloc",
      targetArch: "x86_64",
      buildFlags: @[]
    )
    
    let canon1 = canonicalizeVariantDemand(demand1)
    let canon2 = canonicalizeVariantDemand(demand2)
    
    check canon1 == canon2
  
  test "Build flag order doesn't affect canonical form":
    let demand1 = VariantDemand(
      useFlags: @[],
      libc: "musl",
      allocator: "jemalloc",
      targetArch: "x86_64",
      buildFlags: @["-O2", "-march=native", "-flto"]
    )
    
    let demand2 = VariantDemand(
      useFlags: @[],
      libc: "musl",
      allocator: "jemalloc",
      targetArch: "x86_64",
      buildFlags: @["-flto", "-march=native", "-O2"]  # Different order
    )
    
    let canon1 = canonicalizeVariantDemand(demand1)
    let canon2 = canonicalizeVariantDemand(demand2)
    
    check canon1 == canon2
