package sborepo

import (
	"sort"
	"strings"
	"testing"
)

// --- helpers ---------------------------------------------------------------

func setRequires(t *testing.T, p *pkgNode, requires string) {
	t.Helper()
	createPkgTestfilesWithRequires(p.Package, t, requires)
}

// sortedEqual compares two string slices disregarding order.
func sortedEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	aa := append([]string(nil), a...)
	bb := append([]string(nil), b...)
	sort.Strings(aa)
	sort.Strings(bb)
	for i := range aa {
		if aa[i] != bb[i] {
			return false
		}
	}
	return true
}

func mockPkgNode(name string) *pkgNode { return &pkgNode{Package: &Package{Name: name}} }

func nodeRepoMap(nodes ...*pkgNode) RepoMap {
	rm := make(RepoMap, len(nodes))
	for _, node := range nodes {
		rm[node.Name] = node.Package.ParentPath
	}
	return rm
}

func testDependencyGraph(nodes ...*pkgNode) DependencyGraph {
	dg := DependencyGraph{
		graphMap: make(map[PkgName]*pkgNode, len(nodes)),
		root:     &pkgNode{},
	}
	for _, node := range nodes {
		dg.graphMap[node.Name] = node
	}
	return dg
}

// --- graph-building & traversal tests --------------------------------------

// 1) No circular dependency: parent -> {dependency1, dependency2}
func TestResolvDep_NoCircularDependency_Graph(t *testing.T) {
	dep1 := mockPkgNode("dependency1")
	dep2 := mockPkgNode("dependency2")
	parent := mockPkgNode("parent")

	// LEAVES: use empty requires, not "%README%"
	setRequires(t, dep1, "")
	setRequires(t, dep2, "")
	// Parent requires the two leaves (no %README% so dependencies() is non-empty)
	setRequires(t, parent, "dependency1 dependency2")

	rm := nodeRepoMap(dep1, dep2, parent)
	dg := testDependencyGraph(parent)

	parent.resolveDep(rm, dg)
	root := dg.root
	dep1 = dg.graphMap["dependency1"]
	dep2 = dg.graphMap["dependency2"]

	// Now leaves are attached to root
	if len(root.dependents) != 2 {
		t.Fatalf("expected root to have 2 dependents (leaves), got %d", len(root.dependents))
	}

	// Each leaf must have "parent" as a dependent
	if len(dep1.dependents) != 1 || dep1.dependents[0].Name != "parent" {
		t.Fatalf("dependency1 should have parent as dependent, got: %+v", dep1.dependents)
	}
	if len(dep2.dependents) != 1 || dep2.dependents[0].Name != "parent" {
		t.Fatalf("dependency2 should have parent as dependent, got: %+v", dep2.dependents)
	}

	if parent.ndep != 2 {
		t.Fatalf("expected parent to count 2 dependencies, got ndep=%d", parent.ndep)
	}
}

func TestResolvDep_ReadmeTokenIsNotDependency(t *testing.T) {
	leaf := mockPkgNode("leaf")
	parent := mockPkgNode("parent")

	setRequires(t, leaf, "")
	setRequires(t, parent, "%README% leaf")

	rm := nodeRepoMap(leaf, parent)
	dg := testDependencyGraph(parent)
	parent.resolveDep(rm, dg)

	if parent.ndep != 1 {
		t.Fatalf("expected only the real dependency to be counted, got ndep=%d", parent.ndep)
	}

	if len(dg.root.dependents) != 1 || dg.root.dependents[0].Name != "leaf" {
		t.Fatalf("expected leaf to be attached to root, got %+v", dg.root.dependents)
	}
}

func TestResolvDep_ReadmeOnlyPackageIsLeaf(t *testing.T) {
	pkg := mockPkgNode("pkg")
	setRequires(t, pkg, "%README%")

	rm := nodeRepoMap(pkg)
	dg := testDependencyGraph(pkg)
	pkg.resolveDep(rm, dg)

	if pkg.ndep != 1 {
		t.Fatalf("expected README-only package to be attached to root, got ndep=%d", pkg.ndep)
	}
	if len(dg.root.dependents) != 1 || dg.root.dependents[0] != pkg {
		t.Fatalf("expected README-only package under root, got %+v", dg.root.dependents)
	}
}

// 2) Circular dependency detection: parent requires itself.
func TestResolvDep_CircularDependency_Graph(t *testing.T) {
	parent := &pkgNode{Package: &Package{Name: "parent"}}
	setRequires(t, parent, "%README% parent") // self-dependency
	rm := nodeRepoMap(parent)
	dg := testDependencyGraph(parent)

	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic due to circular dependency, but none occurred")
		} else if msg, ok := r.(string); ok {
			if !strings.Contains(msg, "Circular dependency detected: parent") {
				t.Fatalf("unexpected panic message: %q", msg)
			}
		}
	}()

	parent.resolveDep(rm, dg)
	t.Fatalf("should have panicked before this line")
}

func TestResolveDep_MissingDependencyPanics(t *testing.T) {
	parent := mockPkgNode("parent")
	setRequires(t, parent, "missing")
	rm := nodeRepoMap(parent)
	dg := testDependencyGraph(parent)

	defer func() {
		r := recover()
		if r == nil {
			t.Fatal("expected panic for missing dependency")
		}
		msg, ok := r.(string)
		if !ok || !strings.Contains(msg, "Package missing is a dependency of parent") {
			t.Fatalf("unexpected panic: %v", r)
		}
	}()

	parent.resolveDep(rm, dg)
}

func TestProcessDependents(t *testing.T) {
	a := &pkgNode{Package: &Package{Name: "A"}, InGraph: true}
	b := &pkgNode{Package: &Package{Name: "B"}, InGraph: true}
	c := &pkgNode{Package: &Package{Name: "C"}, InGraph: true}
	d := &pkgNode{Package: &Package{Name: "D"}, InGraph: true}

	a.AddDependent(b)
	a.AddDependent(c)
	b.AddDependent(d)
	c.AddDependent(d)

	var got []string
	a.ProcessDependents(func(dep *pkgNode) { got = append(got, dep.Name) })
	if !sortedEqual(got, []string{"A", "B", "C", "D"}) {
		t.Fatalf("expected A and its dependents once; got %v", got)
	}

	got = nil
	(&pkgNode{Package: &Package{Name: "out-of-graph"}}).ProcessDependents(
		func(dep *pkgNode) { got = append(got, dep.Name) },
	)
	if len(got) != 0 {
		t.Fatalf("expected no dependents for out-of-graph node, got %v", got)
	}
}
