// file: repo_map_test.go
package sborepo

import (
	"path/filepath"
	"sort"
	"testing"

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

// This test showcases how to create a real directory structure in a temp dir.
func TestNewRepoMap(t *testing.T) {
	tmpDir := t.TempDir()

	/* Let's create the following structure:
	   tmpDir/
	      pkgdir1/ (dir)
	          pkgA (file)
	          pkgB (file)
	      pkgdir2/ (dir)
	          pkgC (file)
	      notADir (file) */

	pkgdir1 := filepath.Join(tmpDir, "pkgdir1")
	pkgdir2 := filepath.Join(tmpDir, "pkgdir2")

	must.Mkdir(pkgdir1)
	must.Mkdir(pkgdir2)

	// Create empty files
	must.Create(filepath.Join(pkgdir1, "pkgA"))
	must.Create(filepath.Join(pkgdir1, "pkgB"))
	must.Create(filepath.Join(pkgdir2, "pkgC"))

	// A file in tmpDir that should be ignored, because !IsDir()
	must.Create(filepath.Join(tmpDir, "notADir"))

	// Now invoke NewRepoMap on tmpDir
	repo := NewRepoMap(tmpDir)

	if len(repo) != 3 {
		t.Fatalf("expected 3 packages in repo, got %d", len(repo))
	}

	cases := []struct {
		pkgName    string
		wantParent string
		wantDir    string
	}{
		{"pkgA", pkgdir1, filepath.Join(pkgdir1, "pkgA")},
		{"pkgB", pkgdir1, filepath.Join(pkgdir1, "pkgB")},
		{"pkgC", pkgdir2, filepath.Join(pkgdir2, "pkgC")},
	}
	for _, c := range cases {
		pkg, ok := repo.GetPkg(c.pkgName)
		if !ok {
			t.Fatalf("expected package %q to be in repo", c.pkgName)
		}
		if pkg.Path() != c.wantDir {
			t.Errorf("expected path %q for pkg %q, got %q", c.wantDir, c.pkgName, pkg.Path())
		}
		if pkg.ParentPath != c.wantParent {
			t.Errorf("expected parent path %q for pkg %q, got %q", c.wantParent, c.pkgName, pkg.ParentPath)
		}
	}
}

func TestRepoMapGetPkg(t *testing.T) {
	rm := dummyRepoMap()

	got, ok := rm.GetPkg("foo")
	if !ok {
		t.Fatal("expected foo to be found")
	}
	if got.Name != "foo" {
		t.Errorf("expected 'foo', got '%s'", got.Name)
	}

	got, ok = rm.GetPkg("foobar")
	if ok {
		t.Fatal("expected foobar not to be found")
	}
	if got.Name != "foobar" || got.ParentPath != "" {
		t.Errorf("unexpected missing package value: %+v", got)
	}
}

func TestRepoMap_DependencyGraph(t *testing.T) {
	// p1 and p2 are true leaves: empty REQUIRES (not %README%).
	p1, p2 := mockPkgNode("p1"), mockPkgNode("p2")
	createPkgTestfilesWithRequires(p1.Package, t, "")
	createPkgTestfilesWithRequires(p2.Package, t, "")

	rm := nodeRepoMap(p1, p2)

	// Build the graph root with dependency leaves attached below it.
	dg, err := rm.DependencyGraph([]string{"p1", "p2"})
	if err != nil {
		t.Fatal(err)
	}
	root, graphMap := dg.root, dg.graphMap

	if root == nil {
		t.Fatalf("expected non-nil root")
	}
	if len(graphMap) != 2 || graphMap["p1"] == nil || graphMap["p2"] == nil {
		t.Fatalf("expected graph map to contain p1 and p2; got %v", graphMap)
	}
	if got := len(root.dependents); got != 2 {
		t.Fatalf("expected 2 dependents under root, got %d", got)
	}

	// Names present (order not guaranteed because resolveDep may append in any order).
	names := []string{root.dependents[0].Name, root.dependents[1].Name}
	sort.Strings(names)
	if names[0] != "p1" || names[1] != "p2" {
		t.Fatalf("expected dependents p1,p2; got %v", names)
	}

}

func TestRepoMap_NamedPackageGraph(t *testing.T) {
	// This one just appends the provided packages as root.dependents (no dependency resolution).
	p1, p2 := mockPkgNode("p1"), mockPkgNode("p2")

	rm := nodeRepoMap(p1, p2)

	dg, err := rm.NamedPackageGraph([]string{"p1", "p2"})
	if err != nil {
		t.Fatal(err)
	}
	root, graphMap := dg.root, dg.graphMap
	if root == nil {
		t.Fatalf("expected non-nil root")
	}
	if len(graphMap) != 2 || graphMap["p1"] == nil || graphMap["p2"] == nil {
		t.Fatalf("expected graph map to contain p1 and p2; got %v", graphMap)
	}
	if got := len(root.dependents); got != 2 {
		t.Fatalf("expected 2 dependents under root, got %d", got)
	}
	// Order **is** defined here: AddDependent appends in the given order.
	if root.dependents[0].Name != "p1" || root.dependents[1].Name != "p2" {
		t.Fatalf("expected root dependents [p1, p2]; got [%s, %s]",
			root.dependents[0].Name, root.dependents[1].Name)
	}

	for _, pkg := range root.dependents {
		if pkg.ndep != 1 || !pkg.InGraph {
			t.Fatalf("expected %s to be in-graph with one dependency count, got InGraph=%t ndep=%d",
				pkg.Name, pkg.InGraph, pkg.ndep)
		}
	}
}

func TestIterOver(t *testing.T) {
	res := make([]string, 0)
	rm := dummyRepoMap()
	iter, err := rm.IterOver([]string{"foo", "bar", "baz"})
	if err != nil {
		t.Fatal(err)
	}
	iter(func(pkg *Package) {
		res = append(res, pkg.Name)
	})
	if len(res) != 3 {
		t.Fatalf("expected 3 items, got %d", len(res))
	}
	if res[0] != "foo" || res[1] != "bar" || res[2] != "baz" {
		t.Errorf("expected foo, bar, baz, got %v", res)
	}

	res = res[:0]
	iter, err = rm.IterOver([]string{"foo", "missing", "bar"})
	if err == nil {
		t.Fatal("expected an error for missing package")
	}
	iter(func(pkg *Package) {
		res = append(res, pkg.Name)
	})
	if len(res) != 2 || res[0] != "foo" || res[1] != "bar" {
		t.Errorf("expected found packages foo, bar, got %v", res)
	}
}

func TestRepoMapSearch(t *testing.T) {
	var got []string
	iter, err := dummyRepoMap().Search("ba.")
	if err != nil {
		t.Fatal(err)
	}
	iter(func(pkg *Package) {
		got = append(got, pkg.Name)
	})

	if !sortedEqual(got, []string{"bar", "baz"}) {
		t.Fatalf("expected bar and baz, got %v", got)
	}

	const invalid_regex = "["
	_, err = dummyRepoMap().Search(invalid_regex)
	if err == nil {
		t.Fatalf("expected error for invalid regex %q", invalid_regex)
	}
}

func TestRepoMapInstalled(t *testing.T) {
	docDir := t.TempDir()
	must.Mkdir(filepath.Join(docDir, "foo-1.0"))
	must.Mkdir(filepath.Join(docDir, "baz-2.0"))
	must.Mkdir(filepath.Join(docDir, "not-in-repo-3.0"))

	var got []string
	dummyRepoMap().Installed(docDir)(func(pkg *Package) {
		got = append(got, pkg.Name)
	})

	if !sortedEqual(got, []string{"baz", "foo"}) {
		t.Fatalf("expected installed repo packages, got %v", got)
	}
}

func dummyRepoMap() RepoMap {
	return RepoMap{
		"foo": mockPkgNode("foo").Package.ParentPath,
		"bar": mockPkgNode("bar").Package.ParentPath,
		"baz": mockPkgNode("baz").Package.ParentPath,
	}
}
