## Cache Invalidation Strategy Tests
##
## This test suite verifies that the GlobalRepoStateHash correctly triggers
## cache invalidation when repository metadata changes. This is the keystone
## of the caching system's correctness.
##
## **Test Strategy:**
## - Verify hash changes on metadata modifications
## - Verify cache invalidation on hash changes
## - Verify cache remains valid when hash unchanged
## - Test various metadata change scenarios

import unittest
import tables
import ../src/nip/resolver/serialization
import ../src/nip/resolver/resolution_cache
import ../src/nip/resolver/types
import ../src/nip/cas/storage

suite "Global Repo State Hash Calculation":
  test "Empty repositories produce deterministic hash":
    let repos1: seq[Repository] = @[]
    let repos2: seq[Repository] = @[]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    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", "arch": "x86_64"}.toTable
          ),
          PackageMetadata(
            name: "zlib",
            version: "1.2.13",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repos2 = repos1  # Identical
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 == hash2
  
  test "Different package version 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 "Different package metadata produces different hash":
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official", "arch": "x86_64"}.toTable
          )
        ]
      )
    ]
    
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official", "arch": "aarch64"}.toTable  # Different arch
          )
        ]
      )
    ]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 != hash2
  
  test "Adding package 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.0",
            metadata: {"source": "official"}.toTable
          ),
          PackageMetadata(
            name: "apache",
            version: "2.4.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 != hash2
  
  test "Removing package produces different hash":
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          ),
          PackageMetadata(
            name: "apache",
            version: "2.4.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            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
  
  test "Multiple repositories combined correctly":
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      ),
      Repository(
        name: "testing",
        packages: @[
          PackageMetadata(
            name: "apache",
            version: "2.4.0",
            metadata: {"source": "testing"}.toTable
          )
        ]
      )
    ]
    
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 != hash2

suite "Cache Invalidation on Metadata Changes":
  test "Cache invalidated when package version changes":
    let cas = newCASStorage("/tmp/test-cas-inv-1")
    let cache = newResolutionCache(cas)
    
    # Initial repository state
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash1 = calculateGlobalRepoStateHash(repos1)
    cache.updateRepoHash(repoHash1)
    
    # Cache a resolution result
    let key = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: repoHash1,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    cache.put(key, graph)
    check cache.get(key).value.isSome
    
    # Update repository (new package version)
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.1",  # Version changed
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash2 = calculateGlobalRepoStateHash(repos2)
    check repoHash1 != repoHash2  # Hash should change
    
    cache.updateRepoHash(repoHash2)
    
    # Cache should be invalidated
    check cache.get(key).value.isNone
  
  test "Cache invalidated when package added":
    let cas = newCASStorage("/tmp/test-cas-inv-2")
    let cache = newResolutionCache(cas)
    
    # Initial repository state
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash1 = calculateGlobalRepoStateHash(repos1)
    cache.updateRepoHash(repoHash1)
    
    # Cache a resolution result
    let key = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: repoHash1,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    cache.put(key, graph)
    check cache.get(key).value.isSome
    
    # Add new package to repository
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          ),
          PackageMetadata(
            name: "apache",
            version: "2.4.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash2 = calculateGlobalRepoStateHash(repos2)
    check repoHash1 != repoHash2  # Hash should change
    
    cache.updateRepoHash(repoHash2)
    
    # Cache should be invalidated
    check cache.get(key).value.isNone
  
  test "Cache invalidated when package metadata changes":
    let cas = newCASStorage("/tmp/test-cas-inv-3")
    let cache = newResolutionCache(cas)
    
    # Initial repository state
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official", "arch": "x86_64"}.toTable
          )
        ]
      )
    ]
    
    let repoHash1 = calculateGlobalRepoStateHash(repos1)
    cache.updateRepoHash(repoHash1)
    
    # Cache a resolution result
    let key = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: repoHash1,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    cache.put(key, graph)
    check cache.get(key).value.isSome
    
    # Update package metadata
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official", "arch": "aarch64"}.toTable  # Arch changed
          )
        ]
      )
    ]
    
    let repoHash2 = calculateGlobalRepoStateHash(repos2)
    check repoHash1 != repoHash2  # Hash should change
    
    cache.updateRepoHash(repoHash2)
    
    # Cache should be invalidated
    check cache.get(key).value.isNone
  
  test "Cache remains valid when repo state unchanged":
    let cas = newCASStorage("/tmp/test-cas-inv-4")
    let cache = newResolutionCache(cas)
    
    # Initial repository state
    let repos = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash = calculateGlobalRepoStateHash(repos)
    cache.updateRepoHash(repoHash)
    
    # Cache a resolution result
    let key = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: repoHash,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    cache.put(key, graph)
    check cache.get(key).value.isSome
    
    # Update with same hash (no metadata change)
    cache.updateRepoHash(repoHash)
    
    # Cache should still be valid
    check cache.get(key).value.isSome
    check cache.get(key).source == L1Hit

