package cli

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

	"banger/internal/api"
	"banger/internal/installmeta"
)

func TestEnsureDaemonRequiresSystemInstallWhenMetadataMissing(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "config"))
	t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "state"))
	t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache"))
	t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "run"))

	restoreLoad := loadInstallMetadata
	restoreUID := currentUID
	t.Cleanup(func() {
		loadInstallMetadata = restoreLoad
		currentUID = restoreUID
	})

	loadInstallMetadata = func() (installmeta.Metadata, error) {
		return installmeta.Metadata{}, os.ErrNotExist
	}
	currentUID = os.Getuid

	d := defaultDeps()
	d.daemonPing = func(context.Context, string) (api.PingResult, error) {
		return api.PingResult{}, errors.New("dial unix /run/banger/bangerd.sock: no such file")
	}

	_, _, err := d.ensureDaemon(context.Background())
	if err == nil || !strings.Contains(err.Error(), "sudo banger system install") {
		t.Fatalf("ensureDaemon error = %v, want install guidance", err)
	}
}

func TestEnsureDaemonSuggestsRestartWhenInstalledButUnavailable(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "config"))
	t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "state"))
	t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache"))
	t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "run"))

	restoreLoad := loadInstallMetadata
	restoreUID := currentUID
	t.Cleanup(func() {
		loadInstallMetadata = restoreLoad
		currentUID = restoreUID
	})

	loadInstallMetadata = func() (installmeta.Metadata, error) {
		return installmeta.Metadata{
			OwnerUser: "tester",
			OwnerUID:  os.Getuid(),
			OwnerGID:  os.Getgid(),
			OwnerHome: t.TempDir(),
		}, nil
	}
	currentUID = os.Getuid

	d := defaultDeps()
	d.daemonPing = func(context.Context, string) (api.PingResult, error) {
		return api.PingResult{}, errors.New("dial unix /run/banger/bangerd.sock: connection refused")
	}

	_, _, err := d.ensureDaemon(context.Background())
	if err == nil || !strings.Contains(err.Error(), "sudo banger system restart") {
		t.Fatalf("ensureDaemon error = %v, want restart guidance", err)
	}
}

func TestEnsureDaemonRejectsNonOwnerUser(t *testing.T) {
	restoreLoad := loadInstallMetadata
	restoreUID := currentUID
	t.Cleanup(func() {
		loadInstallMetadata = restoreLoad
		currentUID = restoreUID
	})

	loadInstallMetadata = func() (installmeta.Metadata, error) {
		return installmeta.Metadata{
			OwnerUser: "alice",
			OwnerUID:  os.Getuid() + 1,
			OwnerGID:  os.Getgid(),
			OwnerHome: t.TempDir(),
		}, nil
	}
	currentUID = os.Getuid

	d := defaultDeps()
	d.daemonPing = func(context.Context, string) (api.PingResult, error) {
		t.Fatal("daemonPing should not be called for a non-owner user")
		return api.PingResult{}, nil
	}

	_, _, err := d.ensureDaemon(context.Background())
	if err == nil || !strings.Contains(err.Error(), "installed for alice") {
		t.Fatalf("ensureDaemon error = %v, want owner mismatch guidance", err)
	}
}

func TestSystemSubcommandFlagsAreScoped(t *testing.T) {
	root := NewBangerCommand()

	systemCmd, _, err := root.Find([]string{"system"})
	if err != nil {
		t.Fatalf("find system: %v", err)
	}
	installCmd, _, err := systemCmd.Find([]string{"install"})
	if err != nil {
		t.Fatalf("find system install: %v", err)
	}
	uninstallCmd, _, err := systemCmd.Find([]string{"uninstall"})
	if err != nil {
		t.Fatalf("find system uninstall: %v", err)
	}
	if installCmd.Flags().Lookup("owner") == nil {
		t.Fatal("system install is missing --owner")
	}
	if uninstallCmd.Flags().Lookup("purge") == nil {
		t.Fatal("system uninstall is missing --purge")
	}
}

