## NimPak Integration Tests
##
## End-to-end integration tests for the NimPak package manager.
## Task 46: Integration testing.
##
## Tests cover:
## - Cross-format deduplication (NPK, NIP, NEXTER sharing CAS)
## - Atomic installations (no partial states)
## - Garbage collection safety (pinned objects preserved)
## - Migration workflows (format conversions)
## - Error recovery (graceful failure handling)

import std/[os, strutils, strformat, times, sequtils, sets]
import unittest
import ../src/nimpak/cas
import ../src/nimpak/migration
import ../src/nimpak/errors
import ../src/nip/types

suite "Integration - Cross-Format Deduplication":
  
  var testDir: string
  var casManager: CasManager
  
  setup:
    testDir = getTempDir() / "nip_integration_test_" & $epochTime().int
    createDir(testDir)
    casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")

  teardown:
    removeDir(testDir)

  test "Same content across NPK, NIP, NEXTER shares CAS object":
    # Simulate the same library being used by all three formats
    let sharedLibData = @[byte(1), byte(2), byte(3), byte(4), byte(5)]
    
    # Store for NPK format
    let npkResult = casManager.storeObject(sharedLibData)
    check npkResult.isOk
    let hash1 = npkResult.get().hash
    discard casManager.addReference(hash1, NPK, "libfoo-npk")
    
    # Store same data for NIP format - should deduplicate
    let nipResult = casManager.storeObject(sharedLibData)
    check nipResult.isOk
    let hash2 = nipResult.get().hash
    discard casManager.addReference(hash2, NIP, "myapp-nip")
    
    # Store same data for NEXTER format - should deduplicate
    let nexterResult = casManager.storeObject(sharedLibData)
    check nexterResult.isOk
    let hash3 = nexterResult.get().hash
    discard casManager.addReference(hash3, NEXTER, "devenv-nexter")
    
    # All three should have the same hash (deduplication)
    check hash1 == hash2
    check hash2 == hash3
    
    # Object should exist only once in CAS
    check casManager.objectExists(hash1)
    
    # Reference count should reflect multiple stores
    let refCount = casManager.getRefCount(hash1)
    check refCount >= 3  # At least 3 stores

  test "Deduplication across multiple packages of same format":
    # Common data shared by multiple packages
    let commonData = @[byte(10), byte(20), byte(30)]
    
    var storedHashes: seq[string] = @[]
    
    # Simulate 5 NPK packages sharing the same file
    for i in 1..5:
      let result = casManager.storeObject(commonData)
      check result.isOk
      storedHashes.add(result.get().hash)
      discard casManager.addReference(result.get().hash, NPK, fmt"pkg-{i}")
    
    # All hashes should be identical (deduplication)
    check storedHashes.toHashSet.len == 1
    
    # Object should exist
    check casManager.objectExists(storedHashes[0])
    
    # Reference count should be high
    let refCount = casManager.getRefCount(storedHashes[0])
    check refCount >= 5

  test "Different content produces different hashes":
    let data1 = @[byte(1), byte(2), byte(3)]
    let data2 = @[byte(4), byte(5), byte(6)]
    let data3 = @[byte(7), byte(8), byte(9)]
    
    let hash1 = casManager.storeObject(data1).get().hash
    let hash2 = casManager.storeObject(data2).get().hash
    let hash3 = casManager.storeObject(data3).get().hash
    
    check hash1 != hash2
    check hash2 != hash3
    check hash1 != hash3

suite "Integration - Atomic Installations":
  
  var testDir: string
  var casManager: CasManager
  
  setup:
    testDir = getTempDir() / "nip_atomic_test_" & $epochTime().int
    createDir(testDir)
    casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")

  teardown:
    removeDir(testDir)

  test "Package files are stored atomically":
    # Simulate installing multiple files as a package
    var packageHashes: seq[string] = @[]
    
    for i in 1..5:
      let data = @[byte(i), byte(i+1), byte(i+2)]
      let result = casManager.storeObject(data)
      check result.isOk
      packageHashes.add(result.get().hash)
      # Add reference as part of atomic transaction
      discard casManager.addReference(result.get().hash, NPK, "test-pkg")
    
    # Verify all objects exist
    for hash in packageHashes:
      check casManager.objectExists(hash)

  test "Complete installation is fully committed":
    var packageHashes: seq[string] = @[]
    
    # Install a "package" with 3 files
    for i in 1..3:
      let data = @[byte(100 + i)]
      let result = casManager.storeObject(data)
      check result.isOk
      packageHashes.add(result.get().hash)
      discard casManager.addReference(result.get().hash, NIP, "complete-pkg")
    
    # Verify all exist
    for hash in packageHashes:
      check casManager.objectExists(hash)
    
    # Run GC - should not remove anything (still referenced)
    discard casManager.garbageCollect()
    
    # All still exist
    for hash in packageHashes:
      check casManager.objectExists(hash)

