package daemon

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"banger/internal/api"
	"banger/internal/model"
	"banger/internal/paths"
	"banger/internal/store"
	"banger/internal/vmdns"
	"banger/internal/vsockagent"
)

func TestFindVMPrefixResolution(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	d := &Daemon{store: db}
	wireServices(d)

	for _, vm := range []model.VMRecord{
		testVM("alpha", "image-alpha", "172.16.0.2"),
		testVM("alpine", "image-alpha", "172.16.0.3"),
		testVM("bravo", "image-alpha", "172.16.0.4"),
	} {
		upsertDaemonVM(t, ctx, db, vm)
	}

	vm, err := d.FindVM(ctx, "alpha")
	if err != nil || vm.Name != "alpha" {
		t.Fatalf("FindVM(alpha) = %+v, %v", vm, err)
	}

	vm, err = d.FindVM(ctx, "br")
	if err != nil || vm.Name != "bravo" {
		t.Fatalf("FindVM(br) = %+v, %v", vm, err)
	}

	_, err = d.FindVM(ctx, "al")
	if err == nil || !strings.Contains(err.Error(), "multiple VMs match") {
		t.Fatalf("FindVM(al) error = %v, want ambiguity", err)
	}

	_, err = d.FindVM(ctx, "missing")
	if err == nil || !strings.Contains(err.Error(), `vm "missing" not found`) {
		t.Fatalf("FindVM(missing) error = %v, want not-found", err)
	}
}

func TestFindImagePrefixResolution(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	d := &Daemon{store: db}
	wireServices(d)

	for _, image := range []model.Image{
		testImage("base"),
		testImage("basic"),
		testImage("docker"),
	} {
		if err := db.UpsertImage(ctx, image); err != nil {
			t.Fatalf("UpsertImage(%s): %v", image.Name, err)
		}
	}

	image, err := d.FindImage(ctx, "base")
	if err != nil || image.Name != "base" {
		t.Fatalf("FindImage(base) = %+v, %v", image, err)
	}

	image, err = d.FindImage(ctx, "dock")
	if err != nil || image.Name != "docker" {
		t.Fatalf("FindImage(dock) = %+v, %v", image, err)
	}

	_, err = d.FindImage(ctx, "ba")
	if err == nil || !strings.Contains(err.Error(), "multiple images match") {
		t.Fatalf("FindImage(ba) error = %v, want ambiguity", err)
	}

	_, err = d.FindImage(ctx, "missing")
	if err == nil || !strings.Contains(err.Error(), `image "missing" not found`) {
		t.Fatalf("FindImage(missing) error = %v, want not-found", err)
	}
}

func TestReconcileStopsStaleRunningVMAndClearsRuntimeHandles(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	if err := os.WriteFile(apiSock, []byte{}, 0o644); err != nil {
		t.Fatalf("WriteFile(api sock): %v", err)
	}
	vmDir := t.TempDir()
	vm := testVM("stale", "image-stale", "172.16.0.9")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	vm.Runtime.VMDir = vmDir
	vm.Runtime.DNSName = ""
	upsertDaemonVM(t, ctx, db, vm)

	// Simulate the prior daemon crashing while this VM was running:
	// the handles.json scratch file survives and names a stale PID +
	// DM snapshot. Reconcile should discover the PID is gone, tear
	// the kernel state down via the runner, and clear the scratch.
	stale := model.VMHandles{
		PID:      999999,
		BaseLoop: "/dev/loop10",
		COWLoop:  "/dev/loop11",
		DMName:   "fc-rootfs-stale",
		DMDev:    "/dev/mapper/fc-rootfs-stale",
	}
	if err := writeHandlesFile(vmDir, stale); err != nil {
		t.Fatalf("writeHandlesFile: %v", err)
	}

	runner := &scriptedRunner{
		t: t,
		steps: []runnerStep{
			// First pgrep: rediscoverHandles tries to verify the PID.
			{call: runnerCall{name: "pgrep", args: []string{"-n", "-f", apiSock}}, err: errors.New("exit status 1")},
			// Second pgrep: cleanupRuntime asks again before killing.
			{call: runnerCall{name: "pgrep", args: []string{"-n", "-f", apiSock}}, err: errors.New("exit status 1")},
			sudoStep("", nil, "dmsetup", "remove", "fc-rootfs-stale"),
			sudoStep("", nil, "losetup", "-d", "/dev/loop11"),
			sudoStep("", nil, "losetup", "-d", "/dev/loop10"),
		},
	}
	d := &Daemon{store: db, runner: runner}
	wireServices(d)

	if err := d.reconcile(ctx); err != nil {
		t.Fatalf("reconcile: %v", err)
	}
	runner.assertExhausted()

	got, err := db.GetVM(ctx, vm.ID)
	if err != nil {
		t.Fatalf("GetVM: %v", err)
	}
	if got.State != model.VMStateStopped || got.Runtime.State != model.VMStateStopped {
		t.Fatalf("vm state after reconcile = %s/%s, want stopped", got.State, got.Runtime.State)
	}
	// The scratch file must be gone — stopped VMs don't carry handles.
	if _, err := os.Stat(handlesFilePath(vmDir)); !os.IsNotExist(err) {
		t.Fatalf("handles.json still present after reconcile: %v", err)
	}
	// And the in-memory cache must be empty.
	if h, ok := d.vm.handles.get(vm.ID); ok && !h.IsZero() {
		t.Fatalf("handle cache not cleared after reconcile: %+v", h)
	}
}

func TestReconcileWithCorruptHandlesFileFallsBackToPersistedRuntimeTeardownState(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	if err := os.WriteFile(apiSock, []byte{}, 0o644); err != nil {
		t.Fatalf("WriteFile(api sock): %v", err)
	}
	vmDir := t.TempDir()
	vm := testVM("corrupt", "image-corrupt", "172.16.0.10")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	vm.Runtime.VMDir = vmDir
	vm.Runtime.DNSName = ""
	vm.Runtime.TapDevice = "tap-fc-corrupt"
	vm.Runtime.BaseLoop = "/dev/loop20"
	vm.Runtime.COWLoop = "/dev/loop21"
	vm.Runtime.DMName = "fc-rootfs-corrupt"
	vm.Runtime.DMDev = "/dev/mapper/fc-rootfs-corrupt"
	upsertDaemonVM(t, ctx, db, vm)

	if err := os.WriteFile(handlesFilePath(vmDir), []byte("{not json"), 0o600); err != nil {
		t.Fatalf("WriteFile(handles.json): %v", err)
	}

	runner := &scriptedRunner{
		t: t,
		steps: []runnerStep{
			{call: runnerCall{name: "pgrep", args: []string{"-n", "-f", apiSock}}, err: errors.New("exit status 1")},
			sudoStep("", nil, "dmsetup", "remove", "fc-rootfs-corrupt"),
			sudoStep("", nil, "losetup", "-d", "/dev/loop21"),
			sudoStep("", nil, "losetup", "-d", "/dev/loop20"),
			sudoStep("", nil, "ip", "link", "del", "tap-fc-corrupt"),
		},
	}
	d := &Daemon{store: db, runner: runner}
	wireServices(d)

	if err := d.reconcile(ctx); err != nil {
		t.Fatalf("reconcile: %v", err)
	}
	runner.assertExhausted()

	got, err := db.GetVM(ctx, vm.ID)
	if err != nil {
		t.Fatalf("GetVM: %v", err)
	}
	if got.State != model.VMStateStopped || got.Runtime.State != model.VMStateStopped {
		t.Fatalf("vm state after reconcile = %s/%s, want stopped", got.State, got.Runtime.State)
	}
	if got.Runtime.TapDevice != "" || got.Runtime.BaseLoop != "" || got.Runtime.COWLoop != "" || got.Runtime.DMName != "" || got.Runtime.DMDev != "" {
		t.Fatalf("runtime teardown state not cleared after reconcile: %+v", got.Runtime)
	}
	if _, err := os.Stat(handlesFilePath(vmDir)); !os.IsNotExist(err) {
		t.Fatalf("handles.json still present after reconcile: %v", err)
	}
}

func TestRebuildDNSIncludesOnlyLiveRunningVMs(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)

	liveSock := filepath.Join(t.TempDir(), "live.sock")
	liveCmd := startFakeFirecrackerProcess(t, liveSock)
	t.Cleanup(func() {
		_ = liveCmd.Process.Kill()
		_ = liveCmd.Wait()
	})

	live := testVM("live", "image-live", "172.16.0.21")
	live.State = model.VMStateRunning
	live.Runtime.State = model.VMStateRunning
	live.Runtime.APISockPath = liveSock

	stale := testVM("stale", "image-stale", "172.16.0.22")
	stale.State = model.VMStateRunning
	stale.Runtime.State = model.VMStateRunning
	stale.Runtime.APISockPath = filepath.Join(t.TempDir(), "stale.sock")

	stopped := testVM("stopped", "image-stopped", "172.16.0.23")

	for _, vm := range []model.VMRecord{live, stale, stopped} {
		upsertDaemonVM(t, ctx, db, vm)
	}

	server, err := vmdns.New("127.0.0.1:0", nil)
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("vmdns.New: %v", err)
	}
	t.Cleanup(func() {
		if err := server.Close(); err != nil {
			t.Fatalf("server.Close: %v", err)
		}
	})

	d := &Daemon{store: db, net: &HostNetwork{vmDNS: server}}
	wireServices(d)
	// rebuildDNS reads the alive check from the handle cache. Seed
	// the live VM with its real PID; leave the stale entry with a PID
	// that definitely isn't running (999999 ≫ max PID on most hosts).
	d.vm.setVMHandlesInMemory(live.ID, model.VMHandles{PID: liveCmd.Process.Pid})
	d.vm.setVMHandlesInMemory(stale.ID, model.VMHandles{PID: 999999})
	if err := d.vm.rebuildDNS(ctx); err != nil {
		t.Fatalf("rebuildDNS: %v", err)
	}

	if _, ok := server.Lookup("live.vm"); !ok {
		t.Fatal("live.vm missing after rebuildDNS")
	}
	if _, ok := server.Lookup("stale.vm"); ok {
		t.Fatal("stale.vm should not be published")
	}
	if _, ok := server.Lookup("stopped.vm"); ok {
		t.Fatal("stopped.vm should not be published")
	}
}

