package sboRepo

import (
	"strings"
	"sync"
	"testing"
)

// --------------------------------------------------
// 1) Test for NO circular dependency
// --------------------------------------------------

func TestResolvDep_NoCircularDependency(t *testing.T) {
	// Suppose 'dependee1' does not actually require itself,
	// so that we won't get a circular dependency.
	dep1 := &Package{name: "dependee1"}
	dep2 := &Package{name: "dependee2"}

	// For dep1, let's require just %README%, no other pkg => no self-reference
	createPkgTestfilesWithRequires(dep1, t, "%README%")

	// For dep2, also no mention of dependee1 => no circular dependency
	createPkgTestfilesWithRequires(dep2, t, "%README%")

	// "parent" is the one from which we start resolving
	parent := &Package{name: "parent"}
	rm := RepoMap{
		"dependee1": dep1,
		"dependee2": dep2,
		"parent":    parent,
	}

	head := &Package{} // sentinel
	tail := head

	// We'll call resolvDep with pkgNames = ["dependee1", "dependee2"]
	tail = tail.resolvDep("parent", []string{"dependee1", "dependee2"}, rm)

	// The returned tail should be the last appended package: the chain is head -> dep1 -> dep2
	if head.next == nil || head.next.name != "dependee1" {
		t.Fatalf("Expected head->dependee1, got something else.")
	}
	if head.next.next == nil || head.next.next.name != "dependee2" {
		t.Fatalf("Expected dependee1->dependee2, got something else.")
	}

	// Check that we added parent as a dependee to each
	d1 := rm["dependee1"]
	d2 := rm["dependee2"]
	if len(d1.dependees) == 0 || d1.dependees[0].Name() != "parent" {
		t.Errorf("Expected dependee1 to have parent as dependee, found: %+v", d1.dependees)
	}
	if len(d2.dependees) == 0 || d2.dependees[0].Name() != "parent" {
		t.Errorf("Expected dependee2 to have parent as dependee, found: %+v", d2.dependees)
	}
}

// --------------------------------------------------
// 2) Test for CIRCULAR dependency
// --------------------------------------------------

func TestResolvDep_CircularDependency(t *testing.T) {
	// For circular dependency, let 'dependee1' require itself:
	dep1 := &Package{name: "dependee1"}
	dep2 := &Package{name: "dependee2"}

	// E.g., REQUIRES="%README% dependee1"
	// That means 'dependee1' is said to require 'dependee1' again => circular
	createPkgTestfilesWithRequires(dep1, t, "%README% dependee1")
	createPkgTestfilesWithRequires(dep2, t, "%README%")

	rm := RepoMap{
		"dependee1": dep1,
		"dependee2": dep2,
		"parent":    &Package{name: "parent"},
	}

	head := &Package{}
	tail := head

	// We expect a panic due to circular dependency
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("Expected panic due to circular dependency, but no panic occurred.")
		} else {
			// Optionally check the message
			msg, ok := r.(string)
			if ok {
				mustContain(t, msg, "Circular dependency", "dependee1")
			}
		}
	}()

	tail = tail.resolvDep("parent", []string{"dependee1", "dependee2"}, rm)
	t.Errorf("Should have panicked before reaching this line.")
}

func TestResolvDep_AlreadyInList(t *testing.T) {
	// 1) Create the "dependee1" package whose .info does NOT create a circular dependency.
	//    For simplicity, we only have: REQUIRES="%README%" (no mention of itself).
	dep1 := &Package{name: "dependee1"}
	createPkgTestfilesWithRequires(dep1, t, "%README%")

	// 2) Create a "parent" package in the RepoMap.
	parent := &Package{name: "parent"}
	rm := RepoMap{
		"dependee1": dep1,
		"parent":    parent,
	}

	// 3) Build an initial chain: head -> dep1 (so dep1 is already "in the list").
	head := &Package{} // sentinel
	tail := head
	tail = dep1.Append(tail) // Now head->dep1

	// 4) Now call resolvDep with "dependee1" again. This should trigger:
	//    if pkgPtr.IsInList() { continue }
	//    but also ensure pkgPtr.AddDependee(parent) still happens.
	tail = tail.resolvDep("parent", []string{"dependee1"}, rm)

	// 5) Verify "dependee1" has "parent" as a dependee
	if len(dep1.dependees) == 0 || dep1.dependees[0].Name() != "parent" {
		t.Errorf("Expected 'dependee1' to have 'parent' as a dependee, got %+v",
			dep1.dependees)
	}

	// 6) Verify the chain is still just head->dep1 (i.e., 'dependee1' was NOT appended again).
	if head.next != dep1 || dep1.next != nil {
		t.Errorf("Expected chain to remain [head->dependee1], but got head->%v->%v",
			head.next, dep1.next)
	}
}

func TestIterate(t *testing.T) {
	p := MockPkg()
	// MockPkg returns [prev -> name -> next], with p = "name"
	var names []string
	p.prev.Iterate(func(pkg *Package) {
		names = append(names, pkg.Name())
	})
	// We expect "name" then "next" in the iteration (since iteration starts at head.next)
	if len(names) != 2 || names[0] != "name" || names[1] != "next" {
		t.Errorf("Expected iteration order [name, next], got %v", names)
	}
}

func TestIterateConcurrently(t *testing.T) {
	var (
		mu    sync.Mutex
		names []string
		p     = MockPkg()
	)

	for i := 0; i < 100; i++ { // Run multiple iterations to test concurrency
		names = []string{} // Reset names for each iteration
		p.prev.IterateConcurrently(func(pkg *Package) {
			mu.Lock()
			names = append(names, pkg.Name())
			mu.Unlock()
		})
		got := strings.Join(names, " ")
		mustContain(t, got, "name", "next")
		mustContain(t, got, "next", "name")
	}
}

func TestIterateDependees(t *testing.T) {
	p := MockPkg()
	// In your code, p has dependees = [prev, next]
	var collected []string
	p.IterateDependees(func(d *Package) {
		collected = append(collected, d.Name())
	})
	// The default mock has two dependees: "prev" and "next".
	// There's no recursion in their dependees by default, but if you want,
	// you can add some nested dependees to test deeper recursion.
	want := []string{"prev", "next"}
	mustBeEqualStringSlices(t, collected, want)
}

func TestIsEmpty(t *testing.T) {
	head := &Package{}
	if !head.IsEmpty() {
		t.Errorf("Expected empty head to be IsEmpty=true, got false")
	}

	// Once we append a package, it should not be empty
	p := &Package{name: "pkg1"}
	p.Append(head)
	if head.IsEmpty() {
		t.Errorf("Expected head to not be empty after append, got IsEmpty=true")
	}

	// If we remove the package, it should be empty again
	p.Del()
	if !head.IsEmpty() {
		t.Errorf("Expected head to be empty after deletion, got IsEmpty=false")
	}
}

func TestNewListHead(t *testing.T) {
	head := NewListHead()
	if head == nil || head.name != "" || head.prev != nil || head.next != nil {
		t.Errorf("NewListHead() did not create an empty head: %+v", head)
	}
}
