## Unit Tests for NipCell Fallback
##
## This module tests the NipCell isolation fallback mechanism for the
## NIP dependency resolver.
##
## **Requirements Tested:**
## - 10.1: Detect unresolvable conflicts and suggest NipCell isolation
## - 10.2: Create separate NipCells for conflicting packages
## - 10.3: Maintain separate dependency graphs per cell
## - 10.4: Support cell switching
## - 10.5: Clean up cell-specific packages when removing cells

import std/[unittest, options, sets, tables, strutils, strformat, json]
import ../src/nip/resolver/nipcell_fallback
import ../src/nip/resolver/conflict_detection
import ../src/nip/resolver/solver_types

# =============================================================================
# Test Helpers
# =============================================================================

proc createVersionConflict(pkg: string): ConflictReport =
  ## Create a test version conflict
  ConflictReport(
    kind: VersionConflict,
    packages: @[pkg],
    details: fmt"Package '{pkg}' has conflicting version requirements",
    suggestions: @["Try relaxing version constraints"],
    conflictingTerms: @[],
    cyclePath: none(seq[string])
  )

proc createVariantConflict(pkg: string, domain: string = "init"): ConflictReport =
  ## Create a test variant conflict
  ConflictReport(
    kind: VariantConflict,
    packages: @[pkg],
    details: fmt"Package '{pkg}' has conflicting exclusive variant flags in domain '{domain}'",
    suggestions: @["Consider using NipCell isolation"],
    conflictingTerms: @[],
    cyclePath: none(seq[string])
  )

proc createCircularDependency(packages: seq[string]): ConflictReport =
  ## Create a test circular dependency conflict
  let cycleStr = packages.join(" -> ")
  ConflictReport(
    kind: CircularDependency,
    packages: packages,
    details: "Circular dependency detected: " & cycleStr,
    suggestions: @["Break the cycle"],
    conflictingTerms: @[],
    cyclePath: some(packages)
  )

# =============================================================================
# Conflict Severity Analysis Tests
# =============================================================================

suite "Conflict Severity Analysis":
  test "Version conflict has low severity":
    let conflict = createVersionConflict("openssl")
    let severity = analyzeConflictSeverity(conflict)
    check severity == Low
  
  test "Variant conflict with exclusive domain has high severity":
    let conflict = createVariantConflict("systemd", "init")
    let severity = analyzeConflictSeverity(conflict)
    check severity == High
  
  test "Circular dependency has critical severity":
    let conflict = createCircularDependency(@["a", "b", "c", "a"])
    let severity = analyzeConflictSeverity(conflict)
    check severity == Critical
  
  test "Low severity does not suggest isolation":
    check shouldSuggestIsolation(Low) == false
  
  test "Medium severity suggests isolation":
    check shouldSuggestIsolation(Medium) == true
  
  test "High severity suggests isolation":
    check shouldSuggestIsolation(High) == true
  
  test "Critical severity suggests isolation":
    check shouldSuggestIsolation(Critical) == true

# =============================================================================
# Isolation Candidate Detection Tests
# =============================================================================

suite "Isolation Candidate Detection":
  test "Detect candidates from variant conflict":
    let conflicts = @[createVariantConflict("openssl", "crypto")]
    let candidates = detectIsolationCandidates(conflicts)
    
    check candidates.len >= 1
    check candidates[0].packageName == "openssl"
    check candidates[0].suggestedCellName == "openssl-cell"
  
  test "No candidates for low severity conflicts":
    let conflicts = @[createVersionConflict("zlib")]
    let candidates = detectIsolationCandidates(conflicts)
    
    # Version conflicts are low severity, should not suggest isolation
    check candidates.len == 0
  
  test "Detect candidates from circular dependency":
    let conflicts = @[createCircularDependency(@["a", "b", "c", "a"])]
    let candidates = detectIsolationCandidates(conflicts)
    
    # Circular dependencies are critical, should suggest isolation
    check candidates.len >= 1
  
  test "Multiple conflicts generate multiple candidates":
    let conflicts = @[
      createVariantConflict("openssl", "crypto"),
      createVariantConflict("nginx", "http")
    ]
    let candidates = detectIsolationCandidates(conflicts)
    
    check candidates.len >= 2

# =============================================================================
# Isolation Suggestion Generation Tests
# =============================================================================

