package pkg

import (
	"bufio"
	"bytes"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"gitlab.com/M0M097/pkg/lib/sboRepo"
	"gitlab.com/M0M097/pkg/lib/utils"
)

func TestFilterInstalledPkgs(t *testing.T) {
	var ( // Step 1: Create the minimal chain [prev -> name -> next]
		prev = sboRepo.NewPackage("prev", "")
		p    = sboRepo.NewPackage("name", "")
		next = sboRepo.NewPackage("next", "")
		head = makeDummyList(t, &prev, &p, &next)
	)

	// Step 2: Initially, none of them are "up-to-date", so filtering should remove nothing.
	filterInstalledPkgs(head, "/doc/dir", "kernel-1.0")

	// Ensure the chain is still [prev -> name -> next]
	mustBeInList(t, &p)    // "name" should still be in the list
	mustBeInList(t, &next) // "next" should still be in the list

	// Step 3: Now simulate "prev" being up-to-date. According to IsUpToDate:
	//         docDir + pkg.FullName() OR docDir + pkg.FullName() + "_" + kernelVersion
	//         must exist, and SlackBuild must match (MD5) with the one in the docDir.
	docDir := t.TempDir()

	// We'll assume "next" has version "1.0" from the .info content. By default, FullName = "next-1.0".
	// Let's create docDir/next-1.0 and copy the SlackBuild file there.
	fullPath := filepath.Join(docDir, next.FullName()) // typically "next-1.0"
	utils.Mkdir(fullPath)

	// Copy the SlackBuild from the package to that location
	srcSlackBuild := utils.Open(next.SlackBuild()) // e.g., /tmp/someRandomDir/next.SlackBuild
	defer srcSlackBuild.Close()
	dstSlackBuildPath := filepath.Join(fullPath, next.Name()+".SlackBuild")
	dstSlackBuild := utils.Create(dstSlackBuildPath)
	defer dstSlackBuild.Close()
	utils.Copy(srcSlackBuild, dstSlackBuild) // so MD5 sums match

	// Step 4: Filter again, now "next" should be removed from the list (IsUpToDate returns true).
	filterInstalledPkgs(head, docDir, "kernel-1.0")

	// The chain should now skip 'next', leaving [prev -> name]
	mustNotBeInList(t, &next) // "next" should be removed from the list
	// Meanwhile, 'name' and 'prev' should still be in the list
	mustBeInList(t, &p)    // "name" should still be in the list
	mustBeInList(t, &prev) // prev should still be in the list
}

func TestLetUserExcludePkgs(t *testing.T) {
	var (
		p      = sboRepo.NewPackage("name", "")
		next   = sboRepo.NewPackage("next", "")
		head   = makeDummyList(t, &p, &next) // head -> name -> next
		buf    bytes.Buffer
		errs   packageErrors
		writer = bufio.NewWriter(&buf)
		reader = strings.NewReader("2\n") // user input: "2" => exclude the 2nd package in the listing
		ipack  int
	)

	// The function prints a list of packages (1, 2, 3, ...) then reads user input
	// In our chain, iteration sees next packages: [name, next]
	// So "1" => name, "2" => next
	letUserExcludePkgs(head, writer, reader, &errs)
	writer.Flush()

	if nerr := len(errs); nerr != 1 {
		t.Fatalf("Expected 1 error entry, got %d\nFull output:\n%s", nerr, buf.String())
	}
	if !strings.Contains(errs[0], "next") || !strings.Contains(errs[0], "Excluded by user") {
		t.Errorf("Expected 'next: Excluded by user', got %q", errs[0])
	}

	// Ensure "next" was removed from chain, only "name" remains after head
	mustNotBeInList(t, &next) // next should be removed
	head.Iterate(func(pkg *sboRepo.Package) { ipack++ })
	if !p.IsInList() || ipack != 1 {
		t.Errorf("Expected 'name' to be the only package in the list, but got %d", ipack)
	}
}

func TestLetUserExcludePkgs_InvalidInput(t *testing.T) {
	var (
		p      = sboRepo.NewPackage(DUMMY_PKG3, "")
		next   = sboRepo.NewPackage("next", "")
		head   = makeDummyList(t, &p, &next) // head -> DUMMY_PKG3 -> next
		output bytes.Buffer
		errs   packageErrors
		writer = bufio.NewWriter(&output)
		reader = strings.NewReader("99\n") // invalid input
	)

	letUserExcludePkgs(head, writer, reader, &errs)
	writer.Flush()

	// Because it's invalid, the function tries again recursively.
	// The easiest check is to confirm that "Invalid input. Repeat..." is in the output
	outStr := output.String()
	if !strings.Contains(outStr, "Invalid input: `99` Repeat...") {
		t.Errorf("Expected 'Invalid input. Repeat...' in output, got:\n%s", outStr)
	}
	// We won't re-check final chain state or errs because the function restarts.
}

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]
		p1   = sboRepo.NewPackage("p1", "")
		p2   = sboRepo.NewPackage("p2", "")
		head = makeDummyList(t, &p1, &p2) // head -> 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")
	wantLines := []string{"alreadyThere1", "alreadyThere2", "p1", "p2"}
	mustBeEqualStringSlices(t, lines, wantLines)
}