func TestSetVMRejectsStoppedOnlyChangesForRunningVM(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "running.sock")
	cmd := startFakeFirecrackerProcess(t, apiSock)
	t.Cleanup(func() {
		_ = cmd.Process.Kill()
		_ = cmd.Wait()
	})

	vm := testVM("running", "image-run", "172.16.0.10")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	upsertDaemonVM(t, ctx, db, vm)

	d := &Daemon{store: db}
	wireServices(d)
	d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: cmd.Process.Pid})
	tests := []struct {
		name   string
		params api.VMSetParams
		want   string
	}{
		{
			name:   "vcpu",
			params: api.VMSetParams{IDOrName: vm.ID, VCPUCount: ptr(4)},
			want:   "vcpu changes require the VM to be stopped",
		},
		{
			name:   "memory",
			params: api.VMSetParams{IDOrName: vm.ID, MemoryMiB: ptr(2048)},
			want:   "memory changes require the VM to be stopped",
		},
		{
			name:   "disk",
			params: api.VMSetParams{IDOrName: vm.ID, WorkDiskSize: "16G"},
			want:   "disk changes require the VM to be stopped",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := d.vm.SetVM(ctx, tt.params)
			if err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("SetVM(%s) error = %v, want %q", tt.name, err, tt.want)
			}
		})
	}
}

func TestHealthVMReturnsHealthyForRunningGuest(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	fake := startFakeFirecrackerProcess(t, apiSock)
	t.Cleanup(func() {
		_ = fake.Process.Kill()
		_ = fake.Wait()
	})

	vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
	listener, err := net.Listen("unix", vsockSock)
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen vsock: %v", err)
	}
	t.Cleanup(func() {
		_ = listener.Close()
		_ = os.Remove(vsockSock)
	})
	serverDone := make(chan error, 1)
	go func() {
		conn, err := listener.Accept()
		if err != nil {
			serverDone <- err
			return
		}
		defer conn.Close()
		buf := make([]byte, 128)
		n, err := conn.Read(buf)
		if err != nil {
			serverDone <- err
			return
		}
		if got := string(buf[:n]); got != "CONNECT 42070\n" {
			serverDone <- fmt.Errorf("unexpected connect message %q", got)
			return
		}
		if _, err := conn.Write([]byte("OK 1\n")); err != nil {
			serverDone <- err
			return
		}
		reqBuf := make([]byte, 0, 512)
		reqBuf = append(reqBuf, buf[:0]...)
		for {
			n, err = conn.Read(buf)
			if err != nil {
				serverDone <- err
				return
			}
			reqBuf = append(reqBuf, buf[:n]...)
			if strings.Contains(string(reqBuf), "\r\n\r\n") {
				break
			}
		}
		if got := string(reqBuf); !strings.Contains(got, "GET /healthz HTTP/1.1\r\n") {
			serverDone <- fmt.Errorf("unexpected health payload %q", got)
			return
		}
		_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}"))
		serverDone <- err
	}()

	vm := testVM("alive", "image-alive", "172.16.0.41")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	vm.Runtime.VSockPath = vsockSock
	vm.Runtime.VSockCID = 10041
	upsertDaemonVM(t, ctx, db, vm)

	handlePID := fake.Process.Pid
	runner := &scriptedRunner{
		t: t,
		steps: []runnerStep{
			sudoStep("", nil, "chmod", "600", vsockSock),
			sudoStep("", nil, "chown", "-h", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
		},
	}
	d := &Daemon{store: db, runner: runner}
	wireServices(d)
	d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: handlePID})
	result, err := d.stats.HealthVM(ctx, vm.Name)
	if err != nil {
		t.Fatalf("HealthVM: %v", err)
	}
	if !result.Healthy || result.Name != vm.Name {
		t.Fatalf("HealthVM result = %+v, want healthy %s", result, vm.Name)
	}
	runner.assertExhausted()
	if err := <-serverDone; err != nil {
		t.Fatalf("server: %v", err)
	}
}

func TestPingVMAliasReturnsAliveForHealthyVM(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	fake := startFakeFirecrackerProcess(t, apiSock)
	t.Cleanup(func() {
		_ = fake.Process.Kill()
		_ = fake.Wait()
	})
	vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
	listener, err := net.Listen("unix", vsockSock)
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen vsock: %v", err)
	}
	t.Cleanup(func() {
		_ = listener.Close()
		_ = os.Remove(vsockSock)
	})
	go func() {
		conn, err := listener.Accept()
		if err != nil {
			return
		}
		defer conn.Close()
		buf := make([]byte, 512)
		_, _ = conn.Read(buf)
		_, _ = conn.Write([]byte("OK 1\n"))
		_, _ = conn.Read(buf)
		_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}"))
	}()
	vm := testVM("healthy-ping", "image-healthy", "172.16.0.42")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	vm.Runtime.VSockPath = vsockSock
	vm.Runtime.VSockCID = 10042
	upsertDaemonVM(t, ctx, db, vm)

	runner := &scriptedRunner{
		t: t,
		steps: []runnerStep{
			sudoStep("", nil, "chmod", "600", vsockSock),
			sudoStep("", nil, "chown", "-h", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
		},
	}
	d := &Daemon{store: db, runner: runner}
	wireServices(d)
	d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: fake.Process.Pid})
	result, err := d.stats.PingVM(ctx, vm.Name)
	if err != nil {
		t.Fatalf("PingVM: %v", err)
	}
	if !result.Alive {
		t.Fatalf("PingVM result = %+v, want alive", result)
	}
}

func TestWaitForGuestVSockAgentRetriesUntilHealthy(t *testing.T) {
	t.Parallel()

	socketPath := filepath.Join(t.TempDir(), "fc.vsock")
	listener, err := net.Listen("unix", socketPath)
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen vsock: %v", err)
	}
	t.Cleanup(func() {
		_ = listener.Close()
		_ = os.Remove(socketPath)
	})

	serverDone := make(chan error, 1)
	go func() {
		for attempt := 0; attempt < 2; attempt++ {
			conn, err := listener.Accept()
			if err != nil {
				serverDone <- err
				return
			}

			buf := make([]byte, 512)
			n, err := conn.Read(buf)
			if err != nil {
				_ = conn.Close()
				serverDone <- err
				return
			}
			if got := string(buf[:n]); got != "CONNECT 42070\n" {
				_ = conn.Close()
				serverDone <- fmt.Errorf("unexpected connect message %q", got)
				return
			}
			if _, err := conn.Write([]byte("OK 1\n")); err != nil {
				_ = conn.Close()
				serverDone <- err
				return
			}

			if attempt == 0 {
				_ = conn.Close()
				continue
			}

			reqBuf := make([]byte, 0, 512)
			for {
				n, err = conn.Read(buf)
				if err != nil {
					_ = conn.Close()
					serverDone <- err
					return
				}
				reqBuf = append(reqBuf, buf[:n]...)
				if strings.Contains(string(reqBuf), "\r\n\r\n") {
					break
				}
			}
			if got := string(reqBuf); !strings.Contains(got, "GET /healthz HTTP/1.1\r\n") {
				_ = conn.Close()
				serverDone <- fmt.Errorf("unexpected health payload %q", got)
				return
			}
			_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}"))
			_ = conn.Close()
			serverDone <- err
			return
		}
		serverDone <- errors.New("health probe did not retry")
	}()

	n := &HostNetwork{}
	if err := n.waitForGuestVSockAgent(context.Background(), socketPath, time.Second); err != nil {
		t.Fatalf("waitForGuestVSockAgent: %v", err)
	}
	if err := <-serverDone; err != nil {
		t.Fatalf("server: %v", err)
	}
}

func TestHealthVMReturnsFalseForStoppedVM(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	vm := testVM("stopped-ping", "image-stopped", "172.16.0.42")
	upsertDaemonVM(t, ctx, db, vm)

	d := &Daemon{store: db}
	wireServices(d)
	result, err := d.stats.HealthVM(ctx, vm.Name)
	if err != nil {
		t.Fatalf("HealthVM: %v", err)
	}
	if result.Healthy {
		t.Fatalf("HealthVM result = %+v, want not healthy", result)
	}
}