suite "Integration - Garbage Collection Safety":
  
  var testDir: string
  var casManager: CasManager
  
  setup:
    testDir = getTempDir() / "nip_gc_safety_test_" & $epochTime().int
    createDir(testDir)
    casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")

  teardown:
    removeDir(testDir)

  test "Pinned objects survive garbage collection":
    let criticalData = @[byte(255), byte(254), byte(253)]
    
    let result = casManager.storeObject(criticalData)
    check result.isOk
    let hash = result.get().hash
    
    # Pin the object
    discard casManager.pinObject(hash, "system-critical")
    
    # Run GC
    discard casManager.garbageCollect()
    
    # Pinned object should still exist
    check casManager.objectExists(hash)

  test "Referenced objects survive GC":
    let data = @[byte(42)]
    
    let result = casManager.storeObject(data)
    check result.isOk
    let hash = result.get().hash
    
    # Add reference
    discard casManager.addReference(hash, NPK, "my-package")
    
    # Run GC
    discard casManager.garbageCollect()
    
    # Object should still exist
    check casManager.objectExists(hash)

  test "Cross-format references all protect object":
    let sharedData = @[byte(50), byte(51)]
    
    let result = casManager.storeObject(sharedData)
    check result.isOk
    let hash = result.get().hash
    
    # Add references from multiple formats
    discard casManager.addReference(hash, NPK, "pkg1")
    discard casManager.addReference(hash, NIP, "app1")
    discard casManager.addReference(hash, NEXTER, "container1")
    
    # Run GC
    discard casManager.garbageCollect()
    
    # Object should still exist (multiple references)
    check casManager.objectExists(hash)

suite "Integration - Migration Workflows":
  
  var testDir: string
  var migrationManager: MigrationManager
  
  setup:
    testDir = getTempDir() / "nip_migration_test_" & $epochTime().int
    createDir(testDir)
    migrationManager = initMigrationManager(testDir / "cas", dryRun = false, verbose = false)

  teardown:
    removeDir(testDir)

  test "Legacy NIP to new format migration preserves data":
    # Create a mock legacy NIP structure
    let legacyDir = testDir / "legacy-app"
    createDir(legacyDir)
    createDir(legacyDir / "bin")
    createDir(legacyDir / "lib")
    
    writeFile(legacyDir / "manifest.kdl", "name \"test-app\"\nversion \"1.0.0\"")
    writeFile(legacyDir / "bin" / "app", "#!/bin/sh\necho hello")
    writeFile(legacyDir / "lib" / "libfoo.so", "ELF binary data simulation")
    
    # Migrate
    let result = migrationManager.migrateLegacyNip(legacyDir)
    
    check result.source == OldNip
    check result.packageName == "legacy-app"
    check result.success == true
    check result.casHashes.len > 0

  test "Format conversion NPK to NIP":
    let result = migrationManager.convertNpkToNip("/mock/package.npk")
    
    check result.success == true  # Placeholder succeeds
    check result.warnings.len > 0  # Has warnings about placeholder

  test "Format conversion NIP to NEXTER":
    let result = migrationManager.convertNipToNexter("/mock/app.nip")
    
    check result.success == true  # Placeholder succeeds
    check result.warnings.len > 0

  test "Migration verification catches missing objects":
    let mockResult = MigrationResult(
      success: true,
      source: OldNip,
      packageName: "missing-objects",
      casHashes: @["xxh3-nonexistent123456"],
      errors: @[]
    )
    
    # Verification should fail for missing objects
    check not migrationManager.verifyMigration(mockResult)

  test "Migration report generation":
    # Create two mock results
    let results = @[
      MigrationResult(success: true, source: OldNip, packageName: "app1"),
      MigrationResult(success: true, source: Flatpak, packageName: "app2"),
      MigrationResult(success: false, source: AppImage, packageName: "app3", errors: @["Test error"])
    ]
    
    let report = generateMigrationReport(results)
    
    check report.len > 0
    check report.contains("app1")
    check report.contains("app2")
    check report.contains("app3")