suite "Cache Invalidation Edge Cases":
  test "Multiple cached entries all invalidated":
    let cas = newCASStorage("/tmp/test-cas-inv-5")
    let cache = newResolutionCache(cas)
    
    # Initial repository state
    let repos1 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          ),
          PackageMetadata(
            name: "apache",
            version: "2.4.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash1 = calculateGlobalRepoStateHash(repos1)
    cache.updateRepoHash(repoHash1)
    
    # Cache multiple resolution results
    let key1 = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: repoHash1,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let key2 = CacheKey(
      rootPackage: "apache",
      rootConstraint: ">=2.4.0",
      repoStateHash: repoHash1,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let graph1 = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    let graph2 = DependencyGraph(
      rootPackage: PackageId(name: "apache", version: "2.4.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    cache.put(key1, graph1)
    cache.put(key2, graph2)
    
    check cache.get(key1).value.isSome
    check cache.get(key2).value.isSome
    
    # Update repository (change metadata)
    let repos2 = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.1",  # Version changed
            metadata: {"source": "official"}.toTable
          ),
          PackageMetadata(
            name: "apache",
            version: "2.4.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash2 = calculateGlobalRepoStateHash(repos2)
    cache.updateRepoHash(repoHash2)
    
    # All cached entries should be invalidated
    check cache.get(key1).value.isNone
    check cache.get(key2).value.isNone
  
  test "Cache survives multiple updates with same hash":
    let cas = newCASStorage("/tmp/test-cas-inv-6")
    let cache = newResolutionCache(cas)
    
    let repos = @[
      Repository(
        name: "main",
        packages: @[
          PackageMetadata(
            name: "nginx",
            version: "1.24.0",
            metadata: {"source": "official"}.toTable
          )
        ]
      )
    ]
    
    let repoHash = calculateGlobalRepoStateHash(repos)
    cache.updateRepoHash(repoHash)
    
    let key = CacheKey(
      rootPackage: "nginx",
      rootConstraint: ">=1.24.0",
      repoStateHash: repoHash,
      variantDemand: VariantDemand(
        useFlags: @[],
        libc: "musl",
        allocator: "jemalloc",
        targetArch: "x86_64",
        buildFlags: @[]
      )
    )
    
    let graph = DependencyGraph(
      rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
      nodes: @[],
      timestamp: 1700000000
    )
    
    cache.put(key, graph)
    
    # Multiple updates with same hash
    for i in 0..<10:
      cache.updateRepoHash(repoHash)
      check cache.get(key).value.isSome
  
  test "Empty repository hash is deterministic":
    let repos1: seq[Repository] = @[]
    let repos2: seq[Repository] = @[]
    
    let hash1 = calculateGlobalRepoStateHash(repos1)
    let hash2 = calculateGlobalRepoStateHash(repos2)
    
    check hash1 == hash2
    
    let cas = newCASStorage("/tmp/test-cas-inv-7")
    let cache = newResolutionCache(cas)
    
    cache.updateRepoHash(hash1)
    cache.updateRepoHash(hash2)
    
    # Should not trigger invalidation (same hash)
    let metrics = cache.getMetrics()
    check metrics.l1Size == 0  # No entries cached yet
