# tests/test_install_manager.nim
# Unit tests for install_manager module

import unittest, os, times, json, tables, strutils, sequtils
import ../src/nimpak/install_manager
import ../src/nimpak/cas

suite "InstallManager Tests":
  var testRoot: string
  var manager: InstallManager
  var config: InstallConfig

  setup:
    # Create temporary test directory
    testRoot = getTempDir() / "nip_test_" & $epochTime().int
    createDir(testRoot)

    # Configure test environment
    config = InstallConfig(
      programsDir: testRoot / "Programs",
      linksDir: testRoot / "System" / "Links",
      cacheDir: testRoot / "cache",
      dbFile: testRoot / "db" / "packages.json",
      autoSymlink: true,
      checkConflicts: true,
      verbose: false
    )

    # Create manager
    manager = newInstallManager(config)

  teardown:
    # Clean up test directory
    if dirExists(testRoot):
      removeDir(testRoot)

  test "Create install manager with default config":
    let defaultMgr = newInstallManager(defaultConfig())
    check defaultMgr != nil
    check defaultMgr.config.programsDir == "/Programs"
    check defaultMgr.config.linksDir == "/System/Links"
    check defaultMgr.config.autoSymlink == true
    check defaultMgr.config.checkConflicts == true

  test "Install package to temporary directory":
    # Create a mock package source directory
    let sourcePath = testRoot / "source" / "testpkg"
    createDir(sourcePath / "bin")
    createDir(sourcePath / "lib")

    # Create test files
    writeFile(sourcePath / "bin" / "testcmd", "#!/bin/sh\necho test")
    writeFile(sourcePath / "lib" / "libtest.so.1", "mock library")

    # Install the package
    let metadata = %*{"description": "Test package"}
    let result = manager.installPackage(
      packageName = "testpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-abc123",
      metadata = metadata
    )

    # Verify installation succeeded
    check result.success == true
    check result.packageName == "testpkg"
    check result.version == "1.0.0"
    check result.errors.len == 0

    # Verify package directory was created
    check dirExists(result.installPath)
    check fileExists(result.installPath / "bin" / "testcmd")
    check fileExists(result.installPath / "lib" / "libtest.so.1")

    # Verify package is in database
    check manager.isInstalled("testpkg")
    let pkg = manager.getInstalledPackage("testpkg")
    check pkg.name == "testpkg"
    check pkg.version == "1.0.0"
    check pkg.source == "test"

  test "Install package with variant descriptor":
    let sourcePath = testRoot / "source" / "varpkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "varcmd", "#!/bin/sh\necho variant")

    # Install with custom variant
    let result = manager.installPackage(
      packageName = "varpkg",
      version = "2.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-def456",
      metadata = newJObject(),
      variantDescriptor = "optimized"
    )

    check result.success == true
    check "optimized" in result.installPath

  test "Symlink creation for executables":
    let sourcePath = testRoot / "source" / "binpkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "cmd1", "#!/bin/sh\necho cmd1")
    writeFile(sourcePath / "bin" / "cmd2", "#!/bin/sh\necho cmd2")

    let result = manager.installPackage(
      packageName = "binpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-bin123"
    )

    check result.success == true
    check result.symlinksCreated.len == 2

    # Verify symlinks were created
    let execDir = config.linksDir / "Executables"
    check symlinkExists(execDir / "cmd1")
    check symlinkExists(execDir / "cmd2")

  test "Symlink creation for libraries":
    let sourcePath = testRoot / "source" / "libpkg"
    createDir(sourcePath / "lib")
    writeFile(sourcePath / "lib" / "libfoo.so.1", "library")
    writeFile(sourcePath / "lib" / "libbar.so.2.0", "library")

    let result = manager.installPackage(
      packageName = "libpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-lib123"
    )

    check result.success == true
    check result.symlinksCreated.len == 2

    # Verify library symlinks
    let libDir = config.linksDir / "Libraries"
    check symlinkExists(libDir / "libfoo.so.1")
    check symlinkExists(libDir / "libbar.so.2.0")

  test "Package removal and cleanup":
    # First install a package
    let sourcePath = testRoot / "source" / "rmpkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "rmcmd", "#!/bin/sh\necho remove")

    let installResult = manager.installPackage(
      packageName = "rmpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-rm123"
    )

    check installResult.success == true
    check manager.isInstalled("rmpkg")

    # Now remove it
    let removeResult = manager.removePackage("rmpkg")

    check removeResult.success == true
    check removeResult.packageName == "rmpkg"
    check removeResult.errors.len == 0

    # Verify package was removed
    check not manager.isInstalled("rmpkg")
    check not dirExists(installResult.installPath)

    # Verify symlinks were removed
    let execDir = config.linksDir / "Executables"
    check not symlinkExists(execDir / "rmcmd")

  test "Remove non-existent package":
    let result = manager.removePackage("nonexistent")

    check result.success == false
    check result.errors.len > 0
    check "not installed" in result.errors[0]

  test "Conflict detection - executable":
    # Install first package
    let source1 = testRoot / "source" / "pkg1"
    createDir(source1 / "bin")
    writeFile(source1 / "bin" / "conflict", "#!/bin/sh\necho pkg1")

    let result1 = manager.installPackage(
      packageName = "pkg1",
      version = "1.0.0",
      source = "test",
      sourcePath = source1,
      graftHash = "blake2b-pkg1"
    )

    check result1.success == true

    # Try to install second package with same executable
    let source2 = testRoot / "source" / "pkg2"
    createDir(source2 / "bin")
    writeFile(source2 / "bin" / "conflict", "#!/bin/sh\necho pkg2")

    let result2 = manager.installPackage(
      packageName = "pkg2",
      version = "1.0.0",
      source = "test",
      sourcePath = source2,
      graftHash = "blake2b-pkg2"
    )

    # Installation should succeed but with warnings
    check result2.success == true
    check result2.warnings.len > 0
    check "conflict" in result2.warnings[0].toLowerAscii()

  test "Conflict detection - library":
    # Install first package with library
    let source1 = testRoot / "source" / "libpkg1"
    createDir(source1 / "lib")
    writeFile(source1 / "lib" / "libconflict.so.1", "lib1")

    let result1 = manager.installPackage(
      packageName = "libpkg1",
      version = "1.0.0",
      source = "test",
      sourcePath = source1,
      graftHash = "blake2b-libpkg1"
    )

    check result1.success == true

    # Try to install second package with same library
    let source2 = testRoot / "source" / "libpkg2"
    createDir(source2 / "lib")
    writeFile(source2 / "lib" / "libconflict.so.1", "lib2")

    let result2 = manager.installPackage(
      packageName = "libpkg2",
      version = "1.0.0",
      source = "test",
      sourcePath = source2,
      graftHash = "blake2b-libpkg2"
    )

    check result2.success == true
    check result2.warnings.len > 0

  test "List installed packages":
    # Install multiple packages
    for i in 1..3:
      let sourcePath = testRoot / "source" / "pkg" & $i
      createDir(sourcePath / "bin")
      writeFile(sourcePath / "bin" / "cmd" & $i, "test")

      let result = manager.installPackage(
        packageName = "pkg" & $i,
        version = "1.0.0",
        source = "test",
        sourcePath = sourcePath,
        graftHash = "blake2b-pkg" & $i
      )
      check result.success == true

    # List packages
    let installed = manager.listInstalled()
    check installed.len == 3

    # Verify package names
    let names = installed.mapIt(it.name)
    check "pkg1" in names
    check "pkg2" in names
    check "pkg3" in names

  test "Get system status":
    # Install a package
    let sourcePath = testRoot / "source" / "statuspkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "statuscmd", "test")

    discard manager.installPackage(
      packageName = "statuspkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-status"
    )

    # Get status
    let status = manager.getStatus()

    check status.hasKey("total_packages")
    check status["total_packages"].getInt() == 1
    check status.hasKey("by_source")
    check status.hasKey("total_size_mb")
    check status.hasKey("programs_dir")
    check status.hasKey("links_dir")

  test "Database persistence":
    # Install a package
    let sourcePath = testRoot / "source" / "dbpkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "dbcmd", "test")

    let result = manager.installPackage(
      packageName = "dbpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-db123"
    )

    check result.success == true

    # Create new manager to test database loading
    let manager2 = newInstallManager(config)

    # Verify package was loaded from database
    check manager2.isInstalled("dbpkg")
    let pkg = manager2.getInstalledPackage("dbpkg")
    check pkg.name == "dbpkg"
    check pkg.version == "1.0.0"

  test "Database backup creation":
    # Install first package
    let source1 = testRoot / "source" / "backup1"
    createDir(source1 / "bin")
    writeFile(source1 / "bin" / "cmd1", "test")

    discard manager.installPackage(
      packageName = "backup1",
      version = "1.0.0",
      source = "test",
      sourcePath = source1,
      graftHash = "blake2b-backup1"
    )

    # Install second package (should create backup)
    let source2 = testRoot / "source" / "backup2"
    createDir(source2 / "bin")
    writeFile(source2 / "bin" / "cmd2", "test")

    discard manager.installPackage(
      packageName = "backup2",
      version = "1.0.0",
      source = "test",
      sourcePath = source2,
      graftHash = "blake2b-backup2"
    )

    # Check for backup files
    let dbDir = parentDir(config.dbFile)
    var backupFound = false
    for file in walkDir(dbDir):
      if ".backup." in extractFilename(file.path):
        backupFound = true
        break

    check backupFound == true

  test "Install with missing source directory":
    let result = manager.installPackage(
      packageName = "missing",
      version = "1.0.0",
      source = "test",
      sourcePath = testRoot / "nonexistent",
      graftHash = "blake2b-missing"
    )

    check result.success == false
    check result.errors.len > 0
    check "does not exist" in result.errors[0]

  test "Install duplicate package":
    # Install package first time
    let sourcePath = testRoot / "source" / "duppkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "dupcmd", "test")

    let result1 = manager.installPackage(
      packageName = "duppkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-dup123"
    )

    check result1.success == true

    # Try to install again
    let result2 = manager.installPackage(
      packageName = "duppkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-dup123"
    )

    check result2.success == false
    check result2.errors.len > 0
    check "already installed" in result2.errors[0]

  test "Symlink creation with auto-symlink disabled":
    # Create manager with auto-symlink disabled
    var noSymlinkConfig = config
    noSymlinkConfig.autoSymlink = false
    let noSymlinkMgr = newInstallManager(noSymlinkConfig)

    let sourcePath = testRoot / "source" / "nosympkg"
    createDir(sourcePath / "bin")
    writeFile(sourcePath / "bin" / "nosymcmd", "test")

    let result = noSymlinkMgr.installPackage(
      packageName = "nosympkg",
      version = "1.0.0",
      source = "test",
      sourcePath = sourcePath,
      graftHash = "blake2b-nosym"
    )

    check result.success == true
    check result.symlinksCreated.len == 0

  test "Current symlink creation and switching":
    # Install first variant
    let source1 = testRoot / "source" / "varpkg1"
    createDir(source1 / "bin")
    writeFile(source1 / "bin" / "varcmd", "variant1")

    let result1 = manager.installPackage(
      packageName = "varpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = source1,
      graftHash = "blake2b-var1",
      variantDescriptor = "default"
    )

    check result1.success == true

    # Verify Current symlink exists
    let versionDir = config.programsDir / "varpkg" / "1.0.0"
    let currentLink = versionDir / "Current"
    check symlinkExists(currentLink)

    # Install second variant (should switch Current)
    let source2 = testRoot / "source" / "varpkg2"
    createDir(source2 / "bin")
    writeFile(source2 / "bin" / "varcmd", "variant2")

    let result2 = manager.installPackage(
      packageName = "varpkg",
      version = "1.0.0",
      source = "test",
      sourcePath = source2,
      graftHash = "blake2b-var2",
      variantDescriptor = "optimized"
    )

    check result2.success == true
    check symlinkExists(currentLink)

    # Current should now point to optimized variant
    let target = expandSymlink(currentLink)
    check "optimized" in target