suite "Isolation Suggestion Generation":
  test "Generate suggestion with commands":
    let conflict = createVariantConflict("openssl", "crypto")
    let candidates = @[
      IsolationCandidate(
        packageName: "openssl",
        conflictingWith: @["nginx"],
        severity: High,
        suggestedCellName: "openssl-cell",
        reason: "Exclusive domain conflict"
      )
    ]
    
    let suggestion = generateIsolationSuggestion(conflict, candidates)
    
    check suggestion.candidates.len == 1
    check suggestion.suggestedCells.len == 1
    check suggestion.commands.len >= 1
    check suggestion.explanation.len > 0
  
  test "Suggestion includes cell creation command":
    let conflict = createVariantConflict("openssl", "crypto")
    let candidates = @[
      IsolationCandidate(
        packageName: "openssl",
        conflictingWith: @[],
        severity: High,
        suggestedCellName: "openssl-cell",
        reason: "Conflict"
      )
    ]
    
    let suggestion = generateIsolationSuggestion(conflict, candidates)
    
    var hasCreateCommand = false
    for cmd in suggestion.commands:
      if cmd.contains("cell create"):
        hasCreateCommand = true
        break
    
    check hasCreateCommand
  
  test "Format suggestion produces readable output":
    let conflict = createVariantConflict("openssl", "crypto")
    let candidates = @[
      IsolationCandidate(
        packageName: "openssl",
        conflictingWith: @["nginx"],
        severity: High,
        suggestedCellName: "openssl-cell",
        reason: "Conflict"
      )
    ]
    
    let suggestion = generateIsolationSuggestion(conflict, candidates)
    let formatted = formatIsolationSuggestion(suggestion)
    
    check formatted.contains("IsolationSuggested")
    check formatted.contains("openssl")
    check formatted.contains("Suggested commands")

# =============================================================================
# NipCell Graph Manager Tests
# =============================================================================

suite "NipCell Graph Manager - Cell Creation":
  test "Create new cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    let result = manager.createCell("test-cell", "Test cell description")
    
    check result.success == true
    check result.cellName == "test-cell"
    check result.cellId.len > 0
    check result.error == ""
  
  test "Cannot create duplicate cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    discard manager.createCell("test-cell")
    let result = manager.createCell("test-cell")
    
    check result.success == false
    check result.error.contains("already exists")
  
  test "List cells returns created cells":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    discard manager.createCell("cell-a")
    discard manager.createCell("cell-b")
    discard manager.createCell("cell-c")
    
    let cells = manager.listCells()
    
    check cells.len == 3
    check "cell-a" in cells
    check "cell-b" in cells
    check "cell-c" in cells

# =============================================================================
# NipCell Graph Manager Tests - Cell Switching
# =============================================================================

suite "NipCell Graph Manager - Cell Switching":
  test "Switch to existing cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    
    let result = manager.switchCell("test-cell")
    
    check result.success == true
    check result.newCell == "test-cell"
    check result.error == ""
  
  test "Cannot switch to non-existent cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    let result = manager.switchCell("non-existent")
    
    check result.success == false
    check result.error.contains("not found")
  
  test "Get active cell after switch":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    discard manager.switchCell("test-cell")
    
    let activeCell = manager.getActiveCell()
    
    check activeCell.isSome
    check activeCell.get() == "test-cell"
  
  test "No active cell initially":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    let activeCell = manager.getActiveCell()
    
    check activeCell.isNone
  
  test "Switch tracks previous cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("cell-a")
    discard manager.createCell("cell-b")
    
    discard manager.switchCell("cell-a")
    let result = manager.switchCell("cell-b")
    
    check result.success == true
    check result.previousCell.isSome
    check result.previousCell.get() == "cell-a"
    check result.newCell == "cell-b"

# =============================================================================
# NipCell Graph Manager Tests - Separate Graphs
# =============================================================================

suite "NipCell Graph Manager - Separate Graphs":
  test "Each cell has its own graph":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("cell-a")
    discard manager.createCell("cell-b")
    
    let graphA = manager.getCellGraph("cell-a")
    let graphB = manager.getCellGraph("cell-b")
    
    check graphA.isSome
    check graphB.isSome
    check graphA.get().cellName == "cell-a"
    check graphB.get().cellName == "cell-b"
    check graphA.get().cellId != graphB.get().cellId
  
  test "Get active cell graph":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    discard manager.switchCell("test-cell")
    
    let graph = manager.getActiveCellGraph()
    
    check graph.isSome
    check graph.get().cellName == "test-cell"
  
  test "No active graph when no cell active":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    
    let graph = manager.getActiveCellGraph()
    
    check graph.isNone

# =============================================================================
# NipCell Graph Manager Tests - Package Management
# =============================================================================

