## Unit Tests for Conflict Detection
##
## Tests for detecting and reporting various types of conflicts
## in the NIP dependency resolver.
##
## Requirements tested:
## - 7.1: Detect and report version conflicts
## - 7.2: Detect and report variant conflicts
## - 7.3: Detect and report circular dependencies
## - 7.4: Detect and report missing packages
## - 7.5: Provide actionable suggestions for resolution

import std/[unittest, tables, sets, options, sequtils, strutils]
import ../src/nip/resolver/conflict_detection
import ../src/nip/resolver/solver_types
import ../src/nip/resolver/variant_types
import ../src/nip/manifest_parser

suite "Conflict Detection Tests":
  
  # --- Version Conflict Tests ---
  
  test "Detect version conflict - exact versions":
    ## Test detecting conflicting exact version requirements
    ## Requirements: 7.1
    
    let constraints = @[
      VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)),
      VersionConstraint(operator: OpExact, version: SemanticVersion(major: 2, minor: 0, patch: 0))
    ]
    
    let conflict = detectVersionConflict("nginx", constraints)
    
    check conflict.isSome
    check conflict.get().kind == VersionConflict
    check conflict.get().packages.contains("nginx")
    check conflict.get().details.contains("1.0.0")
    check conflict.get().details.contains("2.0.0")
    check conflict.get().suggestions.len > 0
  
  test "No version conflict - compatible versions":
    ## Test that compatible versions don't trigger conflict
    ## Requirements: 7.1
    
    let constraints = @[
      VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 1, minor: 0, patch: 0)),
      VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 1, minor: 5, patch: 0))
    ]
    
    let conflict = detectVersionConflict("nginx", constraints)
    
    check conflict.isNone
  
  test "No version conflict - single constraint":
    ## Test that single constraint doesn't trigger conflict
    ## Requirements: 7.1
    
    let constraints = @[
      VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0))
    ]
    
    let conflict = detectVersionConflict("nginx", constraints)
    
    check conflict.isNone
  
  test "Detect version conflict - exact vs greater-equal":
    ## Test detecting conflict between exact and >= constraint
    ## Requirements: 7.1
    
    let constraints = @[
      VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)),
      VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 2, minor: 0, patch: 0))
    ]
    
    let conflict = detectVersionConflict("zlib", constraints)
    
    check conflict.isSome
    check conflict.get().kind == VersionConflict
    check conflict.get().suggestions.len > 0
  
  # --- Variant Conflict Tests ---
  
  test "Detect variant conflict - exclusive domains":
    ## Test detecting conflicting exclusive variant flags
    ## Requirements: 7.2
    
    var profile1 = newVariantProfile()
    var domain1 = newVariantDomain("init", Exclusive)
    domain1.flags.incl("systemd")
    profile1.addDomain(domain1)
    
    var profile2 = newVariantProfile()
    var domain2 = newVariantDomain("init", Exclusive)
    domain2.flags.incl("dinit")
    profile2.addDomain(domain2)
    
    let demands = @[
      VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false),
      VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false)
    ]
    
    let conflict = detectVariantConflict("nginx", demands)
    
    check conflict.isSome
    check conflict.get().kind == conflict_detection.VariantConflict
    check conflict.get().details.contains("init")
    check conflict.get().suggestions.len > 0
  
  test "No variant conflict - non-exclusive domains":
    ## Test that non-exclusive domains don't trigger conflict
    ## Requirements: 7.2
    
    var profile1 = newVariantProfile()
    var domain1 = newVariantDomain("features", NonExclusive)
    domain1.flags.incl("wayland")
    profile1.addDomain(domain1)
    
    var profile2 = newVariantProfile()
    var domain2 = newVariantDomain("features", NonExclusive)
    domain2.flags.incl("x11")
    profile2.addDomain(domain2)
    
    let demands = @[
      VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false),
      VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false)
    ]
    
    let conflict = detectVariantConflict("nginx", demands)
    
    check conflict.isNone
  
  test "No variant conflict - single demand":
    ## Test that single demand doesn't trigger conflict
    ## Requirements: 7.2
    
    var profile = newVariantProfile()
    var domain = newVariantDomain("init", Exclusive)
    domain.flags.incl("systemd")
    profile.addDomain(domain)
    
    let demands = @[
      VariantDemand(packageName: "nginx", variantProfile: profile, optional: false)
    ]
    
    let conflict = detectVariantConflict("nginx", demands)
    
    check conflict.isNone
  
  test "Detect variant conflict - multiple exclusive domains":
    ## Test detecting conflicts in multiple exclusive domains
    ## Requirements: 7.2
    
    var profile1 = newVariantProfile()
    var domain1a = newVariantDomain("init", Exclusive)
    domain1a.flags.incl("systemd")
    profile1.addDomain(domain1a)
    var domain1b = newVariantDomain("libc", Exclusive)
    domain1b.flags.incl("glibc")
    profile1.addDomain(domain1b)
    
    var profile2 = newVariantProfile()
    var domain2a = newVariantDomain("init", Exclusive)
    domain2a.flags.incl("dinit")
    profile2.addDomain(domain2a)
    var domain2b = newVariantDomain("libc", Exclusive)
    domain2b.flags.incl("musl")
    profile2.addDomain(domain2b)
    
    let demands = @[
      VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false),
      VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false)
    ]
    
    let conflict = detectVariantConflict("nginx", demands)
    
    check conflict.isSome
    check conflict.get().kind == conflict_detection.VariantConflict
  
  # --- Circular Dependency Tests ---
  
  test "Detect circular dependency - simple cycle":
    ## Test detecting a simple circular dependency
    ## Requirements: 7.3
    
    var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
    graph["nginx"] = @["zlib"]
    graph["zlib"] = @["nginx"]
    
    let conflict = detectCircularDependency(graph, "nginx")
    
    check conflict.isSome
    check conflict.get().kind == CircularDependency
    check conflict.get().cyclePath.isSome
    check conflict.get().cyclePath.get().len >= 2
    check conflict.get().suggestions.len > 0
  
  test "Detect circular dependency - three-way cycle":
    ## Test detecting a three-way circular dependency
    ## Requirements: 7.3
    
    var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
    graph["nginx"] = @["zlib"]
    graph["zlib"] = @["pcre"]
    graph["pcre"] = @["nginx"]
    
    let conflict = detectCircularDependency(graph, "nginx")
    
    check conflict.isSome
    check conflict.get().kind == CircularDependency
    check conflict.get().cyclePath.isSome
    let cycle = conflict.get().cyclePath.get()
    check cycle.len >= 3
    check cycle.contains("nginx")
    check cycle.contains("zlib")
    check cycle.contains("pcre")
  
  test "No circular dependency - linear chain":
    ## Test that linear dependency chains don't trigger circular conflict
    ## Requirements: 7.3
    
    var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
    graph["nginx"] = @["zlib"]
    graph["zlib"] = @["pcre"]
    graph["pcre"] = @[]
    
    let conflict = detectCircularDependency(graph, "nginx")
    
    check conflict.isNone
  
  test "No circular dependency - diamond dependency":
    ## Test that diamond dependencies don't trigger circular conflict
    ## Requirements: 7.3
    
    var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
    graph["nginx"] = @["zlib", "pcre"]
    graph["zlib"] = @["openssl"]
    graph["pcre"] = @["openssl"]
    graph["openssl"] = @[]
    
    let conflict = detectCircularDependency(graph, "nginx")
    
    check conflict.isNone
  
  test "Detect circular dependency - self-loop":
    ## Test detecting a package that depends on itself
    ## Requirements: 7.3
    
    var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
    graph["nginx"] = @["nginx"]
    
    let conflict = detectCircularDependency(graph, "nginx")
    
    check conflict.isSome
    check conflict.get().kind == CircularDependency
  
  # --- Missing Package Tests ---
  
  test "Detect missing package":
    ## Test detecting a missing package
    ## Requirements: 7.4
    
    let available = toHashSet(["nginx", "zlib", "pcre"])
    
    let conflict = detectMissingPackage("openssl", available)
    
    check conflict.isSome
    check conflict.get().kind == MissingPackage
    check conflict.get().packages.contains("openssl")
    check conflict.get().details.contains("openssl")
    check conflict.get().suggestions.len > 0
  
  test "No missing package - package exists":
    ## Test that existing packages don't trigger missing conflict
    ## Requirements: 7.4
    
    let available = toHashSet(["nginx", "zlib", "pcre", "openssl"])
    
    let conflict = detectMissingPackage("openssl", available)
    
    check conflict.isNone
  
  test "Detect missing package - suggest similar names":
    ## Test that similar package names are suggested
    ## Requirements: 7.4
    
    let available = toHashSet(["nginx", "nginx-ssl", "nginx-http2"])
    
    let conflict = detectMissingPackage("nginx-http3", available)
    
    check conflict.isSome
    check conflict.get().kind == MissingPackage
    # Should suggest similar names
    let suggestions = conflict.get().suggestions.join(" ")
    check suggestions.contains("nginx") or suggestions.contains("Did you mean")
  
  test "Detect missing package - empty repository":
    ## Test detecting missing package in empty repository
    ## Requirements: 7.4
    
    let available: HashSet[string] = initHashSet[string]()
    
    let conflict = detectMissingPackage("nginx", available)
    
    check conflict.isSome
    check conflict.get().kind == MissingPackage
  
  # --- Build Hash Mismatch Tests ---
  
  test "Detect build hash mismatch":
    ## Test detecting build hash mismatch
    ## Requirements: 7.5
    
    let conflict = detectBuildHashMismatch(
      "nginx",
      "blake3-abc123def456",
      "blake3-xyz789abc123"
    )
    
    check conflict.isSome
    check conflict.get().kind == BuildHashMismatch
    check conflict.get().packages.contains("nginx")
    check conflict.get().details.contains("abc123def456")
    check conflict.get().details.contains("xyz789abc123")
    check conflict.get().suggestions.len > 0
  
  test "No build hash mismatch - hashes match":
    ## Test that matching hashes don't trigger mismatch conflict
    ## Requirements: 7.5
    
    let conflict = detectBuildHashMismatch(
      "nginx",
      "blake3-abc123def456",
      "blake3-abc123def456"
    )
    
    check conflict.isNone
  
  # --- Conflict Formatting Tests ---
  
  test "Format version conflict":
    ## Test formatting a version conflict report
    ## Requirements: 7.1, 7.5
    
    let report = ConflictReport(
      kind: VersionConflict,
      packages: @["nginx"],
      details: "nginx requires version 1.0.0 and 2.0.0",
      suggestions: @["Update to compatible version"],
      conflictingTerms: @[],
      cyclePath: none(seq[string])
    )
    
    let formatted = formatConflict(report)
    
    check formatted.contains("VersionConflict")
    check formatted.contains("nginx")
    check formatted.contains("Update to compatible version")
  
  test "Format variant conflict":
    ## Test formatting a variant conflict report
    ## Requirements: 7.2, 7.5
    
    let report = ConflictReport(
      kind: VariantConflict,
      packages: @["nginx"],
      details: "nginx requires +systemd and +dinit",
      suggestions: @["Choose one variant"],
      conflictingTerms: @[],
      cyclePath: none(seq[string])
    )
    
    let formatted = formatConflict(report)
    
    check formatted.contains("VariantConflict")
    check formatted.contains("nginx")
    check formatted.contains("Choose one variant")
  
  test "Format circular dependency":
    ## Test formatting a circular dependency report
    ## Requirements: 7.3, 7.5
    
    let report = ConflictReport(
      kind: CircularDependency,
      packages: @["nginx", "zlib", "nginx"],
      details: "nginx -> zlib -> nginx",
      suggestions: @["Break the cycle"],
      conflictingTerms: @[],
      cyclePath: some(@["nginx", "zlib", "nginx"])
    )
    
    let formatted = formatConflict(report)
    
    check formatted.contains("CircularDependency")
    check formatted.contains("nginx")
    check formatted.contains("Break the cycle")
  
  test "Format missing package":
    ## Test formatting a missing package report
    ## Requirements: 7.4, 7.5
    
    let report = ConflictReport(
      kind: MissingPackage,
      packages: @["openssl"],
      details: "openssl not found",
      suggestions: @["Check package name", "Update repositories"],
      conflictingTerms: @[],
      cyclePath: none(seq[string])
    )
    
    let formatted = formatConflict(report)
    
    check formatted.contains("MissingPackage")
    check formatted.contains("openssl")
    check formatted.contains("Check package name")
  
  test "Format build hash mismatch":
    ## Test formatting a build hash mismatch report
    ## Requirements: 7.5
    
    let report = ConflictReport(
      kind: BuildHashMismatch,
      packages: @["nginx"],
      details: "Hash mismatch for nginx",
      suggestions: @["Reinstall package"],
      conflictingTerms: @[],
      cyclePath: none(seq[string])
    )
    
    let formatted = formatConflict(report)
    
    check formatted.contains("BuildHashMismatch")
    check formatted.contains("nginx")
    check formatted.contains("Reinstall package")
  
  # --- Conflict Analysis Tests ---
  
  test "Analyze conflict origins - version conflict":
    ## Test analyzing origins of a version conflict
    ## Requirements: 7.5
    
    var manifests: Table[string, seq[VariantDemand]] = initTable[string, seq[VariantDemand]]()
    manifests["nginx"] = @[
      VariantDemand(packageName: "nginx", variantProfile: newVariantProfile(), optional: false),
      VariantDemand(packageName: "nginx", variantProfile: newVariantProfile(), optional: false)
    ]
    
    let report = ConflictReport(
      kind: VersionConflict,
      packages: @["nginx"],
      details: "Version conflict",
      suggestions: @[],
      conflictingTerms: @[],
      cyclePath: none(seq[string])
    )
    
    let analysis = analyzeConflictOrigins(report, manifests)
    
    check analysis.len > 0
    check analysis[0].contains("nginx")
    check analysis[0].contains("2")
  
  test "Analyze conflict origins - circular dependency":
    ## Test analyzing origins of a circular dependency
    ## Requirements: 7.5
    
    let report = ConflictReport(
      kind: CircularDependency,
      packages: @["nginx", "zlib", "nginx"],
      details: "Circular dependency",
      suggestions: @[],
      conflictingTerms: @[],
      cyclePath: some(@["nginx", "zlib", "nginx"])
    )
    
    let analysis = analyzeConflictOrigins(report, initTable[string, seq[VariantDemand]]())
    
    check analysis.len > 0
    check analysis[0].contains("3")
  
  # --- Minimal Conflict Extraction Tests ---
  
  test "Extract minimal conflict":
    ## Test extracting minimal conflicting incompatibilities
    ## Requirements: 7.5
    
    let incomp1 = Incompatibility(
      terms: @[],
      cause: Root,
      externalContext: "Incomp 1",
      fromPackage: none(string),
      fromVersion: none(SemanticVersion)
    )
    
    let incomp2 = Incompatibility(
      terms: @[],
      cause: Dependency,
      externalContext: "Incomp 2",
      fromPackage: none(string),
      fromVersion: none(SemanticVersion)
    )
    
    let minimal = extractMinimalConflict(@[incomp1, incomp2])
    
    check minimal.isSome
    check minimal.get().len == 2


  test "Extract minimal conflict - empty list":
    ## Test extracting minimal conflict from empty list
    ## Requirements: 7.5
    
    let minimal = extractMinimalConflict(@[])
    
    check minimal.isNone
  
  test "Extract minimal conflict - single incompatibility":
    ## Test extracting minimal conflict from single incompatibility
    ## Requirements: 7.5
    
    let incomp = Incompatibility(
      terms: @[],
      cause: Root,
      externalContext: "Single incompatibility",
      fromPackage: none(string),
      fromVersion: none(SemanticVersion)
    )
    
    let minimal = extractMinimalConflict(@[incomp])
    
    check minimal.isSome
    check minimal.get().len == 1
  
  test "Extract minimal conflict - multiple incompatibilities":
    ## Test extracting minimal conflict from multiple incompatibilities
    ## Requirements: 7.5
    
    let incomp1 = Incompatibility(
      terms: @[],
      cause: Root,
      externalContext: "Root requirement",
      fromPackage: some("nginx"),
      fromVersion: none(SemanticVersion)
    )
    
    let incomp2 = Incompatibility(
      terms: @[],
      cause: Dependency,
      externalContext: "Dependency conflict",
      fromPackage: some("nginx"),
      fromVersion: none(SemanticVersion)
    )
    
    let incomp3 = Incompatibility(
      terms: @[],
      cause: VariantConflict,
      externalContext: "Variant conflict",
      fromPackage: some("openssl"),
      fromVersion: none(SemanticVersion)
    )
    
    let minimal = extractMinimalConflict(@[incomp1, incomp2, incomp3])
    
    check minimal.isSome
    # The minimal set should have at least 1 incompatibility
    check minimal.get().len >= 1
    # The minimal set should have at most all incompatibilities
    check minimal.get().len <= 3
  
  test "Extract minimal conflict - preserves root incompatibilities":
    ## Test that root incompatibilities are preserved in minimal set
    ## Requirements: 7.5
    
    let rootIncompatibility = Incompatibility(
      terms: @[],
      cause: Root,
      externalContext: "User requirement",
      fromPackage: some("nginx"),
      fromVersion: none(SemanticVersion)
    )
    
    let dependencyIncompatibility = Incompatibility(
      terms: @[],
      cause: Dependency,
      externalContext: "Dependency",
      fromPackage: some("zlib"),
      fromVersion: none(SemanticVersion)
    )
    
    let minimal = extractMinimalConflict(@[rootIncompatibility, dependencyIncompatibility])
    
    check minimal.isSome
    # Root incompatibilities should be preserved
    let hasRoot = minimal.get().anyIt(it.cause == Root)
    check hasRoot
  
  test "Extract minimal conflict - handles variant conflicts":
    ## Test that variant conflicts are handled correctly
    ## Requirements: 7.5
    
    let variantConflict1 = Incompatibility(
      terms: @[],
      cause: IncompatibilityCause.VariantConflict,
      externalContext: "Variant conflict 1",
      fromPackage: some("nginx"),
      fromVersion: none(SemanticVersion)
    )
    
    let variantConflict2 = Incompatibility(
      terms: @[],
      cause: IncompatibilityCause.VariantConflict,
      externalContext: "Variant conflict 2",
      fromPackage: some("nginx"),
      fromVersion: none(SemanticVersion)
    )
    
    let minimal = extractMinimalConflict(@[variantConflict1, variantConflict2])
    
    check minimal.isSome
    # Should have at least one variant conflict
    let hasVariantConflict = minimal.get().anyIt(it.cause == IncompatibilityCause.VariantConflict)
    check hasVariantConflict
  
  test "Extract minimal conflict - deterministic results":
    ## Test that minimal conflict extraction is deterministic
    ## Requirements: 7.5
    
    let incomp1 = Incompatibility(
      terms: @[],
      cause: Root,
      externalContext: "Incomp 1",
      fromPackage: some("pkg1"),
      fromVersion: none(SemanticVersion)
    )
    
    let incomp2 = Incompatibility(
      terms: @[],
      cause: Dependency,
      externalContext: "Incomp 2",
      fromPackage: some("pkg2"),
      fromVersion: none(SemanticVersion)
    )
    
    let incomp3 = Incompatibility(
      terms: @[],
      cause: VariantConflict,
      externalContext: "Incomp 3",
      fromPackage: some("pkg3"),
      fromVersion: none(SemanticVersion)
    )
    
    let incompatibilities = @[incomp1, incomp2, incomp3]
    
    # Extract minimal conflict multiple times
    let minimal1 = extractMinimalConflict(incompatibilities)
    let minimal2 = extractMinimalConflict(incompatibilities)
    
    # Results should be the same
    check minimal1.isSome
    check minimal2.isSome
    check minimal1.get().len == minimal2.get().len