func TestRenderSystemdUnitIncludesHardeningDirectives(t *testing.T) {
	unit := renderSystemdUnit(installmeta.Metadata{
		OwnerUser: "alice",
		OwnerUID:  1000,
		OwnerGID:  1000,
		OwnerHome: "/home/alice/dev home",
	})

	for _, want := range []string{
		"ExecStart=/usr/local/bin/bangerd --system",
		"User=alice",
		"Wants=network-online.target bangerd-root.service",
		"After=bangerd-root.service",
		"Requires=bangerd-root.service",
		"KillMode=process",
		"UMask=0077",
		"Environment=TMPDIR=/run/banger",
		"NoNewPrivileges=yes",
		"PrivateMounts=yes",
		"ProtectSystem=strict",
		"ProtectHome=read-only",
		"ProtectControlGroups=yes",
		"ProtectKernelLogs=yes",
		"ProtectKernelModules=yes",
		"ProtectClock=yes",
		"ProtectHostname=yes",
		"RestrictSUIDSGID=yes",
		"LockPersonality=yes",
		"SystemCallArchitectures=native",
		"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK",
		"StateDirectory=banger",
		"StateDirectoryMode=0700",
		"CacheDirectory=banger",
		"CacheDirectoryMode=0700",
		"RuntimeDirectory=banger",
		"RuntimeDirectoryMode=0700",
		"RuntimeDirectoryPreserve=yes",
		`ReadOnlyPaths="/home/alice/dev home"`,
	} {
		if !strings.Contains(unit, want) {
			t.Fatalf("unit = %q, want %q", unit, want)
		}
	}
}

func TestRenderRootHelperSystemdUnitIncludesRequiredCapabilities(t *testing.T) {
	unit := renderRootHelperSystemdUnit()

	for _, want := range []string{
		"ExecStart=/usr/local/bin/bangerd --root-helper",
		// Both directives are load-bearing for "VM survives helper
		// restart": KillMode=process limits the initial SIGTERM to
		// the helper main, SendSIGKILL=no disables the SIGKILL
		// escalation. The helper itself does the cgroup reparent
		// (see roothelper.reparentToBangerFCCgroup) — without
		// that, even these directives leave firecracker exposed to
		// systemd's stop-time cleanup.
		"KillMode=process",
		"SendSIGKILL=no",
		"Environment=TMPDIR=/run/banger-root",
		"NoNewPrivileges=yes",
		"PrivateTmp=yes",
		"PrivateMounts=yes",
		"ProtectSystem=strict",
		"ProtectHome=yes",
		"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK",
		"CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_KILL CAP_MKNOD CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_SYS_CHROOT",
		"ReadWritePaths=/var/lib/banger",
		"RuntimeDirectory=banger-root",
		"RuntimeDirectoryMode=0711",
		"RuntimeDirectoryPreserve=yes",
	} {
		if !strings.Contains(unit, want) {
			t.Fatalf("unit = %q, want %q", unit, want)
		}
	}
}

func TestRenderSystemdUnitsIncludeOptionalCoverageEnv(t *testing.T) {
	t.Setenv(systemCoverDirEnv, "/var/lib/banger")
	t.Setenv(rootCoverDirEnv, "/var/lib/banger")

	userUnit := renderSystemdUnit(installmeta.Metadata{
		OwnerUser: "alice",
		OwnerUID:  1000,
		OwnerGID:  1000,
		OwnerHome: "/home/alice",
	})
	if !strings.Contains(userUnit, `Environment=GOCOVERDIR="/var/lib/banger"`) {
		t.Fatalf("user unit = %q, want GOCOVERDIR env", userUnit)
	}

	rootUnit := renderRootHelperSystemdUnit()
	if !strings.Contains(rootUnit, `Environment=GOCOVERDIR="/var/lib/banger"`) {
		t.Fatalf("root unit = %q, want GOCOVERDIR env", rootUnit)
	}
}