func TestPortsVMReturnsEnrichedPortsAndWebSchemes(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	fake := startFakeFirecrackerProcess(t, apiSock)
	t.Cleanup(func() {
		_ = fake.Process.Kill()
		_ = fake.Wait()
	})

	webAddr := startHTTPServerOnTCP4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	}))
	tlsAddr := startHTTPSServerOnTCP4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusAccepted)
	}))

	vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
	listener, err := net.Listen("unix", vsockSock)
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen vsock: %v", err)
	}
	t.Cleanup(func() {
		_ = listener.Close()
		_ = os.Remove(vsockSock)
	})
	serverDone := make(chan error, 1)
	go func() {
		conn, err := listener.Accept()
		if err != nil {
			serverDone <- err
			return
		}
		defer conn.Close()
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			serverDone <- err
			return
		}
		if got := string(buf[:n]); got != "CONNECT 42070\n" {
			serverDone <- fmt.Errorf("unexpected connect message %q", got)
			return
		}
		if _, err := conn.Write([]byte("OK 1\n")); err != nil {
			serverDone <- err
			return
		}
		reqBuf := make([]byte, 0, 1024)
		for {
			n, err = conn.Read(buf)
			if err != nil {
				serverDone <- err
				return
			}
			reqBuf = append(reqBuf, buf[:n]...)
			if strings.Contains(string(reqBuf), "\r\n\r\n") {
				break
			}
		}
		if got := string(reqBuf); !strings.Contains(got, "GET /ports HTTP/1.1\r\n") {
			serverDone <- fmt.Errorf("unexpected ports payload %q", got)
			return
		}
		body := fmt.Sprintf(`{"listeners":[{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":44,"process":"python3","command":"python3 -m http.server %d"},{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":77,"process":"caddy","command":"caddy run"},{"proto":"udp","bind_address":"0.0.0.0","port":53,"pid":1,"process":"dnsd","command":"dnsd --foreground"}]}`, webAddr.Port, webAddr.Port, tlsAddr.Port)
		resp := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
		_, err = conn.Write([]byte(resp))
		serverDone <- err
	}()

	vm := testVM("ports", "image-ports", "127.0.0.1")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	vm.Runtime.VSockPath = vsockSock
	vm.Runtime.VSockCID = 10043
	upsertDaemonVM(t, ctx, db, vm)

	runner := &scriptedRunner{
		t: t,
		steps: []runnerStep{
			sudoStep("", nil, "chmod", "600", vsockSock),
			sudoStep("", nil, "chown", "-h", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
		},
	}
	d := &Daemon{store: db, runner: runner}
	wireServices(d)
	d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: fake.Process.Pid})

	result, err := d.stats.PortsVM(ctx, vm.Name)
	if err != nil {
		t.Fatalf("PortsVM: %v", err)
	}
	if result.Name != vm.Name || result.DNSName != vm.Runtime.DNSName {
		t.Fatalf("result = %+v, want name/dns", result)
	}
	if len(result.Ports) != 3 {
		t.Fatalf("ports = %+v, want 3 entries", result.Ports)
	}
	wantHTTP := fmt.Sprintf("http://ports.vm:%d/", webAddr.Port)
	wantHTTPS := fmt.Sprintf("https://ports.vm:%d/", tlsAddr.Port)
	var httpPort, httpsPort, udpPort api.VMPort
	for _, port := range result.Ports {
		switch port.Port {
		case webAddr.Port:
			httpPort = port
		case tlsAddr.Port:
			httpsPort = port
		case 53:
			udpPort = port
		}
	}
	if udpPort.Endpoint != "ports.vm:53" {
		t.Fatalf("udp port = %+v, want endpoint only", udpPort)
	}
	if httpPort.Proto != "http" || httpPort.Endpoint != wantHTTP {
		t.Fatalf("http port = %+v, want http endpoint %q", httpPort, wantHTTP)
	}
	if httpsPort.Proto != "https" || httpsPort.Endpoint != wantHTTPS {
		t.Fatalf("https port = %+v, want https endpoint %q", httpsPort, wantHTTPS)
	}
	runner.assertExhausted()
	if err := <-serverDone; err != nil {
		t.Fatalf("server: %v", err)
	}
}

func TestPortsVMReturnsErrorForStoppedVM(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	vm := testVM("stopped-ports", "image-stopped", "172.16.0.50")
	upsertDaemonVM(t, ctx, db, vm)

	d := &Daemon{store: db}
	wireServices(d)
	_, err := d.stats.PortsVM(ctx, vm.Name)
	if err == nil || !strings.Contains(err.Error(), "is not running") {
		t.Fatalf("PortsVM error = %v, want not running", err)
	}
}

func TestBuildVMPortsDeduplicatesSameRenderedEndpoint(t *testing.T) {
	t.Parallel()

	vm := testVM("dedupe-ports", "image-ports", "")
	vm.Runtime.DNSName = "dedupe-ports.vm"

	ports := buildVMPorts(vm, []vsockagent.PortListener{
		{
			Proto:       "tcp",
			BindAddress: "0.0.0.0",
			Port:        8080,
			PID:         44,
			Process:     "docker-proxy",
			Command:     "/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080",
		},
		{
			Proto:       "tcp",
			BindAddress: "::",
			Port:        8080,
			PID:         45,
			Process:     "docker-proxy",
			Command:     "/usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080",
		},
		{
			Proto:       "udp",
			BindAddress: "0.0.0.0",
			Port:        8080,
			PID:         46,
			Process:     "dnsd",
			Command:     "dnsd --foreground",
		},
	})
	if len(ports) != 2 {
		t.Fatalf("ports = %+v, want tcp+udp entries after dedupe", ports)
	}
	if ports[0].Proto != "tcp" || ports[0].Endpoint != "dedupe-ports.vm:8080" {
		t.Fatalf("first port = %+v, want deduped tcp endpoint", ports[0])
	}
	if ports[1].Proto != "udp" || ports[1].Endpoint != "dedupe-ports.vm:8080" {
		t.Fatalf("second port = %+v, want distinct udp endpoint", ports[1])
	}
}

func TestSetVMDiskResizeFailsPreflightWhenToolsMissing(t *testing.T) {
	ctx := context.Background()
	db := openDaemonStore(t)
	workDisk := filepath.Join(t.TempDir(), "root.ext4")
	if err := os.WriteFile(workDisk, []byte("disk"), 0o644); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}
	vm := testVM("resize", "image-resize", "172.16.0.11")
	vm.Runtime.WorkDiskPath = workDisk
	vm.Spec.WorkDiskSizeBytes = 8 * 1024 * 1024 * 1024
	upsertDaemonVM(t, ctx, db, vm)

	t.Setenv("PATH", t.TempDir())
	d := &Daemon{store: db}
	wireServices(d)
	_, err := d.vm.SetVM(ctx, api.VMSetParams{IDOrName: vm.ID, WorkDiskSize: "16G"})
	if err == nil || !strings.Contains(err.Error(), "work disk resize preflight failed") {
		t.Fatalf("SetVM() error = %v, want preflight failure", err)
	}
}

func TestEnsureGitIdentityOnWorkDiskCopiesHostGlobalIdentity(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not installed")
	}

	hostConfigPath := filepath.Join(t.TempDir(), "host.gitconfig")
	t.Setenv("GIT_CONFIG_GLOBAL", hostConfigPath)
	testSetGitConfig(t, "user.name", "Banger Host")
	testSetGitConfig(t, "user.email", "host@example.com")

	workDiskDir := t.TempDir()
	d := &Daemon{runner: &filesystemRunner{t: t}}
	wireServices(d)
	vm := testVM("git-identity", "image-git-identity", "172.16.0.67")
	vm.Runtime.WorkDiskPath = workDiskDir

	if err := d.ws.ensureGitIdentityOnWorkDisk(context.Background(), &vm); err != nil {
		t.Fatalf("ensureGitIdentityOnWorkDisk: %v", err)
	}

	guestConfigPath := filepath.Join(workDiskDir, workDiskGitConfigRelativePath)
	if got := testGitConfigValue(t, guestConfigPath, "user.name"); got != "Banger Host" {
		t.Fatalf("guest user.name = %q, want Banger Host", got)
	}
	if got := testGitConfigValue(t, guestConfigPath, "user.email"); got != "host@example.com" {
		t.Fatalf("guest user.email = %q, want host@example.com", got)
	}
}

func TestEnsureGitIdentityOnWorkDiskPreservesExistingGuestConfig(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not installed")
	}

	hostConfigPath := filepath.Join(t.TempDir(), "host.gitconfig")
	t.Setenv("GIT_CONFIG_GLOBAL", hostConfigPath)
	testSetGitConfig(t, "user.name", "Fresh Name")
	testSetGitConfig(t, "user.email", "fresh@example.com")

	workDiskDir := t.TempDir()
	guestConfigPath := filepath.Join(workDiskDir, workDiskGitConfigRelativePath)
	if err := os.WriteFile(guestConfigPath, []byte("[safe]\n\tdirectory = /root/repo\n[user]\n\tname = stale\n"), 0o644); err != nil {
		t.Fatalf("WriteFile(guest .gitconfig): %v", err)
	}

	d := &Daemon{runner: &filesystemRunner{t: t}}
	wireServices(d)
	vm := testVM("git-identity-preserve", "image-git-identity", "172.16.0.68")
	vm.Runtime.WorkDiskPath = workDiskDir

	if err := d.ws.ensureGitIdentityOnWorkDisk(context.Background(), &vm); err != nil {
		t.Fatalf("ensureGitIdentityOnWorkDisk: %v", err)
	}

	if got := testGitConfigValue(t, guestConfigPath, "user.name"); got != "Fresh Name" {
		t.Fatalf("guest user.name = %q, want Fresh Name", got)
	}
	if got := testGitConfigValue(t, guestConfigPath, "user.email"); got != "fresh@example.com" {
		t.Fatalf("guest user.email = %q, want fresh@example.com", got)
	}
	if got := testGitConfigValue(t, guestConfigPath, "safe.directory"); got != "/root/repo" {
		t.Fatalf("guest safe.directory = %q, want /root/repo", got)
	}
}

