package kernelcat

import (
	"errors"
	"os"
	"path/filepath"
	"testing"
)

func writeFile(t *testing.T, path string, data string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
		t.Fatal(err)
	}
}

func TestDiscoverPathsPrefersMetadataJSON(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	writeFile(t, filepath.Join(dir, "boot", "vmlinux-custom"), "ignored")
	writeFile(t, filepath.Join(dir, "boot", "initramfs-custom"), "ignored")
	writeFile(t, filepath.Join(dir, "boot", "vmlinux-pick-me"), "kernel")
	writeFile(t, filepath.Join(dir, "boot", "initramfs-pick-me"), "initrd")
	if err := os.MkdirAll(filepath.Join(dir, "lib", "modules", "6.12.79_1"), 0o755); err != nil {
		t.Fatal(err)
	}
	metadata := `{
"kernel_path": "boot/vmlinux-pick-me",
"initrd_path": "boot/initramfs-pick-me",
"modules_dir": "lib/modules/6.12.79_1"
}`
	writeFile(t, filepath.Join(dir, "metadata.json"), metadata)

	got, err := DiscoverPaths(dir)
	if err != nil {
		t.Fatalf("DiscoverPaths: %v", err)
	}
	if got.KernelPath != filepath.Join(dir, "boot", "vmlinux-pick-me") {
		t.Errorf("KernelPath = %q", got.KernelPath)
	}
	if got.InitrdPath != filepath.Join(dir, "boot", "initramfs-pick-me") {
		t.Errorf("InitrdPath = %q", got.InitrdPath)
	}
	if got.ModulesDir != filepath.Join(dir, "lib", "modules", "6.12.79_1") {
		t.Errorf("ModulesDir = %q", got.ModulesDir)
	}
}

func TestDiscoverPathsFallsBackToGlobbing(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	writeFile(t, filepath.Join(dir, "boot", "vmlinux-6.12.0"), "k")
	writeFile(t, filepath.Join(dir, "boot", "vmlinux-6.12.1"), "newer")
	writeFile(t, filepath.Join(dir, "boot", "initramfs-6.12.1"), "i")
	if err := os.MkdirAll(filepath.Join(dir, "lib", "modules", "6.12.0"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(dir, "lib", "modules", "6.12.1"), 0o755); err != nil {
		t.Fatal(err)
	}

	got, err := DiscoverPaths(dir)
	if err != nil {
		t.Fatalf("DiscoverPaths: %v", err)
	}
	if got.KernelPath != filepath.Join(dir, "boot", "vmlinux-6.12.1") {
		t.Errorf("KernelPath = %q, want latest", got.KernelPath)
	}
	if got.InitrdPath != filepath.Join(dir, "boot", "initramfs-6.12.1") {
		t.Errorf("InitrdPath = %q", got.InitrdPath)
	}
	if got.ModulesDir != filepath.Join(dir, "lib", "modules", "6.12.1") {
		t.Errorf("ModulesDir = %q, want latest subdir", got.ModulesDir)
	}
}

func TestDiscoverPathsAlpineVmlinuzFallback(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	// Alpine older layouts may only ship vmlinuz-virt.
	writeFile(t, filepath.Join(dir, "boot", "vmlinuz-virt"), "k")
	writeFile(t, filepath.Join(dir, "boot", "initramfs-virt"), "i")

	got, err := DiscoverPaths(dir)
	if err != nil {
		t.Fatalf("DiscoverPaths: %v", err)
	}
	if got.KernelPath != filepath.Join(dir, "boot", "vmlinuz-virt") {
		t.Errorf("KernelPath = %q, want vmlinuz-virt fallback", got.KernelPath)
	}
}

func TestDiscoverPathsMissingKernelIsError(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	// boot/ exists but contains no kernel
	if err := os.MkdirAll(filepath.Join(dir, "boot"), 0o755); err != nil {
		t.Fatal(err)
	}
	_, err := DiscoverPaths(dir)
	if err == nil {
		t.Fatal("expected error when no kernel present")
	}
	if !errors.Is(err, os.ErrNotExist) && !containsErr(err, "locate kernel") {
		t.Fatalf("error shape: %v", err)
	}
}

func TestDiscoverPathsNotADirectory(t *testing.T) {
	t.Parallel()
	path := filepath.Join(t.TempDir(), "file")
	writeFile(t, path, "")
	_, err := DiscoverPaths(path)
	if err == nil {
		t.Fatal("expected error when fromDir is a file")
	}
}

func containsErr(err error, substr string) bool {
	return err != nil && (err.Error() == substr || len(err.Error()) >= len(substr) && errContains(err.Error(), substr))
}

func errContains(s, substr string) bool {
	for i := 0; i+len(substr) <= len(s); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
