package kernelcat

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

func TestValidateName(t *testing.T) {
	t.Parallel()
	cases := []struct {
		name    string
		wantErr bool
	}{
		{"void-6.12", false},
		{"alpine_3.20", false},
		{"a", false},
		{"Void-6.12", false},
		{"", true},
		{"-leading-dash", true},
		{".leading-dot", true},
		{"has space", true},
		{"has/slash", true},
		{"../escape", true},
	}
	for _, tc := range cases {
		err := ValidateName(tc.name)
		if tc.wantErr && err == nil {
			t.Errorf("ValidateName(%q) err=nil, want error", tc.name)
		}
		if !tc.wantErr && err != nil {
			t.Errorf("ValidateName(%q) err=%v, want nil", tc.name, err)
		}
	}
}

func TestWriteAndReadLocalRoundTrip(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	entry := Entry{
		Name:          "void-6.12",
		Distro:        "void",
		Arch:          "x86_64",
		KernelVersion: "6.12.79_1",
		SHA256:        "deadbeef",
		Source:        "import:testdata",
	}
	if err := WriteLocal(dir, entry); err != nil {
		t.Fatalf("WriteLocal: %v", err)
	}

	kernelPath := filepath.Join(dir, entry.Name, "vmlinux")
	if err := os.WriteFile(kernelPath, []byte("kernel-bytes"), 0o644); err != nil {
		t.Fatalf("write kernel: %v", err)
	}
	modulesPath := filepath.Join(dir, entry.Name, "modules", "6.12.79_1", "modules.dep")
	if err := os.MkdirAll(filepath.Dir(modulesPath), 0o755); err != nil {
		t.Fatalf("mkdir modules: %v", err)
	}
	if err := os.WriteFile(modulesPath, []byte(""), 0o644); err != nil {
		t.Fatalf("write modules stub: %v", err)
	}

	got, err := ReadLocal(dir, entry.Name)
	if err != nil {
		t.Fatalf("ReadLocal: %v", err)
	}
	if got.Name != entry.Name || got.Distro != "void" || got.KernelVersion != "6.12.79_1" {
		t.Fatalf("ReadLocal round-trip mismatch: %+v", got)
	}
	if got.KernelPath != kernelPath {
		t.Errorf("KernelPath = %q, want %q", got.KernelPath, kernelPath)
	}
	if got.InitrdPath != "" {
		t.Errorf("InitrdPath = %q, want empty (no initrd on disk)", got.InitrdPath)
	}
	if got.ModulesDir != filepath.Join(dir, entry.Name, "modules") {
		t.Errorf("ModulesDir = %q", got.ModulesDir)
	}
	if got.ImportedAt.IsZero() {
		t.Errorf("ImportedAt not populated by WriteLocal")
	}
	if time.Since(got.ImportedAt) > time.Minute {
		t.Errorf("ImportedAt too far in the past: %v", got.ImportedAt)
	}
}

func TestReadLocalRejectsMismatchedName(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	entryDir := filepath.Join(dir, "void-6.12")
	if err := os.MkdirAll(entryDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(entryDir, manifestFilename), []byte(`{"name":"other"}`), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := ReadLocal(dir, "void-6.12"); err == nil {
		t.Fatal("ReadLocal should reject manifest with mismatched name")
	}
}

func TestListLocalSkipsManifestless(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	if err := WriteLocal(dir, Entry{Name: "alpine-3.20"}); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(dir, "orphan"), 0o755); err != nil {
		t.Fatal(err)
	}
	entries, err := ListLocal(dir)
	if err != nil {
		t.Fatalf("ListLocal: %v", err)
	}
	if len(entries) != 1 || entries[0].Name != "alpine-3.20" {
		t.Fatalf("ListLocal = %+v, want one alpine-3.20", entries)
	}
}

func TestListLocalReturnsEmptyForMissingDir(t *testing.T) {
	t.Parallel()
	entries, err := ListLocal(filepath.Join(t.TempDir(), "nope"))
	if err != nil {
		t.Fatalf("ListLocal: %v", err)
	}
	if len(entries) != 0 {
		t.Fatalf("ListLocal = %v, want empty", entries)
	}
}

func TestDeleteLocalRemovesEntry(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	if err := WriteLocal(dir, Entry{Name: "void-6.12"}); err != nil {
		t.Fatal(err)
	}
	if err := DeleteLocal(dir, "void-6.12"); err != nil {
		t.Fatalf("DeleteLocal: %v", err)
	}
	if _, err := os.Stat(EntryDir(dir, "void-6.12")); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("expected entry dir removed, stat err=%v", err)
	}
}

func TestDeleteLocalIdempotent(t *testing.T) {
	t.Parallel()
	if err := DeleteLocal(t.TempDir(), "never-existed"); err != nil {
		t.Fatalf("DeleteLocal on missing entry: %v", err)
	}
}

func TestSumFileMatchesSHA256(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	path := filepath.Join(dir, "data")
	if err := os.WriteFile(path, []byte("banger"), 0o644); err != nil {
		t.Fatal(err)
	}
	sum, err := SumFile(path)
	if err != nil {
		t.Fatalf("SumFile: %v", err)
	}
	// precomputed sha256("banger")
	const want = "e0c69eae8afb38872fa425c2cdba794176f3b9d97e8eefb7b0e7c831f566458f"
	if sum != want {
		t.Fatalf("SumFile = %q, want %q", sum, want)
	}
}