func TestEnsureGitIdentityOnWorkDiskWarnsAndSkipsWhenHostIdentityIncomplete(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not installed")
	}

	hostConfigPath := filepath.Join(t.TempDir(), "host.gitconfig")
	t.Setenv("GIT_CONFIG_GLOBAL", hostConfigPath)
	testSetGitConfig(t, "user.name", "Only Name")

	workDiskDir := t.TempDir()
	guestConfigPath := filepath.Join(workDiskDir, workDiskGitConfigRelativePath)
	original := []byte("[user]\n\temail = keep@example.com\n")
	if err := os.WriteFile(guestConfigPath, original, 0o644); err != nil {
		t.Fatalf("WriteFile(guest .gitconfig): %v", err)
	}

	var buf bytes.Buffer
	logger, _, err := newDaemonLogger(&buf, "info")
	if err != nil {
		t.Fatalf("newDaemonLogger: %v", err)
	}

	d := &Daemon{
		runner: &filesystemRunner{t: t},
		logger: logger,
	}
	wireServices(d)
	vm := testVM("git-identity-missing", "image-git-identity", "172.16.0.69")
	vm.Runtime.WorkDiskPath = workDiskDir

	if err := d.ws.ensureGitIdentityOnWorkDisk(context.Background(), &vm); err != nil {
		t.Fatalf("ensureGitIdentityOnWorkDisk: %v", err)
	}

	got, err := os.ReadFile(guestConfigPath)
	if err != nil {
		t.Fatalf("ReadFile(guest .gitconfig): %v", err)
	}
	if string(got) != string(original) {
		t.Fatalf("guest .gitconfig = %q, want preserved %q", string(got), string(original))
	}

	entries := parseLogEntries(t, buf.Bytes())
	if !hasLogEntry(entries, map[string]string{
		"msg":     "guest git identity sync skipped",
		"vm_name": vm.Name,
		"source":  hostGlobalGitIdentitySource,
		"error":   "host git user.email is empty",
	}) {
		t.Fatalf("expected warn log, got %v", entries)
	}
}

func TestRunFileSyncNoOpWhenConfigEmpty(t *testing.T) {
	d := &Daemon{runner: &filesystemRunner{t: t}}
	wireServices(d)
	vm := testVM("no-sync", "image", "172.16.0.70")
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}
}

