## Unit Tests for Graph Builder
##
## Tests for the dependency graph builder which constructs complete
## dependency graphs from package demands.

import std/[unittest, options, tables, sequtils]
import ../src/nip/resolver/graph_builder
import ../src/nip/resolver/dependency_graph
import ../src/nip/resolver/variant_types

suite "Graph Builder Tests":
  
  test "Build simple chain (A → B → C)":
    ## Test building a simple dependency chain
    
    # Create variant profiles
    var profileA = newVariantProfile()
    profileA.addFlag("optimization", "lto")
    profileA.calculateHash()
    
    var profileB = newVariantProfile()
    profileB.addFlag("optimization", "lto")
    profileB.calculateHash()
    
    var profileC = newVariantProfile()
    profileC.addFlag("optimization", "lto")
    profileC.calculateHash()
    
    # Create demands
    let demandA = VariantDemand(
      packageName: "packageA",
      variantProfile: profileA,
      optional: false
    )
    
    let demandB = VariantDemand(
      packageName: "packageB",
      variantProfile: profileB,
      optional: false
    )
    
    let demandC = VariantDemand(
      packageName: "packageC",
      variantProfile: profileC,
      optional: false
    )
    
    # Create dependency map: A -> B, B -> C, C -> []
    let dependencyMap = {
      "packageA": @[demandB],
      "packageB": @[demandC],
      "packageC": @[]
    }.toTable()
    
    # Build graph
    let buildResult = buildSimpleGraph(@[demandA], dependencyMap)
    check buildResult.conflicts.len == 0
    check buildResult.warnings.len == 0
    
    let graph = buildResult.graph
    check graph.getStats().terms == 3
    check graph.getStats().edges == 2
    
    # Verify chain structure
    let rootTerms = graph.getRootTerms()
    check rootTerms.len == 1  # Only A should be root
    
    let leafTerms = graph.getLeafTerms()
    check leafTerms.len == 1  # Only C should be leaf
  
  test "Build diamond (A → B,C → D)":
    ## Test building a diamond dependency structure
    
    # Create variant profiles (all compatible)
    var profile = newVariantProfile()
    profile.addFlag("optimization", "lto")
    profile.calculateHash()
    
    # Create demands
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile, optional: false)
    let demandB = VariantDemand(packageName: "packageB", variantProfile: profile, optional: false)
    let demandC = VariantDemand(packageName: "packageC", variantProfile: profile, optional: false)
    let demandD = VariantDemand(packageName: "packageD", variantProfile: profile, optional: false)
    
    # Create dependency map: A -> [B,C], B -> D, C -> D, D -> []
    let dependencyMap = {
      "packageA": @[demandB, demandC],
      "packageB": @[demandD],
      "packageC": @[demandD],
      "packageD": @[]
    }.toTable()
    
    # Build graph
    let buildResult = buildSimpleGraph(@[demandA], dependencyMap)
    check buildResult.conflicts.len == 0
    
    let graph = buildResult.graph
    check graph.getStats().terms == 4
    check graph.getStats().edges == 4  # A->B, A->C, B->D, C->D
    
    # Verify diamond structure
    let rootTerms = graph.getRootTerms()
    check rootTerms.len == 1  # Only A should be root
    
    let leafTerms = graph.getLeafTerms()
    check leafTerms.len == 1  # Only D should be leaf
    
    # D should have 2 incoming edges (from B and C)
    let dTerms = graph.getTermsByPackage("packageD")
    check dTerms.len == 1
    let dIncoming = graph.getIncomingEdges(dTerms[0].id)
    check dIncoming.len == 2
  
  test "Build multiple roots":
    ## Test building graph with multiple root packages
    
    var profile = newVariantProfile()
    profile.addFlag("optimization", "lto")
    profile.calculateHash()
    
    # Create demands
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile, optional: false)
    let demandB = VariantDemand(packageName: "packageB", variantProfile: profile, optional: false)
    let demandC = VariantDemand(packageName: "packageC", variantProfile: profile, optional: false)
    
    # Create dependency map: A -> C, B -> C, C -> []
    let dependencyMap = {
      "packageA": @[demandC],
      "packageB": @[demandC],
      "packageC": @[]
    }.toTable()
    
    # Build graph with multiple roots
    let buildResult = buildSimpleGraph(@[demandA, demandB], dependencyMap)
    check buildResult.conflicts.len == 0
    
    let graph = buildResult.graph
    check graph.getStats().terms == 3
    check graph.getStats().edges == 2  # A->C, B->C
    
    # Should have 2 root terms (A and B)
    let rootTerms = graph.getRootTerms()
    check rootTerms.len == 2
    
    # Should have 1 leaf term (C)
    let leafTerms = graph.getLeafTerms()
    check leafTerms.len == 1
  
  test "Circular dependency detection":
    ## Test detection of circular dependencies
    
    var profile = newVariantProfile()
    profile.addFlag("optimization", "lto")
    profile.calculateHash()
    
    # Create demands
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile, optional: false)
    let demandB = VariantDemand(packageName: "packageB", variantProfile: profile, optional: false)
    
    # Create circular dependency: A -> B -> A
    let dependencyMap = {
      "packageA": @[demandB],
      "packageB": @[demandA]
    }.toTable()
    
    # Build graph
    let buildResult = buildSimpleGraph(@[demandA], dependencyMap)
    
    let graph = buildResult.graph
    check graph.getStats().terms == 2
    check graph.getStats().edges == 2
    
    # Graph should detect the cycle
    check graph.hasCycle()
    
    let cycles = graph.findCycles()
    check cycles.len > 0
  
  test "Variant unification during build":
    ## Test that variant unification happens during graph building
    
    # Create compatible variant profiles
    var profile1 = newVariantProfile()
    profile1.addFlag("optimization", "lto")
    profile1.calculateHash()
    
    var profile2 = newVariantProfile()
    profile2.addFlag("security", "hardened")
    profile2.calculateHash()
    
    # Create demands for same package with different variants
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile1, optional: false)
    let demandShared1 = VariantDemand(packageName: "shared", variantProfile: profile1, optional: false)
    let demandShared2 = VariantDemand(packageName: "shared", variantProfile: profile2, optional: false)
    
    # Create dependency map: A -> shared, B -> shared
    let dependencyMap = {
      "packageA": @[demandShared1],
      "packageB": @[demandShared2],
      "shared": @[]
    }.toTable()
    
    # Build graph with both A and B as roots
    let demandB = VariantDemand(packageName: "packageB", variantProfile: profile2, optional: false)
    let buildResult = buildSimpleGraph(@[demandA, demandB], dependencyMap)
    check buildResult.conflicts.len == 0
    
    let graph = buildResult.graph
    
    # Should have unified the "shared" package
    let sharedTerms = graph.getTermsByPackage("shared")
    check sharedTerms.len == 1  # Should be unified into single term
    
    # The unified variant should have both flags
    let unifiedProfile = sharedTerms[0].variantProfile
    check unifiedProfile.hasDomain("optimization")
    check unifiedProfile.hasDomain("security")
  
  test "Handle missing package gracefully":
    ## Test handling of missing packages in dependency map
    
    var profile = newVariantProfile()
    profile.addFlag("optimization", "lto")
    profile.calculateHash()
    
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile, optional: false)
    let demandMissing = VariantDemand(packageName: "missing", variantProfile: profile, optional: false)
    
    # Create dependency map where A depends on missing package
    let dependencyMap = {
      "packageA": @[demandMissing]
      # "missing" is not in the map
    }.toTable()
    
    # Build graph
    let buildResult = buildSimpleGraph(@[demandA], dependencyMap)
    
    # Should create terms for both A and missing (missing gets added as a demand)
    let graph = buildResult.graph
    let aTerms = graph.getTermsByPackage("packageA")
    check aTerms.len == 1
    
    let missingTerms = graph.getTermsByPackage("missing")
    check missingTerms.len == 1  # Missing package is added as a term
    
    # There should be an edge from A to missing since A depends on missing
    let aOutgoing = graph.getOutgoingEdges(aTerms[0].id)
    check aOutgoing.len == 1  # Edge from A to missing
    check aOutgoing[0].toTerm == missingTerms[0].id
  
  test "Handle variant conflicts":
    ## Test handling of variant conflicts during unification
    
    # Create conflicting variant profiles (exclusive flags)
    var profile1 = newVariantProfile()
    profile1.addDomain(newVariantDomain("libc", Exclusive))
    profile1.addFlag("libc", "glibc")  # Exclusive flag
    profile1.calculateHash()
    
    var profile2 = newVariantProfile()
    profile2.addDomain(newVariantDomain("libc", Exclusive))
    profile2.addFlag("libc", "musl")   # Conflicting exclusive flag
    profile2.calculateHash()
    
    # Create demands for same package with conflicting variants
    let demandShared1 = VariantDemand(packageName: "shared", variantProfile: profile1, optional: false)
    let demandShared2 = VariantDemand(packageName: "shared", variantProfile: profile2, optional: false)
    
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile1, optional: false)
    let demandB = VariantDemand(packageName: "packageB", variantProfile: profile2, optional: false)
    
    # Create dependency map: A -> shared, B -> shared
    let dependencyMap = {
      "packageA": @[demandShared1],
      "packageB": @[demandShared2],
      "shared": @[]
    }.toTable()
    
    # Build graph
    let buildResult = buildSimpleGraph(@[demandA, demandB], dependencyMap)
    
    # Should detect variant conflict
    check buildResult.conflicts.len > 0
    check buildResult.warnings.len > 0
    
    # Conflicted package should not appear in graph
    let graph = buildResult.graph
    let sharedTerms = graph.getTermsByPackage("shared")
    check sharedTerms.len == 0  # Conflicted package excluded
  
  test "Validate graph structure":
    ## Test graph validation functionality
    
    var profile = newVariantProfile()
    profile.addFlag("optimization", "lto")
    profile.calculateHash()
    
    let demandA = VariantDemand(packageName: "packageA", variantProfile: profile, optional: false)
    let demandB = VariantDemand(packageName: "packageB", variantProfile: profile, optional: false)
    
    let dependencyMap = {
      "packageA": @[demandB],
      "packageB": @[]
    }.toTable()
    
    let buildResult = buildSimpleGraph(@[demandA], dependencyMap)
    let graph = buildResult.graph
    
    # Graph should be valid
    let isValid = validateGraph(graph)
    check isValid
  
  test "Get root and leaf terms":
    ## Test identification of root and leaf terms
    
    var profile = newVariantProfile()
    profile.addFlag("optimization", "lto")
    profile.calculateHash()
    
    # Create chain: root -> middle -> leaf
    let demandRoot = VariantDemand(packageName: "root", variantProfile: profile, optional: false)
    let demandMiddle = VariantDemand(packageName: "middle", variantProfile: profile, optional: false)
    let demandLeaf = VariantDemand(packageName: "leaf", variantProfile: profile, optional: false)
    
    let dependencyMap = {
      "root": @[demandMiddle],
      "middle": @[demandLeaf],
      "leaf": @[]
    }.toTable()
    
    let buildResult = buildSimpleGraph(@[demandRoot], dependencyMap)
    let graph = buildResult.graph
    
    # Check root terms
    let rootTerms = getRootTerms(graph)
    check rootTerms.len == 1
    let rootTerm = graph.getTerm(rootTerms[0]).get
    check rootTerm.packageName == "root"
    
    # Check leaf terms
    let leafTerms = getLeafTerms(graph)
    check leafTerms.len == 1
    let leafTerm = graph.getTerm(leafTerms[0]).get
    check leafTerm.packageName == "leaf"

  test "Property: Graph Completeness":
    ## **Property 3: Graph Completeness**
    ## **Validates: Requirements 3.1, 3.5**
    ## 
    ## For any set of package demands, the resulting dependency graph
    ## should contain all reachable packages and their dependencies.
    
    # Test with various graph structures
    for i in 1..10:
      var profile = newVariantProfile()
      profile.addFlag("test", "property" & $i)
      profile.calculateHash()
      
      # Create a chain of dependencies: pkg1 -> pkg2 -> pkg3 -> ... -> pkgN
      let chainLength = (i mod 5) + 2  # Chain length between 2 and 6
      var demands: seq[VariantDemand] = @[]
      var dependencyMap = initTable[string, seq[VariantDemand]]()
      
      # Build chain
      for j in 1..chainLength:
        let pkgName = "pkg" & $j
        let demand = VariantDemand(
          packageName: pkgName,
          variantProfile: profile,
          optional: false
        )
        demands.add(demand)
        
        # Each package depends on the next one (except the last)
        if j < chainLength:
          let nextPkgName = "pkg" & $(j + 1)
          let nextDemand = VariantDemand(
            packageName: nextPkgName,
            variantProfile: profile,
            optional: false
          )
          dependencyMap[pkgName] = @[nextDemand]
        else:
          dependencyMap[pkgName] = @[]  # Last package has no dependencies
      
      # Build graph from first package only
      let buildResult = buildSimpleGraph(@[demands[0]], dependencyMap)
      let graph = buildResult.graph
      
      # Property: Graph should contain all packages in the chain
      check graph.getStats().terms == chainLength
      
      # Property: Graph should have exactly (chainLength - 1) edges
      check graph.getStats().edges == chainLength - 1
      
      # Property: Each package in the chain should be present
      for j in 1..chainLength:
        let pkgName = "pkg" & $j
        let terms = graph.getTermsByPackage(pkgName)
        check terms.len == 1  # Each package should appear exactly once
      
      # Property: Graph should be acyclic
      check not graph.hasCycle()
      
      # Property: Graph should have exactly one root (pkg1)
      let rootTerms = graph.getRootTerms()
      check rootTerms.len == 1
      
      # Property: Graph should have exactly one leaf (last package)
      let leafTerms = graph.getLeafTerms()
      check leafTerms.len == 1