## Tests for Dependency Graph
##
## Tests the core graph operations: node/edge addition, cycle detection,
## and graph analysis functions.

import unittest
import tables
import sets
import options
import ../src/nip/resolver/dependency_graph
import ../src/nip/resolver/variant_types
import ../src/nip/manifest_parser

suite "Dependency Graph Tests":
  
  # Helper to create a dummy variant profile
  proc makeVariant(hash: string): VariantProfile =
    VariantProfile(
      domains: initTable[string, VariantDomain](),
      hash: hash
    )

  # Helper to create a dummy term
  proc makeTerm(name, ver, hash: string): PackageTerm =
    let id = createTermId(name, hash)
    PackageTerm(
      id: id,
      packageName: name,
      version: parseSemanticVersion(ver),
      variantHash: hash,
      variantProfile: makeVariant(hash),
      optional: false,
      source: "test"
    )

  test "Create empty graph":
    let graph = newDependencyGraph()
    check graph.nodeCount() == 0
    check graph.edgeCount() == 0
    check graph.getRoots().len == 0
    check graph.getLeaves().len == 0
  
  test "Add single node":
    var graph = newDependencyGraph()
    let term = makeTerm("nginx", "1.24.0", "abc123")
    graph.addTerm(term)
    
    check graph.nodeCount() == 1
    check graph.edgeCount() == 0
    check graph.getRoots().len == 1
    check graph.getLeaves().len == 1
  
  test "Add multiple nodes":
    var graph = newDependencyGraph()
    let nginx = makeTerm("nginx", "1.24.0", "abc123")
    let openssl = makeTerm("openssl", "3.0.0", "def456")
    let zlib = makeTerm("zlib", "1.2.13", "ghi789")
    
    graph.addTerm(nginx)
    graph.addTerm(openssl)
    graph.addTerm(zlib)
    
    check graph.nodeCount() == 3
    check graph.getRoots().len == 3
  
  test "Add edge between nodes":
    var graph = newDependencyGraph()
    let nginx = makeTerm("nginx", "1.24.0", "abc123")
    let openssl = makeTerm("openssl", "3.0.0", "def456")
    
    graph.addTerm(nginx)
    graph.addTerm(openssl)
    
    # Use explicit edge construction
    graph.addEdge(DependencyEdge(
      fromTerm: nginx.id,
      toTerm: openssl.id,
      dependencyType: Required
    ))
    
    check graph.nodeCount() == 2
    check graph.edgeCount() == 1
    check graph.getRoots().len == 1  # nginx is root
    check graph.getLeaves().len == 1  # openssl is leaf
    
    # Verify edge properties
    let edges = graph.getOutgoingEdges(nginx.id)
    check edges.len == 1
    check edges[0].fromTerm == nginx.id
    check edges[0].toTerm == openssl.id
  
  test "Get incoming edges":
    var graph = newDependencyGraph()
    let nginx = makeTerm("nginx", "1.24.0", "abc123")
    let openssl = makeTerm("openssl", "3.0.0", "def456")
    
    graph.addTerm(nginx)
    graph.addTerm(openssl)
    graph.addEdge(DependencyEdge(fromTerm: nginx.id, toTerm: openssl.id, dependencyType: Required))
    
    let incoming = graph.getIncomingEdges(openssl.id)
    check incoming.len == 1
    check incoming[0].fromTerm == nginx.id
    check incoming[0].toTerm == openssl.id
  
  test "Get outgoing edges":
    var graph = newDependencyGraph()
    let nginx = makeTerm("nginx", "1.24.0", "abc123")
    let openssl = makeTerm("openssl", "3.0.0", "def456")
    
    graph.addTerm(nginx)
    graph.addTerm(openssl)
    graph.addEdge(DependencyEdge(fromTerm: nginx.id, toTerm: openssl.id, dependencyType: Required))
    
    let outgoing = graph.getOutgoingEdges(nginx.id)
    check outgoing.len == 1
    check outgoing[0].fromTerm == nginx.id
    check outgoing[0].toTerm == openssl.id
  
  test "Get dependencies":
    var graph = newDependencyGraph()
    let nginx = makeTerm("nginx", "1.24.0", "abc123")
    let openssl = makeTerm("openssl", "3.0.0", "def456")
    let zlib = makeTerm("zlib", "1.2.13", "ghi789")
    
    graph.addTerm(nginx)
    graph.addTerm(openssl)
    graph.addTerm(zlib)
    
    graph.addEdge(DependencyEdge(fromTerm: nginx.id, toTerm: openssl.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: nginx.id, toTerm: zlib.id, dependencyType: Required))
    
    let deps = graph.getDependencies(nginx.id)
    check deps.len == 2
    check openssl in deps
    check zlib in deps
  
  test "Get dependents":
    var graph = newDependencyGraph()
    let nginx = makeTerm("nginx", "1.24.0", "abc123")
    let openssl = makeTerm("openssl", "3.0.0", "def456")
    let curl = makeTerm("curl", "8.0.0", "jkl012")
    
    graph.addTerm(nginx)
    graph.addTerm(openssl)
    graph.addTerm(curl)
    
    graph.addEdge(DependencyEdge(fromTerm: nginx.id, toTerm: openssl.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: curl.id, toTerm: openssl.id, dependencyType: Required))
    
    let dependents = graph.getDependents(openssl.id)
    check dependents.len == 2
    check nginx in dependents
    check curl in dependents
  
  test "Detect no cycle in simple chain":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    let c = makeTerm("c", "1.0.0", "c")
    
    graph.addTerm(a)
    graph.addTerm(b)
    graph.addTerm(c)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: b.id, toTerm: c.id, dependencyType: Required))
    
    check graph.hasCycle() == false
  
  test "Detect cycle in simple loop":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    
    graph.addTerm(a)
    graph.addTerm(b)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: b.id, toTerm: a.id, dependencyType: Required))
    
    check graph.hasCycle() == true
  
  test "Detect cycle in self-loop":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    
    graph.addTerm(a)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: a.id, dependencyType: Required))
    
    check graph.hasCycle() == true
  
  test "Detect cycle in complex graph":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    let c = makeTerm("c", "1.0.0", "c")
    let d = makeTerm("d", "1.0.0", "d")
    
    graph.addTerm(a)
    graph.addTerm(b)
    graph.addTerm(c)
    graph.addTerm(d)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: b.id, toTerm: c.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: c.id, toTerm: d.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: d.id, toTerm: b.id, dependencyType: Required))  # Creates cycle: b -> c -> d -> b
    
    check graph.hasCycle() == true
  
  test "Find cycle path":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    
    graph.addTerm(a)
    graph.addTerm(b)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: b.id, toTerm: a.id, dependencyType: Required))
    
    let cycle = graph.findCycle()
    check cycle.len >= 2
    # Check if terms are in cycle (by checking if any term in cycle has same ID)
    var foundA = false
    var foundB = false
    for term in cycle:
      if term.id == a.id: foundA = true
      if term.id == b.id: foundB = true
    
    check foundA
    check foundB
  
  test "Get roots":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    let c = makeTerm("c", "1.0.0", "c")
    
    graph.addTerm(a)
    graph.addTerm(b)
    graph.addTerm(c)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: c.id, dependencyType: Required))
    
    let roots = graph.getRoots()
    check roots.len == 1
    check roots[0].id == a.id
  
  test "Get leaves":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    let c = makeTerm("c", "1.0.0", "c")
    
    graph.addTerm(a)
    graph.addTerm(b)
    graph.addTerm(c)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: c.id, dependencyType: Required))
    
    let leaves = graph.getLeaves()
    check leaves.len == 2
    # Check IDs
    let leafIds = @[leaves[0].id, leaves[1].id]
    check b.id in leafIds
    check c.id in leafIds
  
  test "Get depth":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    let c = makeTerm("c", "1.0.0", "c")
    
    graph.addTerm(a)
    graph.addTerm(b)
    graph.addTerm(c)
    
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: b.id, toTerm: c.id, dependencyType: Required))
    
    check graph.getDepth(a.id) == 2
    check graph.getDepth(b.id) == 1
    check graph.getDepth(c.id) == 0
  
  test "Diamond dependency graph":
    var graph = newDependencyGraph()
    let a = makeTerm("a", "1.0.0", "a")
    let b = makeTerm("b", "1.0.0", "b")
    let c = makeTerm("c", "1.0.0", "c")
    let d = makeTerm("d", "1.0.0", "d")
    
    graph.addTerm(a)
    graph.addTerm(b)
    graph.addTerm(c)
    graph.addTerm(d)
    
    # Diamond: a -> b,c -> d
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: b.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: a.id, toTerm: c.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: b.id, toTerm: d.id, dependencyType: Required))
    graph.addEdge(DependencyEdge(fromTerm: c.id, toTerm: d.id, dependencyType: Required))
    
    check graph.nodeCount() == 4
    check graph.edgeCount() == 4
    check graph.hasCycle() == false
    check graph.getDependencies(a.id).len == 2
    check graph.getDependents(d.id).len == 2