suite "NipCell Graph Manager - Package Management":
  test "Add package to cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    
    let result = manager.addPackageToCell("test-cell", "nginx")
    
    check result == true
    check manager.isPackageInCell("test-cell", "nginx")
  
  test "Cannot add package to non-existent cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    let result = manager.addPackageToCell("non-existent", "nginx")
    
    check result == false
  
  test "Remove package from cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    discard manager.addPackageToCell("test-cell", "nginx")
    
    let result = manager.removePackageFromCell("test-cell", "nginx")
    
    check result == true
    check not manager.isPackageInCell("test-cell", "nginx")
  
  test "Get cell packages":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    discard manager.addPackageToCell("test-cell", "nginx")
    discard manager.addPackageToCell("test-cell", "openssl")
    discard manager.addPackageToCell("test-cell", "zlib")
    
    let packages = manager.getCellPackages("test-cell")
    
    check packages.len == 3
    check "nginx" in packages
    check "openssl" in packages
    check "zlib" in packages
  
  test "Packages are isolated between cells":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("cell-a")
    discard manager.createCell("cell-b")
    
    discard manager.addPackageToCell("cell-a", "nginx")
    discard manager.addPackageToCell("cell-b", "apache")
    
    check manager.isPackageInCell("cell-a", "nginx")
    check not manager.isPackageInCell("cell-a", "apache")
    check manager.isPackageInCell("cell-b", "apache")
    check not manager.isPackageInCell("cell-b", "nginx")

# =============================================================================
# NipCell Graph Manager Tests - Cell Deletion
# =============================================================================

suite "NipCell Graph Manager - Cell Deletion":
  test "Delete existing cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    
    let result = manager.deleteCell("test-cell")
    
    check result == true
    check manager.listCells().len == 0
  
  test "Cannot delete non-existent cell":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    
    let result = manager.deleteCell("non-existent")
    
    check result == false
  
  test "Deleting active cell deactivates it":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    discard manager.switchCell("test-cell")
    
    discard manager.deleteCell("test-cell")
    
    check manager.getActiveCell().isNone
  
  test "Packages are cleaned up when cell is deleted":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    discard manager.createCell("test-cell")
    discard manager.addPackageToCell("test-cell", "nginx")
    discard manager.addPackageToCell("test-cell", "openssl")
    
    discard manager.deleteCell("test-cell")
    
    # Cell no longer exists, so packages are gone
    check manager.getCellPackages("test-cell").len == 0

# =============================================================================
# Conflict-Triggered Fallback Tests
# =============================================================================

suite "Conflict-Triggered Fallback":
  test "No suggestion for empty conflicts":
    let suggestion = checkForIsolationFallback(@[])
    check suggestion.isNone
  
  test "No suggestion for low severity conflicts":
    let conflicts = @[createVersionConflict("zlib")]
    let suggestion = checkForIsolationFallback(conflicts)
    check suggestion.isNone
  
  test "Suggestion for high severity conflicts":
    let conflicts = @[createVariantConflict("openssl", "crypto")]
    let suggestion = checkForIsolationFallback(conflicts)
    
    check suggestion.isSome
    check suggestion.get().candidates.len >= 1
  
  test "Handle unresolvable conflict with auto-create":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    let conflict = createVariantConflict("openssl", "crypto")
    
    let (suggestion, cellsCreated) = manager.handleUnresolvableConflict(
      conflict, autoCreate = true
    )
    
    check suggestion.candidates.len >= 1
    check cellsCreated.len >= 1
    check manager.listCells().len >= 1
  
  test "Handle unresolvable conflict without auto-create":
    let manager = newNipCellGraphManager("/tmp/test-cells")
    let conflict = createVariantConflict("openssl", "crypto")
    
    let (suggestion, cellsCreated) = manager.handleUnresolvableConflict(
      conflict, autoCreate = false
    )
    
    check suggestion.candidates.len >= 1
    check cellsCreated.len == 0
    check manager.listCells().len == 0

# =============================================================================
# Cell Serialization Tests
# =============================================================================

suite "Cell Serialization":
  test "Serialize cell to JSON":
    var cell = newNipCellGraph("test-cell", "test-id-123")
    cell.packages.incl("nginx")
    cell.packages.incl("openssl")
    cell.metadata["description"] = "Test cell"
    
    let json = cell.toJson()
    
    check json["cellName"].getStr() == "test-cell"
    check json["cellId"].getStr() == "test-id-123"
    check json["packages"].len == 2
    check json["metadata"]["description"].getStr() == "Test cell"
  
  test "Deserialize cell from JSON":
    let json = %*{
      "cellName": "test-cell",
      "cellId": "test-id-123",
      "packages": ["nginx", "openssl"],
      "created": "2025-01-01T00:00:00Z",
      "lastModified": "2025-01-01T00:00:00Z",
      "metadata": {"description": "Test cell"}
    }
    
    let cell = fromJson(json)
    
    check cell.cellName == "test-cell"
    check cell.cellId == "test-id-123"
    check "nginx" in cell.packages
    check "openssl" in cell.packages
    check cell.metadata["description"] == "Test cell"

# =============================================================================
# Run Tests
# =============================================================================

when isMainModule:
  echo "Running NipCell Fallback Tests..."
