package sborepo

import (
	"path/filepath"
	"sync"
	"testing"
	"time"

	"gitlab.com/M0M097/pkg/internal/must"
)

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

	var (
		mu    sync.Mutex
		order []string
	)
	DependencyGraph{root: root}.ProcessTopological(func(pkg *Package) {
		mu.Lock()
		order = append(order, pkg.Name)
		mu.Unlock()
	})

	if !sortedEqual(order, []string{"A", "B", "C"}) || order[2] != "C" {
		t.Fatalf("expected A and B before C, got %v", order)
	}
}

func TestDependencyGraphProcessUnorderedAndIsEmpty(t *testing.T) {
	a := &pkgNode{Package: &Package{Name: "A"}, InGraph: true}
	b := &pkgNode{Package: &Package{Name: "B"}}
	dg := DependencyGraph{graphMap: map[PkgName]*pkgNode{"A": a, "B": b}}

	var got []string
	dg.ProcessUnordered(func(pkg *Package) { got = append(got, pkg.Name) })
	if !sortedEqual(got, []string{"A"}) {
		t.Fatalf("expected only in-graph packages, got %v", got)
	}
	if dg.IsEmpty() {
		t.Fatal("expected graph with A in-graph to be non-empty")
	}
	dg.Remove("A")
	if a.InGraph {
		t.Fatal("expected A to be removed from the graph")
	}

	a.InGraph = false
	if !dg.IsEmpty() {
		t.Fatal("expected graph with no in-graph packages to be empty")
	}
}

func TestDependencyGraphProcessUnorderedParallel(t *testing.T) {
	a := &pkgNode{Package: &Package{Name: "A"}, InGraph: true}
	b := &pkgNode{Package: &Package{Name: "B"}, InGraph: true}
	skip := &pkgNode{Package: &Package{Name: "skip"}}
	dg := DependencyGraph{graphMap: map[PkgName]*pkgNode{"A": a, "B": b, "skip": skip}}

	started := make(chan string, 2)
	release := make(chan struct{})
	done := make(chan []string, 1)

	go func() {
		var (
			mu  sync.Mutex
			got []string
		)
		Parallel(dg.ProcessUnordered)(func(pkg *Package) {
			started <- pkg.Name
			<-release
			mu.Lock()
			got = append(got, pkg.Name)
			mu.Unlock()
		})
		done <- got
	}()

	startedNames := make([]string, 0, 2)
	for len(startedNames) < 2 {
		select {
		case name := <-started:
			startedNames = append(startedNames, name)
		case <-time.After(time.Second):
			close(release)
			t.Fatalf("expected both callbacks to start before release, got %v", startedNames)
		}
	}
	close(release)

	select {
	case got := <-done:
		if !sortedEqual(got, []string{"A", "B"}) {
			t.Fatalf("parallel traversal visited %v", got)
		}
	case <-time.After(time.Second):
		t.Fatal("parallel traversal did not return")
	}
	if !sortedEqual(startedNames, []string{"A", "B"}) {
		t.Fatalf("parallel traversal started %v", startedNames)
	}
}

func TestDependencyGraphRemoveDependents(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)

	dg := DependencyGraph{
		graphMap: map[PkgName]*pkgNode{
			"A": a,
			"B": b,
			"C": c,
			"D": d,
		},
	}

	if removed := dg.RemoveDependents("A"); !sortedEqual(removed, []string{"A", "B", "C", "D"}) {
		t.Fatalf("expected A and its dependents to be removed once, got %v", removed)
	}
	for _, pkg := range []*pkgNode{a, b, c, d} {
		if pkg.InGraph {
			t.Fatalf("expected %s to be removed from the graph", pkg.Name)
		}
	}
}

func TestDependencyGraphIterDependents(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}
	skip := &pkgNode{Package: &Package{Name: "skip"}}

	a.AddDependent(b)
	b.AddDependent(c)
	a.AddDependent(skip)

	dg := DependencyGraph{
		graphMap: map[PkgName]*pkgNode{
			"A":    a,
			"B":    b,
			"C":    c,
			"skip": skip,
		},
	}

	var got []string
	dg.IterDependents("A")(func(pkg *Package) {
		got = append(got, pkg.Name)
	})
	if !sortedEqual(got, []string{"A", "B", "C"}) {
		t.Fatalf("unexpected dependents: %v", got)
	}
}

func TestDependencyGraphFilterOut(t *testing.T) {
	newPkg := func(name string) *pkgNode {
		t.Helper()
		pkg := PackageCreator{
			Name:              name,
			Path:              t.TempDir(),
			Version:           DUMMY_VERSION,
			SlackBuildContent: MOCK_PKG_SLACKBUILD_CONTENT,
		}.Create()
		return &pkgNode{Package: &pkg, InGraph: true}
	}
	var (
		p    = newPkg("name")
		next = newPkg("next")
		dg   = DependencyGraph{graphMap: map[PkgName]*pkgNode{
			p.Name:    p,
			next.Name: next,
		}}
	)

	dg.FilterOut(func(pkg *Package) bool {
		return pkg.IsUpToDate("/doc/dir", "kernel-1.0")
	})

	for _, pkg := range []*pkgNode{p, next} {
		if !pkg.InGraph {
			t.Fatalf("expected %s to remain in the graph", pkg.Name)
		}
	}

	docDir := t.TempDir()
	fullPath := filepath.Join(docDir, next.FullName())
	must.Mkdir(fullPath)
	must.Content2File(filepath.Join(fullPath, next.Name+".SlackBuild"), MOCK_PKG_SLACKBUILD_CONTENT)

	dg.FilterOut(func(pkg *Package) bool {
		return pkg.IsUpToDate(docDir, "kernel-1.0")
	})

	if next.InGraph {
		t.Fatal("expected next to be filtered out")
	}
	if !p.InGraph {
		t.Fatal("expected name to remain in the graph")
	}
}