suite "Integration - Error Recovery":
  
  var testDir: string
  var casManager: CasManager
  
  setup:
    testDir = getTempDir() / "nip_error_test_" & $epochTime().int
    createDir(testDir)
    casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")

  teardown:
    removeDir(testDir)

  test "Retrieve nonexistent object returns error":
    let result = casManager.retrieveObject("xxh3-doesnotexist000000")
    
    check result.isErr

  test "Store and retrieve cycle works correctly":
    let originalData = @[byte(1), byte(2), byte(3), byte(4), byte(5)]
    
    # Store
    let storeResult = casManager.storeObject(originalData)
    check storeResult.isOk
    let hash = storeResult.get().hash
    
    # Retrieve
    let retrieveResult = casManager.retrieveObject(hash)
    check retrieveResult.isOk
    
    let retrievedData = retrieveResult.get()
    check retrievedData == originalData

  test "Error factory provides actionable suggestions":
    let err = checksumMismatchError("pkg.nip", "xxh3-expected", "xxh3-actual")
    
    check err.code == ChecksumMismatch
    check err.suggestions.len > 0
    check err.msg.len > 0

  test "Recovery strategies are appropriate for error types":
    # Network errors should suggest retry
    let networkErr = networkError("timeout", "https://example.com")
    check suggestRecovery(networkErr) == Retry
    
    # Permission errors should suggest manual intervention
    let permErr = permissionDeniedError("/root/file", "write")
    check suggestRecovery(permErr) == Manual
    
    # Checksum errors should abort
    let checksumErr = checksumMismatchError("file", "a", "b")
    check suggestRecovery(checksumErr) == Abort

suite "Integration - End-to-End Workflows":
  
  var testDir: string
  var casManager: CasManager
  
  setup:
    testDir = getTempDir() / "nip_e2e_test_" & $epochTime().int
    createDir(testDir)
    casManager = initCasManager(testDir / "cas", testDir / "cas" / "system")

  teardown:
    removeDir(testDir)

  test "Multiple package versions coexist":
    # Install v1.0
    let v1Data = @[byte(1), byte(0)]
    let v1Result = casManager.storeObject(v1Data)
    check v1Result.isOk
    let v1Hash = v1Result.get().hash
    discard casManager.addReference(v1Hash, NPK, "myapp-1.0")
    
    # Install v2.0 (different content)
    let v2Data = @[byte(2), byte(0)]
    let v2Result = casManager.storeObject(v2Data)
    check v2Result.isOk
    let v2Hash = v2Result.get().hash
    discard casManager.addReference(v2Hash, NPK, "myapp-2.0")
    
    # Both versions exist
    check casManager.objectExists(v1Hash)
    check casManager.objectExists(v2Hash)
    check v1Hash != v2Hash

  test "Concurrent format usage: NPK + NIP + NEXTER":
    # System library (NPK)
    let libData = @[byte(100)]
    let libResult = casManager.storeObject(libData)
    check libResult.isOk
    discard casManager.addReference(libResult.get().hash, NPK, "libsystem")
    
    # Application using library (NIP)
    let appData = @[byte(200)]
    let appResult = casManager.storeObject(appData)
    check appResult.isOk
    discard casManager.addReference(appResult.get().hash, NIP, "myapp")
    
    # Dev container (NEXTER)
    let containerData = @[byte(255)]
    let containerResult = casManager.storeObject(containerData)
    check containerResult.isOk
    discard casManager.addReference(containerResult.get().hash, NEXTER, "devenv")
    
    # All three exist
    check casManager.objectExists(libResult.get().hash)
    check casManager.objectExists(appResult.get().hash)
    check casManager.objectExists(containerResult.get().hash)

  test "Hash consistency across store cycles":
    let data = @[byte(1), byte(2), byte(3)]
    
    # Store same data multiple times
    let hash1 = casManager.storeObject(data).get().hash
    let hash2 = casManager.storeObject(data).get().hash
    let hash3 = casManager.storeObject(data).get().hash
    
    # All hashes are identical
    check hash1 == hash2
    check hash2 == hash3

when isMainModule:
  echo "Integration Tests Complete"
