package pkg

import (
	"bufio"
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"

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

func TestCliUIChoosePkgs(t *testing.T) {
	var (
		head, _ = makeDummyList(t, "name", "next")
		buf     bytes.Buffer
		writer  = bufio.NewWriter(&buf)
		ui      = CliUI{
			stdout: writer,
			stdin:  strings.NewReader("1\n"), // user input: exclude one package in the graph listing
		}
	)

	head.Remove(sborepo.Names(ui.ChoosePkgs(head.ProcessUnordered))...)
	writer.Flush()

	if got := graphPackageNames(head); len(got) != 1 {
		t.Errorf("Expected one package to remain in the graph, got %v", got)
	}
}

func TestCliUIChoosePkgsInvalidInput(t *testing.T) {
	var (
		head, _ = makeDummyList(t, DUMMY_PKG3, "next")
		output  bytes.Buffer
		writer  = bufio.NewWriter(&output)
		ui      = CliUI{
			stdout: writer,
			stdin:  strings.NewReader("99\n\n"), // invalid input, then no exclusions
		}
	)

	ui.ChoosePkgs(head.ProcessUnordered)
	writer.Flush()

	outStr := output.String()
	if !strings.Contains(outStr, "Invalid input: `99` Repeat...") {
		t.Errorf("Expected 'Invalid input. Repeat...' in output, got:\n%s", outStr)
	}
}

func TestUpdateBlacklist(t *testing.T) {
	// We'll test that UpdateBlacklist copies otherBlacklist -> blacklist
	// and then appends the packages from the chain to the end.

	var ( // Prepare a chain: [p1, p2]
		head, _ = makeDummyList(t, "p1", "p2") // root -> p1, p2
	)

	// Create two temp files: one for 'otherBlacklist' (source), one for 'blacklist' (destination)
	tdir := t.TempDir()
	blacklistPath := filepath.Join(tdir, "blacklist.txt")
	otherPath := filepath.Join(tdir, "other.txt")
	defaultFile := "someDefaultFile" // not strictly used in success path, but part of error text

	// Write some lines to other.txt
	if err := os.WriteFile(otherPath, []byte("alreadyThere1\nalreadyThere2\n"), 0o644); err != nil {
		t.Fatalf("failed to write other.txt: %v", err)
	}

	// Now call UpdateBlacklist
	if err := updateBlacklist(head, blacklistPath, otherPath, defaultFile); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Check that blacklist.txt now contains original lines plus p1, p2
	content, err := os.ReadFile(blacklistPath)
	if err != nil {
		t.Fatalf("failed to read blacklist.txt: %v", err)
	}
	lines := strings.Split(strings.TrimSpace(string(content)), "\n")
	mustBeEqualStringSlices(t, lines[:2], []string{"alreadyThere1", "alreadyThere2"})
	gotPkgs := append([]string(nil), lines[2:]...)
	sort.Strings(gotPkgs)
	mustBeEqualStringSlices(t, gotPkgs, []string{"p1", "p2"})
}

func TestUpdateBlacklist_MissingOtherFile(t *testing.T) {
	// If otherBlacklist doesn’t exist, we expect an error with certain text
	head, _ := makeDummyList(t)
	tdir := t.TempDir()
	blacklistPath := filepath.Join(tdir, "blacklist.txt")
	otherPath := filepath.Join(tdir, "doesnotexist.txt")

	err := updateBlacklist(head, blacklistPath, otherPath, "defaultFile")
	if err == nil {
		t.Fatalf("expected an error for missing otherBlacklist, got nil")
	}
	if !strings.Contains(err.Error(), "no such file or directory") {
		t.Errorf("expected 'no such file or directory' in error, got %v", err)
	}
}

func TestUpdateBlacklist_PermissionDenied(t *testing.T) {
	// 1) Create a root temp directory.
	rootDir := t.TempDir()

	// 2) Create a *subdirectory* in which we *can* write files
	//    for the "otherBlacklist" file.
	otherDir := filepath.Join(rootDir, "other_dir")
	if err := os.Mkdir(otherDir, 0o755); err != nil {
		t.Fatalf("failed to create otherDir: %v", err)
	}

	// 3) Create another subdirectory for the "blacklist" file,
	//    but remove write permissions from it so `os.Create` fails inside it.
	noWriteDir := filepath.Join(rootDir, "no_write_dir")
	if err := os.Mkdir(noWriteDir, 0o755); err != nil {
		t.Fatalf("failed to create noWriteDir: %v", err)
	}

	// 4) Create the other file *before* removing write permission from noWriteDir
	otherFilePath := filepath.Join(otherDir, "other.txt")
	if err := os.WriteFile(otherFilePath, []byte("existing line\n"), 0o644); err != nil {
		t.Fatalf("failed to create other.txt: %v", err)
	}

	// 5) Now remove write permissions from noWriteDir.
	if err := os.Chmod(noWriteDir, 0o500); err != nil {
		t.Fatalf("failed to chmod the noWriteDir: %v", err)
	}

	// 6) Our “blacklist” path will be inside noWriteDir, which is not writable.
	blacklistPath := filepath.Join(noWriteDir, "blacklist.txt")

	// 7) Prepare an empty graph root
	head, _ := makeDummyList(t)

	// 8) Call UpdateBlacklist - expect "permission denied" when trying to create blacklist.txt
	err := updateBlacklist(head, blacklistPath, otherFilePath, "defaultFile")
	if err == nil {
		t.Fatalf("expected an error due to insufficient permissions, got nil")
	}

	// 9) Optionally check the error text
	t.Logf("Got error: %v", err)
	if !strings.Contains(strings.ToLower(err.Error()), "permission") &&
		!strings.Contains(strings.ToLower(err.Error()), "denied") {
		t.Errorf("expected error mentioning 'permission denied', got: %v", err)
	}
}

func TestFilesToDownload(t *testing.T) {
	var ( // Let’s create a chain [p1, p2]
		head, _ = makeDummyList(t, "p1", "p2") // root -> p1, p2
	)

	urls := filesToDownload(head, t.TempDir(), true)
	// Each package has 1 DownloadNames => total 2 URLs to download
	if len(urls) != 2 {
		t.Fatalf("expected 4 URLs, got %d", len(urls))
	}
	// Each URL is DUMMY_PKG3_DOWNLOAD by default from our stub
	for i, u := range urls {
		if u != DUMMY_PKG3_DOWNLOAD {
			t.Errorf("expected '"+DUMMY_PKG3_DOWNLOAD+"', got %q at index %d", u, i)
		}
	}
}

func TestFilesToDownloadUsesProvidedTmpDir(t *testing.T) {
	const (
		fileContent  = "already downloaded"
		downloadName = "unique-source-for-custom-tmp.tar.gz"
	)
	var (
		correctMD5 = md5Hex(fileContent)
		pkgInfo    = sborepo.PackageCreator{
			Name:     DUMMY_PKG3,
			Path:     filepath.Join(t.TempDir(), "test-category"),
			Version:  DUMMY_VERSION,
			Download: downloadName,
			MD5Sum:   correctMD5,
		}.Create()
		head = mustDependencyGraph(t, sborepo.NewRepoMap(filepath.Dir(filepath.Dir(pkgInfo.Path()))), DUMMY_PKG3)
	)
	tmpDir := t.TempDir()
	must.Content2File(filepath.Join(tmpDir, downloadName), fileContent)

	urls := filesToDownload(head, tmpDir, true)

	if len(urls) != 0 {
		t.Fatalf("expected no URLs when source exists in provided tmpDir, got %v", urls)
	}
}

func TestFilesToDownloadSkipsMD5WhenDisabled(t *testing.T) {
	const (
		fileContent  = "already downloaded with different content"
		downloadName = "existing-source-with-wrong-md5.tar.gz"
		wrongMD5     = "00000000000000000000000000000000"
	)
	var (
		pkgInfo = sborepo.PackageCreator{
			Name:     DUMMY_PKG3,
			Path:     filepath.Join(t.TempDir(), "test-category"),
			Version:  DUMMY_VERSION,
			Download: downloadName,
			MD5Sum:   wrongMD5,
		}.Create()
		head = mustDependencyGraph(t, sborepo.NewRepoMap(filepath.Dir(filepath.Dir(pkgInfo.Path()))), DUMMY_PKG3)
	)
	tmpDir := t.TempDir()
	must.Content2File(filepath.Join(tmpDir, downloadName), fileContent)

	urls := filesToDownload(head, tmpDir, false)

	if len(urls) != 0 {
		t.Fatalf("expected no URLs when MD5 checks are disabled and source exists, got %v", urls)
	}
}

func TestBuildCmdFail(t *testing.T) {
	pkg := &sborepo.Package{Name: DUMMY_PKG3}
	mustBeEqual(t, buildCmd(pkg, []string{}), BUILD_CMD+pkg.SlackBuild())
	mustBeEqual(t, buildCmd(pkg, []string{"option"}),
		ENV_CMD+"option "+BUILD_CMD+pkg.SlackBuild())
}

func TestBuildPullCmd(t *testing.T) {
	const template = "git clone --branch {VERSION} -- {MIRROR} /repo"

	t.Run("Stable", func(t *testing.T) {
		versionFile := must.Content2File(filepath.Join(t.TempDir(), "slackware-version"), "Slackware 15.0\n")
		got := buildPullCmd(template, versionFile)
		mustBeEqual(t, got, "git clone --branch 15.0 -- "+MIRROR+" /repo")
	})

	t.Run("Current", func(t *testing.T) {
		versionFile := must.Content2File(filepath.Join(t.TempDir(), "slackware-version"), "Slackware 15.0+\n")
		got := buildPullCmd(template, versionFile)
		mustBeEqual(t, got, "git clone --branch current -- "+MIRROR_CURRENT+" /repo")
	})

	t.Run("SinglePlaceholder", func(t *testing.T) {
		versionFile := must.Content2File(filepath.Join(t.TempDir(), "slackware-version"), "Slackware 15.0\n")
		got := buildPullCmd("git clone --branch {VERSION} /repo", versionFile)
		mustBeEqual(t, got, "git clone --branch 15.0 /repo")
	})

	t.Run("NoPlaceholders", func(t *testing.T) {
		got := buildPullCmd("echo pull", "")
		mustBeEqual(t, got, "echo pull")
	})
}

func TestPreBuildCmdSuccess(t *testing.T) {
	pkg := &sborepo.Package{Name: DUMMY_PKG3}
	mockConfDir := t.TempDir()
	if _, err := preBuildCmd(pkg, mockConfDir); err == nil {
		t.Error("preBuildCmd() did not return an error")
	}

	// Create a prebuild script
	fname := filepath.Join(mockConfDir, pkg.Name+PREBUILD_EXT)
	must.Create(fname)
	if _, err := preBuildCmd(pkg, mockConfDir); err == nil {
		t.Error("preBuildCmd() returned nil error for a non-executable file")
	}
	must.Chmod(fname, 0o755) // Make it executable
	cmd, err := preBuildCmd(pkg, mockConfDir)
	if err != nil {
		t.Error("preBuildCmd() returned an error")
	}
	if cmd != fname+" "+pkg.Path() {
		t.Error("preBuildCmd() returned an incorrect value")
	}
}

func TestLogPkgCmdReturnsError(t *testing.T) {
	pkgInfo := sborepo.PackageCreator{
		Name:              DUMMY_PKG3,
		Path:              t.TempDir(),
		Version:           DUMMY_VERSION,
		SlackBuildContent: MOCK_PKG_SLACKBUILD_CONTENT,
		READMEContent:     MOCK_PKG_README_CONTENT,
		SlackDescContent:  MOCK_PKG_SLACK_DES_CONTENT,
	}.Create()
	dg := mustDependencyGraph(t, sborepo.NewRepoMap(filepath.Dir(filepath.Dir(pkgInfo.Path()))), DUMMY_PKG3)
	var progressMessages []string
	progress := func(pkgName, msg string) {
		progressMessages = append(progressMessages, pkgName+": "+msg)
	}

	err := logPkgCmd(&pkgInfo, dg, "false", "Build", progress)

	if err == nil {
		t.Fatal("expected logPkgCmd to return an error")
	}
	mustContain(t, err.Error(), "false failed")
	if len(progressMessages) != 2 {
		t.Fatalf("expected two progress messages, got %d: %v", len(progressMessages), progressMessages)
	}
	mustContain(t, progressMessages[0], DUMMY_PKG3, "build", filepath.Join(pkgInfo.Path(), "Build.log"))
	mustContain(t, progressMessages[1], DUMMY_PKG3, "Failed Build", filepath.Join(pkgInfo.Path(), "Build.log"))
}

func TestCheckDownloadsFileNotFound(t *testing.T) {
	const downloadName = "foo.tar.gz"
	var (
		head, graphMap, _ = mockDependents(t, DUMMY_PKG1, DUMMY_PKG2, DUMMY_PKG3)
		pkg               = graphMap[DUMMY_PKG1]
		progress          []string
	)
	pkgInfo := sborepo.PackageCreator{
		Name:     DUMMY_PKG1,
		Path:     filepath.Join(t.TempDir(), "test-category"),
		Download: downloadName,
	}.Create()
	*pkg = pkgInfo

	ok := checkDownloads(head, pkg.Path(), true, func(pkgName, msg string) {
		progress = append(progress, pkgName+": "+msg)
	})

	if ok {
		t.Fatal("expected checkDownloads to report a failure")
	}
	mustBeEqual(t, len(progress), 3)
	mustContain(t, progress[0], DUMMY_PKG1, filepath.Join(pkg.Path(), downloadName), "no such file or directory")
	mustContain(t, progress[1], "Skipped; depends on "+DUMMY_PKG1)
	mustContain(t, progress[2], "Skipped; depends on "+DUMMY_PKG1)
	mustNotBeInList(t, head, DUMMY_PKG1)
	mustNotBeInList(t, head, DUMMY_PKG2)
	mustNotBeInList(t, head, DUMMY_PKG3)
}

func TestCheckDownloadsFileNotFoundWhenMD5Disabled(t *testing.T) {
	const downloadName = "foo.tar.gz"
	var (
		head, graphMap, _ = mockDependents(t, DUMMY_PKG1, DUMMY_PKG2, DUMMY_PKG3)
		pkg               = graphMap[DUMMY_PKG1]
		progress          []string
	)
	pkgInfo := sborepo.PackageCreator{
		Name:     DUMMY_PKG1,
		Path:     filepath.Join(t.TempDir(), "test-category"),
		Download: downloadName,
	}.Create()
	*pkg = pkgInfo

	ok := checkDownloads(head, pkg.Path(), false, func(pkgName, msg string) {
		progress = append(progress, pkgName+": "+msg)
	})

	if ok {
		t.Fatal("expected checkDownloads to report a failure")
	}
	mustBeEqual(t, len(progress), 3)
	mustContain(t, progress[0], DUMMY_PKG1, filepath.Join(pkg.Path(), downloadName), "no such file or directory")
	mustContain(t, progress[1], "Skipped; depends on "+DUMMY_PKG1)
	mustContain(t, progress[2], "Skipped; depends on "+DUMMY_PKG1)
	mustNotBeInList(t, head, DUMMY_PKG1)
	mustNotBeInList(t, head, DUMMY_PKG2)
	mustNotBeInList(t, head, DUMMY_PKG3)
}

func TestCheckDownloadsSuccess(t *testing.T) {
	const (
		fileContent  = "Hello from the test file"
		downloadName = "foo.tar.gz"
	)
	var (
		correctMD5 = md5Hex(fileContent)
		pkgInfo    = sborepo.PackageCreator{
			Name:     DUMMY_PKG3,
			Path:     t.TempDir(),
			Version:  DUMMY_VERSION,
			Download: downloadName,
			MD5Sum:   correctMD5,
		}.Create()
		head = mustDependencyGraph(t, sborepo.NewRepoMap(filepath.Dir(filepath.Dir(pkgInfo.Path()))), DUMMY_PKG3)
	)
	must.Content2File(filepath.Join(pkgInfo.Path(), downloadName), fileContent)

	var progress []string
	ok := checkDownloads(head, pkgInfo.Path(), true, func(pkgName, msg string) {
		progress = append(progress, pkgName+": "+msg)
	})

	if !ok {
		t.Fatal("expected checkDownloads to succeed")
	}
	mustBeEqual(t, len(progress), 0)
}

func TestCheckDownloadsMD5Mismatch(t *testing.T) {
	const (
		fileContent  = "Hello from mismatch land"
		downloadName = "foo.tar.gz"
		wrongMD5     = "00000000000000000000000000000000"
	)
	var (
		head, graphMap, _ = mockDependents(t, DUMMY_PKG1, DUMMY_PKG2, DUMMY_PKG3)
		pkg               = graphMap[DUMMY_PKG1]
		progress          []string
	)
	pkgInfo := sborepo.PackageCreator{
		Name:     DUMMY_PKG1,
		Path:     filepath.Join(t.TempDir(), "test-category"),
		Download: downloadName,
		MD5Sum:   wrongMD5,
	}.Create()
	*pkg = pkgInfo
	must.Content2File(filepath.Join(pkg.Path(), downloadName), fileContent)

	ok := checkDownloads(head, pkg.Path(), true, func(pkgName, msg string) {
		progress = append(progress, pkgName+": "+msg)
	})

	if ok {
		t.Fatal("expected checkDownloads to report a failure")
	}
	mustBeEqual(t, len(progress), 3)
	mustContain(t, progress[0], DUMMY_PKG1, downloadName, "MD5Sum Mismatch")
	mustContain(t, progress[1], "Skipped; depends on "+DUMMY_PKG1)
	mustContain(t, progress[2], "Skipped; depends on "+DUMMY_PKG1)
}

func TestCheckDownloadsSkipsMD5WhenDisabled(t *testing.T) {
	const (
		fileContent  = "Hello from mismatch land"
		downloadName = "foo.tar.gz"
		wrongMD5     = "00000000000000000000000000000000"
	)
	var (
		pkgInfo = sborepo.PackageCreator{
			Name:     DUMMY_PKG3,
			Path:     filepath.Join(t.TempDir(), "test-category"),
			Version:  DUMMY_VERSION,
			Download: downloadName,
			MD5Sum:   wrongMD5,
		}.Create()
		head = mustDependencyGraph(t, sborepo.NewRepoMap(filepath.Dir(filepath.Dir(pkgInfo.Path()))), DUMMY_PKG3)
	)
	must.Content2File(filepath.Join(pkgInfo.Path(), downloadName), fileContent)

	var progress []string
	ok := checkDownloads(head, pkgInfo.Path(), false, func(pkgName, msg string) {
		progress = append(progress, pkgName+": "+msg)
	})

	if !ok {
		t.Fatal("expected checkDownloads to succeed when MD5 checks are disabled")
	}
	mustBeEqual(t, len(progress), 0)
}

func TestPromptIfReadmeRequiredNotSkipped(t *testing.T) {
	var (
		out, pagerOut bytes.Buffer
		pkgInfo       = sborepo.PackageCreator{
			Name:              DUMMY_PKG3,
			Path:              t.TempDir(),
			Version:           DUMMY_VERSION,
			Download:          DUMMY_PKG3_DOWNLOAD,
			MD5Sum:            "d41d8cd98f00b204e9800998ecf8427e",
			Requires:          "%README%",
			SlackBuildContent: MOCK_PKG_SLACKBUILD_CONTENT,
			READMEContent:     MOCK_PKG_README_CONTENT,
			SlackDescContent:  MOCK_PKG_SLACK_DES_CONTENT,
		}.Create()
		dg    = mustDependencyGraph(t, sborepo.NewRepoMap(filepath.Dir(filepath.Dir(pkgInfo.Path()))), DUMMY_PKG3)
		pkg   = mustGetGraphPackages(t, dg, DUMMY_PKG3)[DUMMY_PKG3]
		w     = bufio.NewWriter(&out)
		pager = func(files ...string) error {
			for _, file := range files {
				pagerOut.Write(must.ReadFile(file))
			}
			return nil
		}
	)

	if !pkg.IsReadmeRequired() {
		t.Error("Expected IsReadmeRequired() to be true")
	}

	ui := CliUI{stdout: w, stdin: strings.NewReader("y\nn\n"), pager: pager}
	promptIfReadmeRequired(pkg, dg, &ui)
	mustBeEqual(t, pagerOut.String(), MOCK_PKG_README_CONTENT)
	mustBeInList(t, dg, pkg.Name)

	ui.stdin = strings.NewReader("y\ny\n") // Now we skip the package
	promptIfReadmeRequired(pkg, dg, &ui)
	mustNotBeInList(t, dg, pkg.Name)
}

func TestPromptIfReadmeRequiredSkippedWithDependents(t *testing.T) {
	var (
		dg, graphMap, _ = mockDependents(t, DUMMY_PKG1, DUMMY_PKG2, DUMMY_PKG3)
		pkg             = graphMap[DUMMY_PKG1]
		out             bytes.Buffer
		w               = bufio.NewWriter(&out)
	)
	pkgInfo := sborepo.PackageCreator{
		Name:              DUMMY_PKG1,
		Path:              pkg.ParentPath,
		Version:           DUMMY_VERSION,
		Requires:          "%README%",
		SlackBuildContent: MOCK_PKG_SLACKBUILD_CONTENT,
		READMEContent:     MOCK_PKG_README_CONTENT,
		SlackDescContent:  MOCK_PKG_SLACK_DES_CONTENT,
	}.Create()
	*pkg = pkgInfo

	ui := CliUI{stdout: w, stdin: strings.NewReader("y\ny\n"), pager: func(...string) error { return nil }}
	promptIfReadmeRequired(pkg, dg, &ui)

	mustContain(t, out.String(), "dependents", DUMMY_PKG2, DUMMY_PKG3)
	mustNotBeInList(t, dg, pkg.Name)
	mustNotBeInList(t, dg, DUMMY_PKG2)
	mustNotBeInList(t, dg, DUMMY_PKG3)
}

func TestPkgNameFromFile(t *testing.T) {
	greylistFile := filepath.Join(t.TempDir(), "mygreylist.txt")
	must.Content2File(greylistFile, DUMMY_PKG2+"\n"+DUMMY_PKG3+"\n")

	names := pkgNameFromFile(greylistFile)
	sort.Strings(names)

	mustBeEqualStringSlices(t, names, []string{DUMMY_PKG2, DUMMY_PKG3})
}

func TestPkgNameFromFileMissingFile(t *testing.T) {
	names := pkgNameFromFile(filepath.Join(t.TempDir(), "missing-greylist"))

	mustBeEqual(t, len(names), 0)
}

func mustBeInList(t *testing.T, dg sborepo.DependencyGraph, name string) {
	t.Helper()
	if !graphContains(dg, name) {
		t.Errorf("expected package %s to be in the list, but it is not", name)
	}
}

func mustNotBeInList(t *testing.T, dg sborepo.DependencyGraph, name string) {
	t.Helper()
	if graphContains(dg, name) {
		t.Errorf("expected package %s to NOT be in the list, but it is", name)
	}
}

func graphContains(dg sborepo.DependencyGraph, name string) bool {
	for _, pkgName := range graphPackageNames(dg) {
		if pkgName == name {
			return true
		}
	}
	return false
}

func graphPackageNames(dg sborepo.DependencyGraph) (names []string) {
	dg.ProcessUnordered(func(pkg *sborepo.Package) { names = append(names, pkg.Name) })
	return names
}

func makeDummyList(t *testing.T, pkgNames ...string) (sborepo.DependencyGraph, sborepo.RepoMap) {
	t.Helper()

	repo := t.TempDir()
	categoryPath := filepath.Join(repo, "category")
	for _, pkgName := range pkgNames {
		sborepo.PackageCreator{
			Name:              pkgName,
			Path:              categoryPath,
			Version:           DUMMY_VERSION,
			Download:          DUMMY_PKG3_DOWNLOAD,
			MD5Sum:            "d41d8cd98f00b204e9800998ecf8427e",
			SlackBuildContent: MOCK_PKG_SLACKBUILD_CONTENT,
			READMEContent:     MOCK_PKG_README_CONTENT,
			SlackDescContent:  MOCK_PKG_SLACK_DES_CONTENT,
		}.Create()
	}

	rm := sborepo.NewRepoMap(repo)
	dg := mustDependencyGraph(t, rm, pkgNames...)
	return dg, rm
}

func mockDependents(t *testing.T, pkgName string, dependentNames ...string) (sborepo.DependencyGraph, map[string]*sborepo.Package, sborepo.RepoMap) {
	t.Helper()

	repo := t.TempDir()
	categoryPath := filepath.Join(repo, "test-category")
	sborepo.PackageCreator{
		Name:    pkgName,
		Path:    categoryPath,
		Version: DUMMY_VERSION,
	}.Create()
	for _, dependentName := range dependentNames {
		sborepo.PackageCreator{
			Name:     dependentName,
			Path:     categoryPath,
			Version:  DUMMY_VERSION,
			Requires: pkgName,
		}.Create()
	}
	rm := sborepo.NewRepoMap(repo)
	dg := mustDependencyGraph(t, rm, dependentNames...)
	return dg, mustGetGraphPackages(t, dg, append([]string{pkgName}, dependentNames...)...), rm
}

func mustDependencyGraph(t *testing.T, rm sborepo.RepoMap, pkgNames ...string) sborepo.DependencyGraph {
	t.Helper()

	dg, err := rm.DependencyGraph(pkgNames)
	if err != nil {
		t.Fatal(err)
	}
	return dg
}

func mustGetGraphPackages(t *testing.T, dg sborepo.DependencyGraph, pkgNames ...string) map[string]*sborepo.Package {
	t.Helper()

	pkgs := make(map[string]*sborepo.Package, len(pkgNames))
	dg.ProcessUnordered(func(pkg *sborepo.Package) {
		pkgs[pkg.Name] = pkg
	})
	for _, name := range pkgNames {
		if pkgs[name] == nil {
			t.Fatalf("expected graph to contain package %q", name)
		}
	}
	return pkgs
}

func md5Hex(s string) string {
	sum := md5.Sum([]byte(s))
	return hex.EncodeToString(sum[:])
}
