## NimPak Filesystem Management Tests
##
## This module contains unit tests for the filesystem management functionality,
## focusing on the GoboLinux-style directory structure and atomic symlink operations.

import unittest, os, strutils, strformat
import nimpak/[types, filesystem]

# Create a temporary test directory
let testDir = getTempDir() / "nimpak_test_filesystem"
let programsRoot = testDir / "Programs"
let indexRoot = testDir / "System/Index"
let logFile = testDir / "filesystem.log"

# Setup and teardown helpers
proc setupTestDir() =
  if dirExists(testDir):
    removeDir(testDir)
  createDir(programsRoot)
  createDir(indexRoot)

proc teardownTestDir() =
  if dirExists(testDir):
    removeDir(testDir)

suite "FilesystemManager Tests":
  setup:
    setupTestDir()
    let fm = newFilesystemManager(
      programsRoot = programsRoot,
      indexRoot = indexRoot,
      logFile = logFile,
      dryRun = false
    )

  teardown:
    teardownTestDir()

  test "Create FilesystemManager with custom paths":
    let customFm = newFilesystemManager(
      programsRoot = "/custom/Programs",
      indexRoot = "/custom/Index",
      logFile = "/custom/logs/fs.log",
      dryRun = true
    )

    check customFm.programsRoot == "/custom/Programs"
    check customFm.indexRoot == "/custom/Index"
    check customFm.logFile == "/custom/logs/fs.log"
    check customFm.dryRun == true

  test "Create program directory structure":
    let pkg = PackageId(name: "test", version: "1.0.0", stream: Stable)
    let result = fm.createProgramDirectory(pkg)

    check result.isOk
    let programDir = result.get()
    check programDir == joinPath(programsRoot, "Test", "1.0.0")
    check dirExists(programDir)

    # Check standard subdirectories
    for subdir in ["bin", "lib", "share", "etc", "var"]:
      check dirExists(joinPath(programDir, subdir))

  test "Remove program directory":
    let pkg = PackageId(name: "test", version: "1.0.0", stream: Stable)
    let createResult = fm.createProgramDirectory(pkg)
    check createResult.isOk

    let programDir = createResult.get()
    check dirExists(programDir)

    let removeResult = fm.removeProgramDirectory(pkg)
    check removeResult.isOk
    check not dirExists(programDir)

  test "Create and remove symlink":
    # Create a test file
    let sourceDir = joinPath(programsRoot, "Test", "1.0.0", "bin")
    createDir(sourceDir)
    let sourceFile = joinPath(sourceDir, "testapp")
    writeFile(sourceFile, "test content")

    let targetFile = joinPath(indexRoot, "bin", "testapp")

    # Create symlink
    let createResult = fm.createSymlink(sourceFile, targetFile)
    check createResult.isOk
    check symlinkExists(targetFile)
    check expandSymlink(targetFile) == sourceFile

    # Remove symlink
    let removeResult = fm.removeSymlink(targetFile)
    check removeResult.isOk
    check not symlinkExists(targetFile)

  test "Atomic symlink update with rollback":
    # Create test files
    let sourceDir = joinPath(programsRoot, "Test", "1.0.0", "bin")
    createDir(sourceDir)
    let sourceFile1 = joinPath(sourceDir, "app1")
    let sourceFile2 = joinPath(sourceDir, "app2")
    writeFile(sourceFile1, "app1 content")
    writeFile(sourceFile2, "app2 content")

    let targetFile1 = joinPath(indexRoot, "bin", "app1")
    let targetFile2 = joinPath(indexRoot, "bin", "app2")

    # Create symlink pairs
    let pairs = @[
      SymlinkPair(source: sourceFile1, target: targetFile1),
      SymlinkPair(source: sourceFile2, target: targetFile2)
    ]

    # Perform atomic update
    let result = fm.atomicSymlinkUpdate(pairs)
    check result.isOk

    # Verify symlinks were created
    check symlinkExists(targetFile1)
    check symlinkExists(targetFile2)
    check expandSymlink(targetFile1) == sourceFile1
    check expandSymlink(targetFile2) == sourceFile2

  test "Generate symlinks for program directory":
    # Create test program directory with files
    let programDir = joinPath(programsRoot, "Test", "1.0.0")
    createDir(programDir)

    for dir in ["bin", "lib", "share", "etc"]:
      let dirPath = joinPath(programDir, dir)
      createDir(dirPath)
      # Create a test file in each directory
      writeFile(joinPath(dirPath, fmt"test_{dir}"), fmt"test content for {dir}")

    # Generate symlinks
    let result = fm.generateSymlinks(programDir)
    check result.isOk

    let symlinks = result.get()
    check symlinks.len == 4  # One for each directory

    # Verify symlink structure
    for pair in symlinks:
      check pair.source.startsWith(programDir)
      check pair.target.startsWith(indexRoot)

  test "Install and uninstall package":
    let pkg = PackageId(name: "testapp", version: "2.0.0", stream: Stable)

    # Install package
    let installResult = fm.installPackage(pkg)
    check installResult.isOk

    let location = installResult.get()
    check dirExists(location.programDir)

    # Create some test files to generate real symlinks
    let binDir = joinPath(location.programDir, "bin")
    let appPath = joinPath(binDir, "testapp")
    writeFile(appPath, "test app content")

    # Re-generate symlinks
    let symlinksResult = fm.generateSymlinks(location.programDir)
    check symlinksResult.isOk
    let symlinks = symlinksResult.get()

    # Apply symlinks
    let updateResult = fm.atomicSymlinkUpdate(symlinks)
    check updateResult.isOk

    # Verify symlink was created
    let indexBinPath = joinPath(indexRoot, "bin", "testapp")
    check symlinkExists(indexBinPath)

    # Uninstall package
    let uninstallResult = fm.uninstallPackage(pkg)
    check uninstallResult.isOk

    # Verify directory and symlinks were removed
    check not dirExists(location.programDir)
    check not symlinkExists(indexBinPath)

  test "Validate filesystem state":
    # Create a broken symlink
    let sourceDir = joinPath(programsRoot, "Test", "1.0.0", "bin")
    createDir(sourceDir)
    let sourceFile = joinPath(sourceDir, "testapp")
    writeFile(sourceFile, "test content")

    let targetDir = joinPath(indexRoot, "bin")
    createDir(targetDir)
    let targetFile = joinPath(targetDir, "testapp")

    # Create symlink
    discard fm.createSymlink(sourceFile, targetFile)

    # Remove source file to create broken symlink
    removeFile(sourceFile)

    # Validate filesystem state
    let result = fm.validateFilesystemState()
    check result.isOk

    let issues = result.get()
    if issues.len == 0:
      fail()
    else:
      check issues.len >= 1 # At least one issue (broken symlink)
      check issues[0].contains("Broken symlink")

  test "Repair filesystem":
    # Remove root directories
    removeDir(programsRoot)
    removeDir(indexRoot)

    # Create a broken symlink
    # createDir(indexRoot / "bin") # REMOVED THIS LINE
    createSymlink("/tmp/nonexistent_path_for_testing", indexRoot / "bin" / "broken")

    # Repair filesystem
    let result = fm.repairFilesystem()
    check result.isOk

    let repairs = result.get()
    check repairs.len >= 2  # At least created programs root and removed broken symlink

    # Verify repairs
    check dirExists(programsRoot)
    check dirExists(indexRoot)
    check not symlinkExists(indexRoot / "bin" / "broken")

  test "Dry run mode":
    let dryRunFm = newFilesystemManager(
      programsRoot = programsRoot,
      indexRoot = indexRoot,
      logFile = logFile,
      dryRun = true
    )

    # Remove directories to test dry run
    removeDir(programsRoot)
    removeDir(indexRoot)

    let pkg = PackageId(name: "drytest", version: "1.0.0", stream: Stable)
    let result = dryRunFm.createProgramDirectory(pkg)

    # Should succeed in dry run mode
    check result.isOk

    # But directory should not actually be created
    check not dirExists(joinPath(programsRoot, "Drytest", "1.0.0"))

  test "Error handling for invalid paths":
    let invalidFm = newFilesystemManager(
      programsRoot = "/nonexistent/invalid/path",
      indexRoot = indexRoot,
      logFile = logFile,
      dryRun = false
    )

    let pkg = PackageId(name: "test", version: "1.0.0", stream: Stable)
    let result = invalidFm.createProgramDirectory(pkg)

    check result.isErr
    let error = result.getError()
    check error.kind == DirectoryCreationFailed