func TestUpdateBlacklist_MissingOtherFile(t *testing.T) {
	// If otherBlacklist doesn’t exist, we expect an error with certain text
	head := sboRepo.NewListHead()
	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(), "does not exist") {
		t.Errorf("expected 'does not exist' 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 a Head package
	head := sboRepo.NewListHead()

	// 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]
		p1   = sboRepo.NewPackage("p1", "")
		p2   = sboRepo.NewPackage("p2", "")
		head = makeDummyList(t, &p1, &p2) // head -> p1 -> p2
	)

	urls := filesToDownload(head)
	// 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 TestBuildCmdFail(t *testing.T) {
	pkg := sboRepo.NewPackage(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 TestPreBuildCmdSuccess(t *testing.T) {
	pkg := sboRepo.NewPackage(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)
	utils.Create(fname)
	utils.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 TestPromptIfReadmeRequiredNotSkipped(t *testing.T) {
	PagerCmd := os.Getenv("PAGER")
	os.Setenv("PAGER", "/bin/cat")
	defer os.Setenv("PAGER", PagerCmd)

	var (
		out, pagerOut, logOut bytes.Buffer
		pkg                   = sboRepo.PkgCreator{
			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()
		w      = bufio.NewWriter(&out)
		logw   = bufio.NewWriter(&logOut)
		pagerw = bufio.NewWriter(&pagerOut)
		exec   = &utils.Executor{Stdout: pagerw, Stderr: pagerw, Logger: log.New(logw, "", 0)}
		pager  = utils.NewPager(exec, FALLBACK_PAGER)
		eh     = &packageErrors{}
		input  = "y\nn\n" // Show the README but do not skip the package
	)

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

	promptIfReadmeRequired(pkg, w, strings.NewReader(input), pager, eh)
	pagerw.Flush()
	mustBeEqual(t, pagerOut.String(), MOCK_PKG_README_CONTENT)
	if len(*eh) > 0 {
		t.Error("There should be no errors added to the error handler")
	}

	input = "y\ny\n" // Now we skip the package
	promptIfReadmeRequired(pkg, w, strings.NewReader(input), pager, eh)
	pagerw.Flush()
	if len(*eh) == 0 {
		t.Error("The package was not skipped")
	}
}

func TestFilterGreylistedPkgs(t *testing.T) {
	var ( // Build a small repo and a list. Suppose we have "pkg1", "pkg2", "pkg3"
		repo, tmpDir = makeDummyRepo(t)
		rm           = sboRepo.NewRepoMap(repo)
		head         = sboRepo.NewListHead()
		greylistFile = filepath.Join(tmpDir, "mygreylist.txt")
		errs         packageErrors
	)

	rm.Get(DUMMY_PKG1).Append(head)               // head -> pkg1
	rm.Get(DUMMY_PKG2).Append(rm.Get(DUMMY_PKG1)) // pkg1 -> pkg2
	rm.Get(DUMMY_PKG3).Append(rm.Get(DUMMY_PKG2)) // pkg2 -> pkg3

	// Now we create a real greylist file in a temp directory
	// tmpDir := t.TempDir()
	utils.Content2File(greylistFile, DUMMY_PKG2+"\n"+DUMMY_PKG3+"\n")

	filterGreylistedPkgs(rm, greylistFile, &errs)

	// Checks
	mustBeEqual(t, len(errs), 2)
	mustContain(t, errs[0], DUMMY_PKG2, "Greylisted")
	mustContain(t, errs[1], DUMMY_PKG3, "Greylisted")
	mustBeInList(t, rm.Get(DUMMY_PKG1))    // pkg1 should remain in the list
	mustNotBeInList(t, rm.Get(DUMMY_PKG2)) // pkg2 should be removed
	mustNotBeInList(t, rm.Get(DUMMY_PKG3)) // pkg3 should be removed
}

func mustBeInList(t *testing.T, pkg *sboRepo.Package) {
	t.Helper()
	if !pkg.IsInList() {
		t.Errorf("expected package %s to be in the list, but it is not", pkg.Name())
	}
}

func mustNotBeInList(t *testing.T, pkg *sboRepo.Package) {
	t.Helper()
	if pkg.IsInList() {
		t.Errorf("expected package %s to NOT be in the list, but it is", pkg.Name())
	}
}

func makeDummyList(t *testing.T, pkgs ...*sboRepo.Package) sboRepo.Head {
	var (
		head = sboRepo.NewListHead()
		tail = head
	)

	for _, p := range pkgs {
		createPkgTestfiles(p, t)
		tail = p.Append(tail)
	}
	return head
}

func TestFullPkgName2PkgName(t *testing.T) {
	expected := "pkg"

	pkgName := fullPkgName2PkgName(expected + "-1.0")
	mustBeEqual(t, pkgName, expected)

	pkgName = fullPkgName2PkgName(expected + "-1.0_kernel5.10.0_1.0.0_amd64")
	mustBeEqual(t, pkgName, expected)

	pkgName = fullPkgName2PkgName("pkg-foo-1.0")
	mustBeEqual(t, pkgName, "pkg-foo")

	pkgName = fullPkgName2PkgName("pkg")
	mustBeEqual(t, pkgName, "")
}
