package imagepull

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"banger/internal/system"
)

func TestInjectGuestAgentsWritesExpectedFiles(t *testing.T) {
	if _, err := exec.LookPath("mkfs.ext4"); err != nil {
		t.Skip("mkfs.ext4 not available; skipping")
	}
	if _, err := exec.LookPath("debugfs"); err != nil {
		t.Skip("debugfs not available; skipping")
	}

	// Build a bare ext4 from an empty (but non-empty-dir) source so
	// debugfs has a valid filesystem to inject into. mkfs.ext4 -d
	// wants the source dir itself to contain at least something.
	src := t.TempDir()
	if err := os.MkdirAll(filepath.Join(src, "usr"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(src, "etc"), 0o755); err != nil {
		t.Fatal(err)
	}
	ext4 := filepath.Join(t.TempDir(), "rootfs.ext4")
	if err := BuildExt4(context.Background(), system.NewRunner(), src, ext4, MinExt4Size); err != nil {
		t.Fatalf("BuildExt4: %v", err)
	}

	// Fake vsock-agent binary content — InjectGuestAgents copies bytes
	// verbatim so any file passes as a stand-in.
	fakeAgent := filepath.Join(t.TempDir(), "banger-vsock-agent")
	if err := os.WriteFile(fakeAgent, []byte("#!/bin/true\n"), 0o755); err != nil {
		t.Fatal(err)
	}

	if err := InjectGuestAgents(context.Background(), system.NewRunner(), ext4, GuestAgentAssets{
		VsockAgentBin: fakeAgent,
	}); err != nil {
		t.Fatalf("InjectGuestAgents: %v", err)
	}

	// Verify each expected path is present via debugfs stat.
	expectPaths := []string{
		"/usr/local/bin/banger-vsock-agent",
		"/usr/local/libexec/banger-network-bootstrap",
		"/etc/systemd/system/banger-network.service",
		"/etc/systemd/system/banger-vsock-agent.service",
		"/etc/modules-load.d/banger-vsock.conf",
		"/etc/systemd/system/multi-user.target.wants/banger-network.service",
		"/etc/systemd/system/multi-user.target.wants/banger-vsock-agent.service",
		// Phase B-3 first-boot bits:
		FirstBootScriptPath,
		"/etc/systemd/system/" + FirstBootUnitName,
		"/etc/systemd/system/multi-user.target.wants/" + FirstBootUnitName,
		FirstBootMarkerPath,
	}
	for _, p := range expectPaths {
		out, err := exec.Command("debugfs", "-R", "stat "+p, ext4).CombinedOutput()
		if err != nil {
			t.Errorf("debugfs stat %s: %v: %s", p, err, out)
			continue
		}
		if strings.Contains(string(out), "couldn't find file") || strings.Contains(string(out), "File not found") {
			t.Errorf("path missing: %s\noutput:\n%s", p, out)
		}
	}

	// Verify ownership on one file (uid=0).
	statOut, err := exec.Command("debugfs", "-R", "stat /usr/local/bin/banger-vsock-agent", ext4).CombinedOutput()
	if err != nil {
		t.Fatalf("debugfs stat agent: %v: %s", err, statOut)
	}
	s := string(statOut)
	if !strings.Contains(s, "User:     0") && !strings.Contains(s, "User: 0") {
		t.Errorf("vsock-agent binary not uid=0:\n%s", s)
	}
	if !strings.Contains(s, "Mode:  0755") && !strings.Contains(s, "Mode:  100755") {
		t.Errorf("vsock-agent binary mode not 0755:\n%s", s)
	}
}

func TestInjectGuestAgentsRequiresVsockAgentBinary(t *testing.T) {
	err := InjectGuestAgents(context.Background(), system.NewRunner(), "/tmp/nonexistent.ext4", GuestAgentAssets{
		VsockAgentBin: "",
	})
	if err == nil || !strings.Contains(err.Error(), "required") {
		t.Fatalf("expected missing-binary error, got %v", err)
	}
}

func TestCollectAncestorsIsShallowFirst(t *testing.T) {
	files := []injectFile{
		{guestPath: "/a/b/c/file"},
	}
	symlinks := []injectSymlink{
		{link: "/x/y/z/link"},
	}
	got := collectAncestors(files, symlinks)
	want := []string{"/a", "/x", "/a/b", "/x/y", "/a/b/c", "/x/y/z"}
	if len(got) != len(want) {
		t.Fatalf("len got=%d want=%d: %v", len(got), len(want), got)
	}
	for i, g := range got {
		if g != want[i] {
			t.Errorf("index %d: got %q want %q", i, g, want[i])
		}
	}
}