func TestRunFileSyncCopiesFile(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	srcPath := filepath.Join(homeDir, ".secrets", "token")
	if err := os.MkdirAll(filepath.Dir(srcPath), 0o755); err != nil {
		t.Fatal(err)
	}
	srcData := []byte(`{"token":"abc"}`)
	if err := os.WriteFile(srcPath, srcData, 0o600); err != nil {
		t.Fatal(err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		config: model.DaemonConfig{
			FileSync: []model.FileSyncEntry{
				{Host: "~/.secrets/token", Guest: "~/.secrets/token"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-file", "image", "172.16.0.71")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	dst := filepath.Join(workDisk, ".secrets", "token")
	got, err := os.ReadFile(dst)
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != string(srcData) {
		t.Fatalf("dst = %q, want %q", got, srcData)
	}
	info, err := os.Stat(dst)
	if err != nil {
		t.Fatal(err)
	}
	if info.Mode().Perm() != 0o600 {
		t.Fatalf("mode = %v, want 0600", info.Mode().Perm())
	}
}

func TestRunFileSyncRespectsCustomMode(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	srcPath := filepath.Join(homeDir, "script")
	if err := os.WriteFile(srcPath, []byte("#!/bin/sh\nexit 0\n"), 0o600); err != nil {
		t.Fatal(err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		config: model.DaemonConfig{
			FileSync: []model.FileSyncEntry{
				{Host: "~/script", Guest: "~/bin/my-script", Mode: "0755"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-mode", "image", "172.16.0.72")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	info, err := os.Stat(filepath.Join(workDisk, "bin", "my-script"))
	if err != nil {
		t.Fatal(err)
	}
	if info.Mode().Perm() != 0o755 {
		t.Fatalf("mode = %v, want 0755", info.Mode().Perm())
	}
}

func TestRunFileSyncSkipsMissingHostPath(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)

	var buf bytes.Buffer
	logger, _, err := newDaemonLogger(&buf, "info")
	if err != nil {
		t.Fatal(err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		logger: logger,
		config: model.DaemonConfig{
			FileSync: []model.FileSyncEntry{
				{Host: "~/does-not-exist", Guest: "~/wherever"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-missing", "image", "172.16.0.73")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	entries := parseLogEntries(t, buf.Bytes())
	if !hasLogEntry(entries, map[string]string{
		"msg":       "file_sync skipped",
		"vm_name":   vm.Name,
		"host_path": filepath.Join(homeDir, "does-not-exist"),
	}) {
		t.Fatalf("expected skipped log, got %v", entries)
	}
}

func TestRunFileSyncOverwritesExistingGuestFile(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	srcPath := filepath.Join(homeDir, "token")
	if err := os.WriteFile(srcPath, []byte("fresh"), 0o600); err != nil {
		t.Fatal(err)
	}
	workDisk := t.TempDir()
	// Work disk is mounted at /root in the guest, so the guest path
	// "/root/token" maps to workDisk/token here.
	existing := filepath.Join(workDisk, "token")
	if err := os.WriteFile(existing, []byte("stale"), 0o600); err != nil {
		t.Fatal(err)
	}

	d := &Daemon{
		runner: &filesystemRunner{t: t},
		config: model.DaemonConfig{
			FileSync: []model.FileSyncEntry{
				{Host: "~/token", Guest: "/root/token"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-overwrite", "image", "172.16.0.74")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	got, err := os.ReadFile(existing)
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "fresh" {
		t.Fatalf("guest file = %q, want fresh", got)
	}
}

func TestRunFileSyncCopiesDirectoryRecursively(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	srcDir := filepath.Join(homeDir, ".aws")
	if err := os.MkdirAll(srcDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(srcDir, "credentials"), []byte("access"), 0o600); err != nil {
		t.Fatal(err)
	}
	sub := filepath.Join(srcDir, "sso", "cache")
	if err := os.MkdirAll(sub, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(sub, "token.json"), []byte("sso-token"), 0o600); err != nil {
		t.Fatal(err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		config: model.DaemonConfig{
			FileSync: []model.FileSyncEntry{
				{Host: "~/.aws", Guest: "~/.aws"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-dir", "image", "172.16.0.75")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	creds, err := os.ReadFile(filepath.Join(workDisk, ".aws", "credentials"))
	if err != nil {
		t.Fatal(err)
	}
	if string(creds) != "access" {
		t.Fatalf("credentials = %q, want access", creds)
	}
	ssoToken, err := os.ReadFile(filepath.Join(workDisk, ".aws", "sso", "cache", "token.json"))
	if err != nil {
		t.Fatal(err)
	}
	if string(ssoToken) != "sso-token" {
		t.Fatalf("sso token = %q, want sso-token", ssoToken)
	}
}

func TestRunFileSyncAllowsTopLevelSymlinkWithinHome(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)

	targetDir := filepath.Join(homeDir, ".config", "gh")
	if err := os.MkdirAll(targetDir, 0o755); err != nil {
		t.Fatal(err)
	}
	targetPath := filepath.Join(targetDir, "hosts.yml")
	if err := os.WriteFile(targetPath, []byte("github.com"), 0o600); err != nil {
		t.Fatal(err)
	}
	linkPath := filepath.Join(homeDir, "gh-hosts.yml")
	if err := os.Symlink(targetPath, linkPath); err != nil {
		t.Skipf("symlink unsupported on this filesystem: %v", err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		config: model.DaemonConfig{
			HostHomeDir: homeDir,
			FileSync: []model.FileSyncEntry{
				{Host: "~/gh-hosts.yml", Guest: "~/.config/gh/hosts.yml"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-top-level-symlink-ok", "image", "172.16.0.77")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	got, err := os.ReadFile(filepath.Join(workDisk, ".config", "gh", "hosts.yml"))
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "github.com" {
		t.Fatalf("guest file = %q, want github.com", got)
	}
}

func TestRunFileSyncRejectsTopLevelSymlinkOutsideHome(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)

	outsideDir := t.TempDir()
	targetPath := filepath.Join(outsideDir, "secret.txt")
	if err := os.WriteFile(targetPath, []byte("must-stay-outside"), 0o600); err != nil {
		t.Fatal(err)
	}
	linkPath := filepath.Join(homeDir, "secret-link")
	if err := os.Symlink(targetPath, linkPath); err != nil {
		t.Skipf("symlink unsupported on this filesystem: %v", err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		config: model.DaemonConfig{
			HostHomeDir: homeDir,
			FileSync: []model.FileSyncEntry{
				{Host: "~/secret-link", Guest: "~/secret.txt"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-top-level-symlink-reject", "image", "172.16.0.78")
	vm.Runtime.WorkDiskPath = workDisk
	err := d.ws.runFileSync(context.Background(), &vm)
	if err == nil || !strings.Contains(err.Error(), "owner home") {
		t.Fatalf("runFileSync error = %v, want owner-home rejection", err)
	}
	if _, statErr := os.Stat(filepath.Join(workDisk, "secret.txt")); !os.IsNotExist(statErr) {
		t.Fatalf("guest file exists after rejected sync (stat err = %v)", statErr)
	}
}

// TestRunFileSyncSkipsNestedSymlinks pins the anti-sprawl contract:
// a symlink INSIDE a synced directory is not followed, even if the
// target holds real files. Without this, a user syncing ~/.aws with
// a ~/.aws/session -> ~/other-creds symlink would copy the unrelated
// creds into the guest. Top-level entries are resolved separately:
// they may still follow, but only when the real target stays under
// the configured owner home.
func TestRunFileSyncSkipsNestedSymlinks(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)

	// Target the user DID NOT name — lives outside the synced tree.
	outsideDir := filepath.Join(homeDir, "other-creds")
	if err := os.MkdirAll(outsideDir, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(outsideDir, "leaked.txt"), []byte("must-not-escape"), 0o600); err != nil {
		t.Fatal(err)
	}

	// The synced directory.
	srcDir := filepath.Join(homeDir, ".aws")
	if err := os.MkdirAll(srcDir, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(srcDir, "credentials"), []byte("access"), 0o600); err != nil {
		t.Fatal(err)
	}
	// File symlink inside .aws pointing OUT of the tree.
	if err := os.Symlink(filepath.Join(outsideDir, "leaked.txt"), filepath.Join(srcDir, "session")); err != nil {
		t.Skipf("symlink unsupported on this filesystem: %v", err)
	}
	// Directory symlink inside .aws pointing OUT of the tree — must
	// not be recursed into.
	if err := os.Symlink(outsideDir, filepath.Join(srcDir, "linked-dir")); err != nil {
		t.Fatal(err)
	}

	var buf bytes.Buffer
	logger, _, err := newDaemonLogger(&buf, "info")
	if err != nil {
		t.Fatal(err)
	}

	workDisk := t.TempDir()
	d := &Daemon{
		runner: &filesystemRunner{t: t},
		logger: logger,
		config: model.DaemonConfig{
			FileSync: []model.FileSyncEntry{
				{Host: "~/.aws", Guest: "~/.aws"},
			},
		},
	}
	wireServices(d)
	vm := testVM("sync-symlink", "image", "172.16.0.76")
	vm.Runtime.WorkDiskPath = workDisk
	if err := d.ws.runFileSync(context.Background(), &vm); err != nil {
		t.Fatalf("runFileSync: %v", err)
	}

	// The real file inside the tree must copy.
	creds, err := os.ReadFile(filepath.Join(workDisk, ".aws", "credentials"))
	if err != nil {
		t.Fatalf("credentials not copied: %v", err)
	}
	if string(creds) != "access" {
		t.Fatalf("credentials = %q, want access", creds)
	}

	// Neither the file symlink nor anything reached through the
	// directory symlink should have been materialised in the guest
	// path.
	for _, shouldNotExist := range []string{
		filepath.Join(workDisk, ".aws", "session"),
		filepath.Join(workDisk, ".aws", "linked-dir"),
		filepath.Join(workDisk, ".aws", "linked-dir", "leaked.txt"),
	} {
		if _, err := os.Stat(shouldNotExist); !os.IsNotExist(err) {
			t.Fatalf("symlinked path %s was materialised in guest tree (stat err = %v); secret leakage path open", shouldNotExist, err)
		}
	}

	// Each skipped symlink must be warned.
	entries := parseLogEntries(t, buf.Bytes())
	for _, want := range []string{
		filepath.Join(srcDir, "session"),
		filepath.Join(srcDir, "linked-dir"),
	} {
		if !hasLogEntry(entries, map[string]string{
			"msg":       "file_sync skipped symlink (would escape the requested tree)",
			"vm_name":   vm.Name,
			"host_path": want,
		}) {
			t.Fatalf("expected warn log for skipped symlink %s; got %v", want, entries)
		}
	}
}

func TestCreateVMRejectsNonPositiveCPUAndMemory(t *testing.T) {
	d := &Daemon{}
	wireServices(d)
	if _, err := d.vm.CreateVM(context.Background(), api.VMCreateParams{VCPUCount: ptr(0)}); err == nil || !strings.Contains(err.Error(), "vcpu must be a positive integer") {
		t.Fatalf("CreateVM(vcpu=0) error = %v", err)
	}
	if _, err := d.vm.CreateVM(context.Background(), api.VMCreateParams{MemoryMiB: ptr(-1)}); err == nil || !strings.Contains(err.Error(), "memory must be a positive integer") {
		t.Fatalf("CreateVM(memory=-1) error = %v", err)
	}
}

func TestBeginVMCreateCompletesAndReturnsStatus(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	image := testImage("default")
	image.ID = "default-image-id"
	image.Name = "default"
	if err := db.UpsertImage(ctx, image); err != nil {
		t.Fatalf("UpsertImage: %v", err)
	}

	d := &Daemon{
		store: db,
		layout: paths.Layout{
			VMsDir: t.TempDir(),
		},
		config: model.DaemonConfig{
			DefaultImageName: image.Name,
			BridgeIP:         model.DefaultBridgeIP,
		},
	}
	wireServices(d)

	op, err := d.vm.BeginVMCreate(ctx, api.VMCreateParams{Name: "queued", NoStart: true})
	if err != nil {
		t.Fatalf("BeginVMCreate: %v", err)
	}
	if op.ID == "" {
		t.Fatal("operation id should be populated")
	}

	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		status, err := d.vm.VMCreateStatus(ctx, op.ID)
		if err != nil {
			t.Fatalf("VMCreateStatus: %v", err)
		}
		if !status.Done {
			time.Sleep(10 * time.Millisecond)
			continue
		}
		if !status.Success {
			t.Fatalf("status = %+v, want success", status)
		}
		if status.VM == nil || status.VM.Name != "queued" {
			t.Fatalf("status VM = %+v, want queued vm", status.VM)
		}
		if status.VM.State != model.VMStateStopped {
			t.Fatalf("status VM state = %s, want stopped", status.VM.State)
		}
		return
	}
	t.Fatal("vm create operation did not finish before timeout")
}

func TestCreateVMUsesDefaultsWhenCPUAndMemoryOmitted(t *testing.T) {
	ctx := context.Background()
	db := openDaemonStore(t)
	image := testImage("default")
	if err := db.UpsertImage(ctx, image); err != nil {
		t.Fatalf("UpsertImage: %v", err)
	}
	d := &Daemon{
		store: db,
		layout: paths.Layout{
			VMsDir: t.TempDir(),
		},
		config: model.DaemonConfig{
			DefaultImageName: image.Name,
			BridgeIP:         model.DefaultBridgeIP,
		},
	}
	wireServices(d)

	vm, err := d.vm.CreateVM(ctx, api.VMCreateParams{Name: "defaults", ImageName: image.Name, NoStart: true})
	if err != nil {
		t.Fatalf("CreateVM: %v", err)
	}
	if vm.Spec.VCPUCount != model.DefaultVCPUCount {
		t.Fatalf("VCPUCount = %d, want %d", vm.Spec.VCPUCount, model.DefaultVCPUCount)
	}
	if vm.Spec.MemoryMiB != model.DefaultMemoryMiB {
		t.Fatalf("MemoryMiB = %d, want %d", vm.Spec.MemoryMiB, model.DefaultMemoryMiB)
	}
}

func TestSetVMRejectsNonPositiveCPUAndMemory(t *testing.T) {
	ctx := context.Background()
	db := openDaemonStore(t)
	vm := testVM("validate", "image-validate", "172.16.0.13")
	upsertDaemonVM(t, ctx, db, vm)
	d := &Daemon{store: db}
	wireServices(d)

	if _, err := d.vm.SetVM(ctx, api.VMSetParams{IDOrName: vm.ID, VCPUCount: ptr(0)}); err == nil || !strings.Contains(err.Error(), "vcpu must be a positive integer") {
		t.Fatalf("SetVM(vcpu=0) error = %v", err)
	}
	if _, err := d.vm.SetVM(ctx, api.VMSetParams{IDOrName: vm.ID, MemoryMiB: ptr(0)}); err == nil || !strings.Contains(err.Error(), "memory must be a positive integer") {
		t.Fatalf("SetVM(memory=0) error = %v", err)
	}
}

func TestCollectStatsIgnoresMalformedMetricsFile(t *testing.T) {
	t.Parallel()

	overlay := filepath.Join(t.TempDir(), "system.cow")
	workDisk := filepath.Join(t.TempDir(), "root.ext4")
	metrics := filepath.Join(t.TempDir(), "metrics.json")
	for _, path := range []string{overlay, workDisk} {
		if err := os.WriteFile(path, []byte("allocated"), 0o644); err != nil {
			t.Fatalf("WriteFile(%s): %v", path, err)
		}
	}
	if err := os.WriteFile(metrics, []byte("{not-json}\n"), 0o644); err != nil {
		t.Fatalf("WriteFile(metrics): %v", err)
	}

	d := &Daemon{}
	wireServices(d)
	stats, err := d.stats.collectStats(context.Background(), model.VMRecord{
		Runtime: model.VMRuntime{
			SystemOverlay: overlay,
			WorkDiskPath:  workDisk,
			MetricsPath:   metrics,
		},
	})
	if err != nil {
		t.Fatalf("collectStats: %v", err)
	}
	if stats.MetricsRaw != nil {
		t.Fatalf("MetricsRaw = %v, want nil for malformed metrics", stats.MetricsRaw)
	}
	if stats.SystemOverlayBytes == 0 || stats.WorkDiskBytes == 0 {
		t.Fatalf("allocated bytes not captured: %+v", stats)
	}
}

func TestValidateStartPrereqsReportsNATUplinkFailure(t *testing.T) {
	ctx := context.Background()
	binDir := t.TempDir()
	for _, name := range []string{
		"sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps",
		"chown", "chmod", "kill", "e2cp", "e2rm", "debugfs", "mkfs.ext4", "mount",
		"umount", "cp", "iptables", "sysctl",
	} {
		writeFakeExecutable(t, filepath.Join(binDir, name))
	}
	t.Setenv("PATH", binDir)

	firecrackerBin := filepath.Join(t.TempDir(), "firecracker")
	rootfsPath := filepath.Join(t.TempDir(), "rootfs.ext4")
	kernelPath := filepath.Join(t.TempDir(), "vmlinux")
	for _, path := range []string{firecrackerBin, rootfsPath, kernelPath} {
		writeFakeExecutable(t, path)
	}

	runner := &scriptedRunner{
		t: t,
		steps: []runnerStep{
			{call: runnerCall{name: "ip", args: []string{"route", "show", "default"}}, out: []byte("10.0.0.0/24 dev br-fc\n")},
		},
	}
	d := &Daemon{
		runner: runner,
		config: model.DaemonConfig{
			FirecrackerBin: firecrackerBin,
		},
	}
	wireServices(d)
	vm := testVM("nat", "image-nat", "172.16.0.12")
	vm.Spec.NATEnabled = true
	vm.Runtime.WorkDiskPath = filepath.Join(t.TempDir(), "missing-root.ext4")
	image := testImage("image-nat")
	image.RootfsPath = rootfsPath
	image.KernelPath = kernelPath

	err := d.vm.validateStartPrereqs(ctx, vm, image)
	if err == nil || !strings.Contains(err.Error(), "uplink interface for NAT") {
		t.Fatalf("validateStartPrereqs() error = %v, want NAT uplink failure", err)
	}
	runner.assertExhausted()
}

func TestCleanupRuntimeRediscoversLiveFirecrackerPID(t *testing.T) {
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	fake := startFakeFirecrackerProcess(t, apiSock)
	t.Cleanup(func() {
		if fake.ProcessState == nil || !fake.ProcessState.Exited() {
			_ = fake.Process.Kill()
			_ = fake.Wait()
		}
	})

	runner := &processKillingRunner{
		scriptedRunner: &scriptedRunner{
			t: t,
			steps: []runnerStep{
				{call: runnerCall{name: "pgrep", args: []string{"-n", "-f", apiSock}}, out: []byte(strconv.Itoa(fake.Process.Pid) + "\n")},
				sudoStep("", nil, "kill", "-KILL", strconv.Itoa(fake.Process.Pid)),
			},
		},
		proc: fake,
	}
	d := &Daemon{runner: runner}
	wireServices(d)
	vm := testVM("cleanup", "image-cleanup", "172.16.0.22")
	vm.Runtime.APISockPath = apiSock
	// Seed a stale PID so cleanupRuntime's findFirecrackerPID pgrep
	// fallback wins — it rediscovers fake.Process.Pid from apiSock.
	d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: fake.Process.Pid + 999})

	if err := d.vm.cleanupRuntime(context.Background(), vm, true); err != nil {
		t.Fatalf("cleanupRuntime returned error: %v", err)
	}
	runner.assertExhausted()
	if systemProcessRunning(fake.Process.Pid, apiSock) {
		t.Fatalf("fake firecracker pid %d still looks running", fake.Process.Pid)
	}
}

func TestDeleteStoppedNATVMDoesNotFailWithoutTapDevice(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := openDaemonStore(t)
	vmDir := filepath.Join(t.TempDir(), "stopped-nat-vm")
	if err := os.MkdirAll(vmDir, 0o755); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}

	vm := testVM("stopped-nat", "image-stopped-nat", "172.16.0.24")
	vm.Spec.NATEnabled = true
	vm.Runtime.VMDir = vmDir
	vm.State = model.VMStateStopped
	vm.Runtime.State = model.VMStateStopped
	upsertDaemonVM(t, ctx, db, vm)

	d := &Daemon{store: db}
	wireServices(d)
	deleted, err := d.vm.DeleteVM(ctx, vm.Name)
	if err != nil {
		t.Fatalf("DeleteVM: %v", err)
	}
	if deleted.ID != vm.ID {
		t.Fatalf("deleted VM = %+v, want %s", deleted, vm.ID)
	}
	if _, err := db.GetVMByID(ctx, vm.ID); err == nil {
		t.Fatal("expected VM record to be deleted")
	}
	if _, err := os.Stat(vmDir); !os.IsNotExist(err) {
		t.Fatalf("vm dir still exists or stat failed: %v", err)
	}
}

func TestStopVMSIGKILLsWhenSSHUnreachable(t *testing.T) {
	ctx := context.Background()
	db := openDaemonStore(t)
	apiSock := filepath.Join(t.TempDir(), "fc.sock")
	startFakeFirecrackerAPI(t, apiSock)

	fake := startFakeFirecrackerProcess(t, apiSock)
	t.Cleanup(func() {
		if fake.ProcessState == nil || !fake.ProcessState.Exited() {
			_ = fake.Process.Kill()
			_ = fake.Wait()
		}
	})

	vm := testVM("stubborn", "image-stubborn", "172.16.0.23")
	vm.State = model.VMStateRunning
	vm.Runtime.State = model.VMStateRunning
	vm.Runtime.APISockPath = apiSock
	upsertDaemonVM(t, ctx, db, vm)

	runner := &processKillingRunner{
		scriptedRunner: &scriptedRunner{
			t: t,
			steps: []runnerStep{
				{call: runnerCall{name: "pgrep", args: []string{"-n", "-f", apiSock}}, out: []byte(strconv.Itoa(fake.Process.Pid) + "\n")},
				sudoStep("", nil, "kill", "-KILL", strconv.Itoa(fake.Process.Pid)),
			},
		},
		proc: fake,
	}
	d := &Daemon{store: db, runner: runner}
	wireServices(d)
	d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: fake.Process.Pid})

	got, err := d.vm.StopVM(ctx, vm.ID)
	if err != nil {
		t.Fatalf("StopVM returned error: %v", err)
	}
	runner.assertExhausted()
	if got.State != model.VMStateStopped || got.Runtime.State != model.VMStateStopped {
		t.Fatalf("StopVM state = %s/%s, want stopped", got.State, got.Runtime.State)
	}
	// APISockPath + VSock paths are deterministic — they stay on the
	// record for debugging and next-start reuse even after stop. The
	// post-stop invariant is that the in-memory cache is empty.
	if h, ok := d.vm.handles.get(vm.ID); ok && !h.IsZero() {
		t.Fatalf("handle cache not cleared: %+v", h)
	}
}

func TestWithVMLockByIDSerializesSameVM(t *testing.T) {
	ctx := context.Background()
	db := openDaemonStore(t)
	vm := testVM("serial", "image-serial", "172.16.0.30")
	upsertDaemonVM(t, ctx, db, vm)
	d := &Daemon{store: db}
	wireServices(d)

	firstEntered := make(chan struct{})
	releaseFirst := make(chan struct{})
	secondEntered := make(chan struct{})
	errCh := make(chan error, 2)

	go func() {
		_, err := d.vm.withVMLockByID(ctx, vm.ID, func(vm model.VMRecord) (model.VMRecord, error) {
			close(firstEntered)
			<-releaseFirst
			return vm, nil
		})
		errCh <- err
	}()

	select {
	case <-firstEntered:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("first lock holder did not enter")
	}

	go func() {
		_, err := d.vm.withVMLockByID(ctx, vm.ID, func(vm model.VMRecord) (model.VMRecord, error) {
			close(secondEntered)
			return vm, nil
		})
		errCh <- err
	}()

	select {
	case <-secondEntered:
		t.Fatal("second same-vm lock holder entered before release")
	case <-time.After(150 * time.Millisecond):
	}

	close(releaseFirst)

	select {
	case <-secondEntered:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("second same-vm lock holder never entered")
	}

	for i := 0; i < 2; i++ {
		if err := <-errCh; err != nil {
			t.Fatalf("withVMLockByID returned error: %v", err)
		}
	}
}

func TestWithVMLockByIDAllowsDifferentVMsConcurrently(t *testing.T) {
	ctx := context.Background()
	db := openDaemonStore(t)
	vmA := testVM("alpha-lock", "image-alpha", "172.16.0.31")
	vmB := testVM("bravo-lock", "image-bravo", "172.16.0.32")
	for _, vm := range []model.VMRecord{vmA, vmB} {
		upsertDaemonVM(t, ctx, db, vm)
	}
	d := &Daemon{store: db}
	wireServices(d)

	started := make(chan string, 2)
	release := make(chan struct{})
	errCh := make(chan error, 2)
	run := func(id string) {
		_, err := d.vm.withVMLockByID(ctx, id, func(vm model.VMRecord) (model.VMRecord, error) {
			started <- vm.ID
			<-release
			return vm, nil
		})
		errCh <- err
	}

	go run(vmA.ID)
	go run(vmB.ID)

	for i := 0; i < 2; i++ {
		select {
		case <-started:
		case <-time.After(500 * time.Millisecond):
			t.Fatal("different VM locks did not overlap")
		}
	}

	close(release)

	for i := 0; i < 2; i++ {
		if err := <-errCh; err != nil {
			t.Fatalf("withVMLockByID returned error: %v", err)
		}
	}
}

func openDaemonStore(t *testing.T) *store.Store {
	t.Helper()
	db, err := store.Open(filepath.Join(t.TempDir(), "state.db"))
	if err != nil {
		t.Fatalf("store.Open: %v", err)
	}
	t.Cleanup(func() {
		_ = db.Close()
	})
	return db
}

func upsertDaemonVM(t *testing.T, ctx context.Context, db *store.Store, vm model.VMRecord) {
	t.Helper()
	image := testImage(vm.ImageID)
	image.ID = vm.ImageID
	image.Name = vm.ImageID
	if err := db.UpsertImage(ctx, image); err != nil {
		t.Fatalf("UpsertImage(%s): %v", image.Name, err)
	}
	if err := db.UpsertVM(ctx, vm); err != nil {
		t.Fatalf("UpsertVM(%s): %v", vm.Name, err)
	}
}

func testVM(name, imageID, guestIP string) model.VMRecord {
	now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
	return model.VMRecord{
		ID:            name + "-id",
		Name:          name,
		ImageID:       imageID,
		State:         model.VMStateStopped,
		CreatedAt:     now,
		UpdatedAt:     now,
		LastTouchedAt: now,
		Spec: model.VMSpec{
			VCPUCount:             2,
			MemoryMiB:             1024,
			SystemOverlaySizeByte: model.DefaultSystemOverlaySize,
			WorkDiskSizeBytes:     model.DefaultWorkDiskSize,
		},
		Runtime: model.VMRuntime{
			State:         model.VMStateStopped,
			GuestIP:       guestIP,
			DNSName:       name + ".vm",
			VMDir:         filepath.Join("/state", name),
			SystemOverlay: filepath.Join("/state", name, "system.cow"),
			WorkDiskPath:  filepath.Join("/state", name, "root.ext4"),
		},
	}
}

func testImage(name string) model.Image {
	now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
	return model.Image{
		ID:         name + "-id",
		Name:       name,
		RootfsPath: filepath.Join("/images", name+".ext4"),
		KernelPath: filepath.Join("/kernels", name),
		CreatedAt:  now,
		UpdatedAt:  now,
	}
}

func TestMergeAuthorizedKey(t *testing.T) {
	t.Parallel()

	managed := []byte("ssh-ed25519 AAAATESTKEY banger\n")
	existing := []byte("ssh-ed25519 AAAAOTHER other\n")
	merged := mergeAuthorizedKey(existing, managed)
	got := string(merged)
	if !strings.Contains(got, "ssh-ed25519 AAAAOTHER other") {
		t.Fatalf("merged keys dropped existing entry: %q", got)
	}
	if !strings.Contains(got, "ssh-ed25519 AAAATESTKEY banger") {
		t.Fatalf("merged keys missing managed entry: %q", got)
	}
	if strings.Count(got, "ssh-ed25519 AAAATESTKEY banger") != 1 {
		t.Fatalf("managed key duplicated in %q", got)
	}

	merged = mergeAuthorizedKey(merged, managed)
	if strings.Count(string(merged), "ssh-ed25519 AAAATESTKEY banger") != 1 {
		t.Fatalf("managed key duplicated after second merge: %q", string(merged))
	}
}

func startFakeFirecrackerProcess(t *testing.T, apiSock string) *exec.Cmd {
	t.Helper()

	cmd := exec.Command("bash", "-lc", fmt.Sprintf("exec -a %q sleep 30", "firecracker --api-sock "+apiSock))
	if err := cmd.Start(); err != nil {
		t.Fatalf("start fake firecracker: %v", err)
	}

	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if cmd.Process != nil && cmd.Process.Pid > 0 && systemProcessRunning(cmd.Process.Pid, apiSock) {
			return cmd
		}
		time.Sleep(20 * time.Millisecond)
	}
	_ = cmd.Process.Kill()
	_ = cmd.Wait()
	t.Fatalf("fake firecracker process never looked running for %s", apiSock)
	return nil
}

func startFakeFirecrackerAPI(t *testing.T, apiSock string) {
	t.Helper()

	if err := os.MkdirAll(filepath.Dir(apiSock), 0o755); err != nil {
		t.Fatalf("MkdirAll(%s): %v", filepath.Dir(apiSock), err)
	}
	listener, err := net.Listen("unix", apiSock)
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen unix %s: %v", apiSock, err)
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/actions", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPut {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})
	server := &http.Server{Handler: mux}
	go func() {
		_ = server.Serve(listener)
	}()
	t.Cleanup(func() {
		_ = server.Close()
		_ = os.Remove(apiSock)
	})
}

func skipIfSocketRestricted(t *testing.T, err error) {
	t.Helper()
	if err == nil {
		return
	}
	if strings.Contains(strings.ToLower(err.Error()), "operation not permitted") {
		t.Skipf("socket creation is restricted in this environment: %v", err)
	}
}

func startHTTPServerOnTCP4(t *testing.T, handler http.Handler) *net.TCPAddr {
	t.Helper()
	listener, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen http: %v", err)
	}
	server := &http.Server{Handler: handler}
	go func() {
		_ = server.Serve(listener)
	}()
	t.Cleanup(func() {
		_ = server.Close()
	})
	return listener.Addr().(*net.TCPAddr)
}

func startHTTPSServerOnTCP4(t *testing.T, handler http.Handler) *net.TCPAddr {
	t.Helper()
	privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		t.Fatalf("GenerateKey: %v", err)
	}
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(time.Hour),
		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
	if err != nil {
		t.Fatalf("CreateCertificate: %v", err)
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
	cert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		t.Fatalf("X509KeyPair: %v", err)
	}
	listener, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		skipIfSocketRestricted(t, err)
		t.Fatalf("listen https: %v", err)
	}
	server := &http.Server{Handler: handler}
	go func() {
		_ = server.Serve(tls.NewListener(listener, &tls.Config{Certificates: []tls.Certificate{cert}}))
	}()
	t.Cleanup(func() {
		_ = server.Close()
	})
	return listener.Addr().(*net.TCPAddr)
}

func testSetGitConfig(t *testing.T, key, value string) {
	t.Helper()

	cmd := exec.Command("git", "config", "--global", key, value)
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git config --global %s: %v: %s", key, err, strings.TrimSpace(string(output)))
	}
}

func testGitConfigValue(t *testing.T, configPath, key string) string {
	t.Helper()

	cmd := exec.Command("git", "config", "--file", configPath, "--get", key)
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git config --file %s --get %s: %v: %s", configPath, key, err, strings.TrimSpace(string(output)))
	}
	return strings.TrimSpace(string(output))
}

type processKillingRunner struct {
	*scriptedRunner
	proc *exec.Cmd
}

type filesystemRunner struct {
	t *testing.T
}

func (r *filesystemRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
	r.t.Helper()
	if name == "git" {
		cmd := exec.CommandContext(ctx, name, args...)
		var stdout bytes.Buffer
		var stderr bytes.Buffer
		cmd.Stdout = &stdout
		cmd.Stderr = &stderr
		if err := cmd.Run(); err != nil {
			if stderr.Len() > 0 {
				return stdout.Bytes(), fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String()))
			}
			return stdout.Bytes(), err
		}
		return stdout.Bytes(), nil
	}
	return nil, fmt.Errorf("unexpected Run call: %s %v", name, args)
}

func (r *filesystemRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
	r.t.Helper()
	if len(args) == 0 {
		return nil, errors.New("missing sudo command")
	}
	switch args[0] {
	case "mount":
		if len(args) != 3 {
			return nil, fmt.Errorf("unexpected mount args: %v", args)
		}
		source, mountDir := args[1], args[2]
		if err := os.Remove(mountDir); err != nil {
			return nil, err
		}
		if err := os.Symlink(source, mountDir); err != nil {
			return nil, err
		}
		return nil, nil
	case "umount":
		return nil, nil
	case "chmod":
		if len(args) != 3 {
			return nil, fmt.Errorf("unexpected chmod args: %v", args)
		}
		mode, err := strconv.ParseUint(args[1], 8, 32)
		if err != nil {
			return nil, err
		}
		return nil, os.Chmod(args[2], os.FileMode(mode))
	case "cp":
		if len(args) != 4 || args[1] != "-a" {
			return nil, fmt.Errorf("unexpected cp args: %v", args)
		}
		return nil, copyIntoDir(args[2], args[3])
	case "rm":
		if len(args) != 3 || args[1] != "-rf" {
			return nil, fmt.Errorf("unexpected rm args: %v", args)
		}
		return nil, os.RemoveAll(args[2])
	case "mkdir":
		if len(args) != 3 || args[1] != "-p" {
			return nil, fmt.Errorf("unexpected mkdir args: %v", args)
		}
		return nil, os.MkdirAll(args[2], 0o755)
	case "cat":
		if len(args) != 2 {
			return nil, fmt.Errorf("unexpected cat args: %v", args)
		}
		return os.ReadFile(args[1])
	case "install":
		// Minimal install(1): expected forms are
		//   install -m MODE SRC DST           (5 args)
		//   install -o 0 -g 0 -m MODE SRC DST (9 args, ignored owners)
		src, dst, mode, err := parseInstallArgs(args)
		if err != nil {
			return nil, err
		}
		data, err := os.ReadFile(src)
		if err != nil {
			return nil, err
		}
		if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
			return nil, err
		}
		return nil, os.WriteFile(dst, data, os.FileMode(mode))
	case "chown":
		// Recognised forms, all no-op under test (we run as the test
		// user and os.Chown would need CAP_CHOWN):
		//   chown OWNER TARGET
		//   chown -R OWNER TARGET
		//   chown -h OWNER TARGET   (symlink-no-follow; required by
		//                            fcproc.chownChmodNoFollow)
		switch {
		case len(args) == 3:
			return nil, nil
		case len(args) == 4 && (args[1] == "-R" || args[1] == "-h"):
			return nil, nil
		default:
			return nil, fmt.Errorf("unexpected chown args: %v", args)
		}
	case "debugfs":
		return runFakeDebugfs(args[1:])
	case "e2cp":
		// e2cp SRC IMAGE:/GUEST → plain file copy into IMAGE dir
		if len(args) != 3 {
			return nil, fmt.Errorf("unexpected e2cp args: %v", args)
		}
		image, guest, ok := splitImageColonPath(args[2])
		if !ok {
			return nil, fmt.Errorf("e2cp dst missing image:path separator: %v", args)
		}
		target := filepath.Join(image, guest)
		data, err := os.ReadFile(args[1])
		if err != nil {
			return nil, err
		}
		if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
			return nil, err
		}
		return nil, os.WriteFile(target, data, 0o600)
	case "e2rm":
		// e2rm IMAGE:/GUEST → plain file delete; missing is not fatal
		if len(args) != 2 {
			return nil, fmt.Errorf("unexpected e2rm args: %v", args)
		}
		image, guest, ok := splitImageColonPath(args[1])
		if !ok {
			return nil, fmt.Errorf("e2rm missing image:path separator: %v", args)
		}
		target := filepath.Join(image, guest)
		if err := os.Remove(target); err != nil && !os.IsNotExist(err) {
			return nil, err
		}
		return nil, nil
	default:
		return nil, fmt.Errorf("unexpected sudo command: %v", args)
	}
}

// runFakeDebugfs emulates the subset of debugfs commands the ext4
// toolkit drives in per-line mode (the stdin-batched path doesn't run
// under filesystemRunner because it doesn't implement StdinRunner).
// Supported: stat/cat, plus -w mkdir/set_inode_field. Inode 2 <2>
// set_inode_field is a no-op — tests don't care about root-inode mode
// beyond it not exploding.
func runFakeDebugfs(args []string) ([]byte, error) {
	// Forms:
	//   debugfs -R "<cmd>" <image>            (read-only)
	//   debugfs -w -R "<cmd>" <image>         (single write)
	if len(args) < 3 {
		return nil, fmt.Errorf("unexpected debugfs args: %v", args)
	}
	write := false
	rest := args
	if rest[0] == "-w" {
		write = true
		rest = rest[1:]
	}
	if len(rest) != 3 || rest[0] != "-R" {
		return nil, fmt.Errorf("unexpected debugfs args: %v", args)
	}
	cmdLine := strings.TrimSpace(rest[1])
	image := rest[2]

	fields := strings.Fields(cmdLine)
	if len(fields) == 0 {
		return nil, fmt.Errorf("empty debugfs command")
	}
	switch fields[0] {
	case "stat":
		if len(fields) != 2 {
			return nil, fmt.Errorf("unexpected debugfs stat: %q", cmdLine)
		}
		target := filepath.Join(image, strings.Trim(fields[1], `"`))
		if _, err := os.Stat(target); err != nil {
			if os.IsNotExist(err) {
				return []byte("stat: File not found by ext2_lookup while starting pathname"), nil
			}
			return nil, err
		}
		return []byte("Inode: 12 Type: directory"), nil
	case "cat":
		if len(fields) != 2 {
			return nil, fmt.Errorf("unexpected debugfs cat: %q", cmdLine)
		}
		target := filepath.Join(image, strings.Trim(fields[1], `"`))
		data, err := os.ReadFile(target)
		if err != nil {
			return nil, err
		}
		return data, nil
	case "mkdir":
		if !write {
			return nil, fmt.Errorf("debugfs mkdir requires -w: %q", cmdLine)
		}
		if len(fields) != 2 {
			return nil, fmt.Errorf("unexpected debugfs mkdir: %q", cmdLine)
		}
		target := filepath.Join(image, strings.Trim(fields[1], `"`))
		return nil, os.MkdirAll(target, 0o755)
	case "set_inode_field":
		// set_inode_field <path-or-<2>> <field> <value>
		// Mode changes on non-root targets: honour the perm bits so
		// tests can assert file mode. Root inode <2>, uid, gid are
		// no-ops — tests don't inspect them.
		if !write {
			return nil, fmt.Errorf("debugfs set_inode_field requires -w: %q", cmdLine)
		}
		if len(fields) != 4 {
			return nil, fmt.Errorf("unexpected set_inode_field: %q", cmdLine)
		}
		target := strings.Trim(fields[1], `"`)
		if target == "<2>" || fields[2] != "mode" {
			return nil, nil
		}
		raw := strings.TrimPrefix(fields[3], "0")
		v, err := strconv.ParseUint(raw, 8, 32)
		if err != nil {
			return nil, fmt.Errorf("parse set_inode_field mode %q: %w", fields[3], err)
		}
		return nil, os.Chmod(filepath.Join(image, target), os.FileMode(v)&os.ModePerm)
	case "rdump":
		// rdump <src> <dst>
		return nil, fmt.Errorf("rdump not supported in filesystemRunner")
	default:
		return nil, fmt.Errorf("unsupported debugfs cmd: %q", cmdLine)
	}
}

// splitImageColonPath splits an e2cp/e2rm "image:path" argument.
// Returns image, path, true on success. Only the LAST colon is split
// on since image paths on disk may contain one (rare) and guest paths
// always start with "/".
func splitImageColonPath(arg string) (string, string, bool) {
	idx := strings.LastIndex(arg, ":/")
	if idx < 0 {
		return "", "", false
	}
	return arg[:idx], arg[idx+1:], true
}

// parseInstallArgs recognises the `install` invocations banger emits
// and returns (source, destination, parsed mode). Anything else is an
// error so the test stub stays a closed set.
func parseInstallArgs(args []string) (string, string, os.FileMode, error) {
	switch len(args) {
	case 5:
		if args[1] != "-m" {
			return "", "", 0, fmt.Errorf("unexpected install args: %v", args)
		}
		mode, err := strconv.ParseUint(args[2], 8, 32)
		if err != nil {
			return "", "", 0, err
		}
		return args[3], args[4], os.FileMode(mode), nil
	case 9:
		if args[1] != "-o" || args[3] != "-g" || args[5] != "-m" {
			return "", "", 0, fmt.Errorf("unexpected install args: %v", args)
		}
		mode, err := strconv.ParseUint(args[6], 8, 32)
		if err != nil {
			return "", "", 0, err
		}
		return args[7], args[8], os.FileMode(mode), nil
	}
	return "", "", 0, fmt.Errorf("unexpected install args: %v", args)
}

func copyIntoDir(sourcePath, targetDir string) error {
	targetDir = strings.TrimSuffix(targetDir, "/")
	info, err := os.Stat(sourcePath)
	if err != nil {
		return err
	}
	destPath := filepath.Join(targetDir, filepath.Base(sourcePath))
	if info.IsDir() {
		if err := os.MkdirAll(destPath, info.Mode().Perm()); err != nil {
			return err
		}
		entries, err := os.ReadDir(sourcePath)
		if err != nil {
			return err
		}
		for _, entry := range entries {
			if err := copyIntoDir(filepath.Join(sourcePath, entry.Name()), destPath); err != nil {
				return err
			}
		}
		return os.Chmod(destPath, info.Mode().Perm())
	}
	data, err := os.ReadFile(sourcePath)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
		return err
	}
	return os.WriteFile(destPath, data, info.Mode().Perm())
}

func (r *processKillingRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
	return r.scriptedRunner.Run(ctx, name, args...)
}

func (r *processKillingRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
	out, err := r.scriptedRunner.RunSudo(ctx, args...)
	if err != nil {
		return out, err
	}
	if len(args) >= 3 && args[0] == "kill" && args[1] == "-KILL" && r.proc != nil && (r.proc.ProcessState == nil || !r.proc.ProcessState.Exited()) {
		_ = r.proc.Process.Kill()
		_ = r.proc.Wait()
	}
	return out, nil
}

func systemProcessRunning(pid int, apiSock string) bool {
	data, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "cmdline"))
	if err != nil {
		return false
	}
	cmdline := strings.ReplaceAll(string(data), "\x00", " ")
	return strings.Contains(cmdline, "firecracker") && strings.Contains(cmdline, apiSock)
}

func writeFakeExecutable(t *testing.T, path string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("MkdirAll(%s): %v", filepath.Dir(path), err)
	}
	if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
		t.Fatalf("WriteFile(%s): %v", path, err)
	}
}

func ptr[T any](value T) *T {
	return &value
}
