package fcproc

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

// pidfileFixture builds the on-disk shape findByJailerPidfile inspects:
// a /proc-like tree (one entry per pid with comm), an api-sock symlink
// pointing into a faux chroot, and the chroot's firecracker.pid file.
type pidfileFixture struct {
	root    string
	proc    string
	runtime string
	chroots string
}

func newPidfileFixture(t *testing.T) *pidfileFixture {
	t.Helper()
	root := t.TempDir()
	f := &pidfileFixture{
		root:    root,
		proc:    filepath.Join(root, "proc"),
		runtime: filepath.Join(root, "runtime"),
		chroots: filepath.Join(root, "chroots"),
	}
	for _, dir := range []string{f.proc, f.runtime, f.chroots} {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatalf("mkdir %s: %v", dir, err)
		}
	}
	prev := procDir
	procDir = f.proc
	t.Cleanup(func() { procDir = prev })
	return f
}

// addProc writes /proc/<pid>/comm. Mirrors the real /proc shape (comm
// has a trailing newline; production code TrimSpaces it).
func (f *pidfileFixture) addProc(t *testing.T, pid int, comm string) {
	t.Helper()
	pidDir := filepath.Join(f.proc, fmt.Sprint(pid))
	if err := os.MkdirAll(pidDir, 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", pidDir, err)
	}
	if err := os.WriteFile(filepath.Join(pidDir, "comm"), []byte(comm+"\n"), 0o644); err != nil {
		t.Fatalf("write comm: %v", err)
	}
}

// buildVMSocket lays out the chroot for a VM and returns the api-sock
// path the test points findByJailerPidfile at. pidfileContent is what
// `cat <chroot>/firecracker.pid` will return — pass an empty string to
// skip writing the pidfile.
func (f *pidfileFixture) buildVMSocket(t *testing.T, vmid, pidfileContent string) (apiSock string) {
	t.Helper()
	chroot := filepath.Join(f.chroots, vmid, "root")
	if err := os.MkdirAll(chroot, 0o755); err != nil {
		t.Fatalf("mkdir chroot: %v", err)
	}
	socketTarget := filepath.Join(chroot, "firecracker.socket")
	if err := os.WriteFile(socketTarget, nil, 0o600); err != nil {
		t.Fatalf("write socket placeholder: %v", err)
	}
	if pidfileContent != "" {
		if err := os.WriteFile(filepath.Join(chroot, "firecracker.pid"), []byte(pidfileContent), 0o600); err != nil {
			t.Fatalf("write pidfile: %v", err)
		}
	}
	apiSock = filepath.Join(f.runtime, "fc-"+vmid+".sock")
	if err := os.Symlink(socketTarget, apiSock); err != nil {
		t.Fatalf("symlink api sock: %v", err)
	}
	return apiSock
}

func TestFindByJailerPidfileHappyPath(t *testing.T) {
	f := newPidfileFixture(t)
	apiSock := f.buildVMSocket(t, "abc", "100\n")
	f.addProc(t, 100, "firecracker")

	got, err := findByJailerPidfile(apiSock)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 100 {
		t.Fatalf("pid = %d, want 100", got)
	}
}

func TestFindByJailerPidfileMissingPidfile(t *testing.T) {
	f := newPidfileFixture(t)
	// VM exists in the chroot layout but no pidfile (e.g. VM was created
	// but never started, or stopped and pidfile cleared).
	apiSock := f.buildVMSocket(t, "abc", "")

	_, err := findByJailerPidfile(apiSock)
	if !errors.Is(err, errFirecrackerPIDNotFound) {
		t.Fatalf("err = %v, want errFirecrackerPIDNotFound", err)
	}
}

func TestFindByJailerPidfileStalePID(t *testing.T) {
	f := newPidfileFixture(t)
	// Pidfile points at a PID with no /proc entry — the FC died but the
	// pidfile was left behind. Reconcile must treat this as "not running"
	// so the rediscoverHandles path can mark the VM stopped cleanly.
	apiSock := f.buildVMSocket(t, "abc", "100\n")
	// Deliberately don't addProc(100, ...).

	_, err := findByJailerPidfile(apiSock)
	if !errors.Is(err, errFirecrackerPIDNotFound) {
		t.Fatalf("err = %v, want errFirecrackerPIDNotFound", err)
	}
}

func TestFindByJailerPidfileWrongComm(t *testing.T) {
	f := newPidfileFixture(t)
	// PID was recycled by the kernel and now belongs to some other
	// process. The comm check is what catches this — pidfile content is
	// untrusted across reboots / PID-wraparound.
	apiSock := f.buildVMSocket(t, "abc", "100\n")
	f.addProc(t, 100, "bash")

	_, err := findByJailerPidfile(apiSock)
	if !errors.Is(err, errFirecrackerPIDNotFound) {
		t.Fatalf("err = %v, want errFirecrackerPIDNotFound", err)
	}
}

func TestFindByJailerPidfileGarbageContent(t *testing.T) {
	f := newPidfileFixture(t)
	apiSock := f.buildVMSocket(t, "abc", "not-a-pid\n")

	_, err := findByJailerPidfile(apiSock)
	if !errors.Is(err, errFirecrackerPIDNotFound) {
		t.Fatalf("err = %v, want errFirecrackerPIDNotFound", err)
	}
}

func TestFindByJailerPidfileNonSymlinkApiSock(t *testing.T) {
	f := newPidfileFixture(t)
	// Direct (non-jailer) launches produce a regular-file api sock with
	// no chroot beside it. Pidfile lookup can't help; fall through cleanly.
	apiSock := filepath.Join(f.runtime, "direct-launch.sock")
	if err := os.WriteFile(apiSock, nil, 0o600); err != nil {
		t.Fatalf("write apiSock: %v", err)
	}

	_, err := findByJailerPidfile(apiSock)
	if !errors.Is(err, errFirecrackerPIDNotFound) {
		t.Fatalf("err = %v, want errFirecrackerPIDNotFound", err)
	}
}

func TestFindByJailerPidfileTrimsWhitespace(t *testing.T) {
	f := newPidfileFixture(t)
	// Some FC versions write the pidfile with stray whitespace; the
	// parser must tolerate it.
	apiSock := f.buildVMSocket(t, "abc", "  100  \n\n")
	f.addProc(t, 100, "firecracker")

	got, err := findByJailerPidfile(apiSock)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 100 {
		t.Fatalf("pid = %d, want 100", got)
	}
}
