package sentinel

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestCollectUsesCommandOutputAndPkgSample(t *testing.T) {
	dir := fakeSentinelCommands(t)
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	snap := Collect()
	if snap.Hostname != "titan-13" || snap.Kernel != "6.6.63" || snap.OSImage != "Metis OS" {
		t.Fatalf("unexpected snapshot: %+v", snap)
	}
	if snap.K3sVersion != "v1.31.5+k3s1" || snap.Containerd != "1.7.99" {
		t.Fatalf("unexpected runtime facts: %+v", snap)
	}
	if len(snap.PackageSample) != 4 || snap.PackageSample["k3s"] != "v1.31.5+k3s1" {
		t.Fatalf("unexpected package sample: %+v", snap.PackageSample)
	}
	if snap.USBScratch == nil || snap.USBScratch.Label != "titan-16-scratch" || !snap.USBScratch.MountHealthy || !snap.USBScratch.LabelHealthy || !snap.USBScratch.BindHealthy {
		t.Fatalf("unexpected usb scratch sample: %+v", snap.USBScratch)
	}
}

func TestCommandOutputUsesNsenterWhenRequested(t *testing.T) {
	dir := fakeSentinelCommands(t)
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
	t.Setenv("METIS_SENTINEL_NSENTER", "1")

	got, err := commandOutput("ignored", "arg")
	if err != nil {
		t.Fatalf("commandOutput: %v", err)
	}
	if strings.TrimSpace(string(got)) != "nsenter-ok" {
		t.Fatalf("unexpected nsenter output: %q", string(got))
	}
}

func TestRunAndTrimAndPkgVersionFallbacks(t *testing.T) {
	dir := t.TempDir()
	write := func(name, body string) {
		path := filepath.Join(dir, name)
		if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	write("cat", `printf 'ID=metis\n'`)
	write("rpm", `exit 1`)
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	if got := runAndTrim("missing-command"); got != "" {
		t.Fatalf("runAndTrim missing command = %q", got)
	}
	if got := osRelease(); got != "" {
		t.Fatalf("osRelease without PRETTY_NAME = %q", got)
	}
	if got := pkgVersion("does-not-exist"); got != "" {
		t.Fatalf("pkgVersion fallback = %q", got)
	}
}

func TestCollectUSBScratchMissingAndMalformedConfig(t *testing.T) {
	cases := []struct {
		name string
		cat  string
	}{
		{name: "cat error", cat: `exit 1`},
		{name: "empty config", cat: `printf '   '`},
		{name: "malformed config", cat: `printf '{'`},
		{name: "missing scratch config", cat: `printf '{}'`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dir := fakeCollectorCommands(t, map[string]string{"cat": tc.cat})
			t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
			if got := collectUSBScratch(); got != nil {
				t.Fatalf("expected nil USB scratch config, got %#v", got)
			}
		})
	}
}

func TestCollectUSBScratchDeviceResolutionAndBindHealth(t *testing.T) {
	dir := fakeCollectorCommands(t, map[string]string{
		"cat": `printf '%s\n' '{"usb_scratch":{"mountpoint":"/mnt/scratch","uuid":"u1","label":"scratch","fs":"xfs","bind_targets":["","/bad","/bind"]}}'`,
		"findmnt": `target=""
for ((i=1; i<=$#; i++)); do
  if [[ "${!i}" == "-T" ]]; then
    j=$((i + 1))
    target="${!j}"
    break
  fi
done
case "${target}" in
  /mnt/scratch) printf 'SOURCE="" TARGET="/mnt/scratch" FSTYPE="ext4"\n' ;;
  /bind) printf 'SOURCE="/mnt/scratch" TARGET="/bind" FSTYPE="none"\n' ;;
  *) exit 1 ;;
esac`,
		"blkid": `case "${1:-}" in
  -U) printf '/dev/by-uuid/u1\n' ;;
  -L) printf '/dev/by-label/scratch\n' ;;
  -o) printf 'UUID=other\nLABEL=scratch\nTYPE=xfs\n' ;;
esac`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	scratch := collectUSBScratch()
	if scratch == nil {
		t.Fatal("expected USB scratch data")
	}
	if scratch.MountHealthy || scratch.UUIDHealthy || !scratch.LabelHealthy || scratch.BindHealthy {
		t.Fatalf("unexpected scratch health: %#v", scratch)
	}
	if len(scratch.BindTargets) != 3 || scratch.BindTargets[2].Path != "/bind" || !scratch.BindTargets[2].Healthy {
		t.Fatalf("unexpected bind target health: %#v", scratch.BindTargets)
	}
}

func TestCollectUSBScratchFallsBackToManagedFstabBlock(t *testing.T) {
	dir := fakeCollectorCommands(t, map[string]string{
		"cat": `case "${1:-}" in
  /etc/metis/node.json) exit 1 ;;
  /etc/fstab) printf '%s\n' '# BEGIN maintenance.bstein.dev usb-scratch' \
    'UUID=usb-1 /mnt/astraios ext4 defaults,noatime 0 2' \
    'tmpfs /tmp tmpfs defaults,nosuid,nodev,mode=1777 0 0' \
    '/mnt/astraios/var/log/pods /var/log/pods none bind,x-systemd.requires-mounts-for=/mnt/astraios 0 0' \
    '/mnt/astraios/var/tmp /var/tmp none bind,x-systemd.requires-mounts-for=/mnt/astraios 0 0' \
    '# END maintenance.bstein.dev usb-scratch'
  ;;
  *) exit 1 ;;
esac`,
		"findmnt": `target=""
for ((i=1; i<=$#; i++)); do
  if [[ "${!i}" == "-T" ]]; then
    j=$((i + 1))
    target="${!j}"
    break
  fi
done
case "${target}" in
  /mnt/astraios) printf 'SOURCE="/dev/sda1" TARGET="/mnt/astraios" FSTYPE="ext4"\n' ;;
  /var/log/pods) printf 'SOURCE="/dev/sda1[/var/log/pods]" TARGET="/var/log/pods" FSTYPE="none"\n' ;;
  /var/tmp) printf 'SOURCE="/dev/sda1[/var/tmp]" TARGET="/var/tmp" FSTYPE="none"\n' ;;
  *) exit 1 ;;
esac`,
		"readlink": `case "${2:-}" in
  /mnt/astraios) printf '/mnt/astraios\n' ;;
  /var/log/pods) printf '/mnt/astraios/var/log/pods\n' ;;
  /var/tmp) printf '/mnt/astraios/var/tmp\n' ;;
  *) exit 1 ;;
esac`,
		"blkid": `case "${1:-}" in
  -U) printf '/dev/sda1\n' ;;
  -o) printf 'UUID=usb-1\nTYPE=ext4\n' ;;
esac`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	scratch := collectUSBScratch()
	if scratch == nil {
		t.Fatal("expected USB scratch data from fstab")
	}
	if scratch.Mountpoint != "/mnt/astraios" || scratch.UUID != "usb-1" || scratch.FS != "ext4" {
		t.Fatalf("unexpected scratch identity: %#v", scratch)
	}
	if !scratch.MountHealthy || !scratch.UUIDHealthy || !scratch.BindHealthy {
		t.Fatalf("expected fstab-derived scratch to be healthy: %#v", scratch)
	}
	if len(scratch.BindTargets) != 2 || scratch.BindTargets[0].Path != "/var/log/pods" || scratch.BindTargets[1].Path != "/var/tmp" {
		t.Fatalf("unexpected bind targets: %#v", scratch.BindTargets)
	}
}

func TestBindHealthyAcceptsScratchSymlinksAndDeviceSubpaths(t *testing.T) {
	dir := fakeCollectorCommands(t, map[string]string{
		"readlink": `case "${2:-}" in
  /mnt/scratch) printf '/mnt/scratch\n' ;;
  /var/log/pods) printf '/mnt/scratch/var/log/pods\n' ;;
  *) exit 1 ;;
esac`,
		"findmnt": `target=""
for ((i=1; i<=$#; i++)); do
  if [[ "${!i}" == "-T" ]]; then
    j=$((i + 1))
    target="${!j}"
    break
  fi
done
case "${target}" in
  /mnt/scratch) printf 'SOURCE="/dev/sdz1" TARGET="/mnt/scratch" FSTYPE="ext4"\n' ;;
  /var/tmp) printf 'SOURCE="/dev/sdz1[/var/tmp]" TARGET="/var/tmp" FSTYPE="ext4"\n' ;;
  /other) printf 'SOURCE="/dev/sdz1[/unexpected]" TARGET="/other" FSTYPE="ext4"\n' ;;
  *) exit 1 ;;
esac`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	if !bindHealthy("/var/log/pods", "/mnt/scratch") {
		t.Fatal("scratch-backed symlink target should be healthy")
	}
	if !bindHealthy("/var/tmp", "/mnt/scratch") {
		t.Fatal("device subpath bind target should be healthy")
	}
	if bindHealthy("/other", "/mnt/scratch") {
		t.Fatal("unrelated device subpath should be unhealthy")
	}
	if device, subpath := splitFindmntSource(`/dev/sdz1[/var/tmp]`); device != "/dev/sdz1" || subpath != "/var/tmp" {
		t.Fatalf("splitFindmntSource device/subpath = %q %q", device, subpath)
	}
	if device, subpath := splitFindmntSource("/dev/sdz1"); device != "/dev/sdz1" || subpath != "" {
		t.Fatalf("plain splitFindmntSource device/subpath = %q %q", device, subpath)
	}
	if device, subpath := splitFindmntSource(""); device != "" || subpath != "" {
		t.Fatalf("empty splitFindmntSource device/subpath = %q %q", device, subpath)
	}
	if got := realPath(""); got != "" {
		t.Fatalf("empty realPath = %q", got)
	}
	if got := realPath("/missing"); got != "" {
		t.Fatalf("missing realPath = %q", got)
	}
	if pathWithin("", "/mnt/scratch") || pathWithin("/mnt/scratch/file", "") {
		t.Fatal("empty pathWithin inputs should be false")
	}
}

func TestBindHealthyRejectsDeviceSubpathWithoutRootMount(t *testing.T) {
	dir := fakeCollectorCommands(t, map[string]string{
		"readlink": `exit 1`,
		"findmnt": `target=""
for ((i=1; i<=$#; i++)); do
  if [[ "${!i}" == "-T" ]]; then
    j=$((i + 1))
    target="${!j}"
    break
  fi
done
case "${target}" in
  /var/tmp) printf 'SOURCE="/dev/sdz1[/var/tmp]" TARGET="/var/tmp" FSTYPE="ext4"\n' ;;
  *) exit 1 ;;
esac`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	if bindHealthy("/var/tmp", "/mnt/scratch") {
		t.Fatal("device subpath should be unhealthy when root scratch mount is unknown")
	}
}

func TestCollectUSBScratchDefaultsAndParserEdges(t *testing.T) {
	dir := fakeCollectorCommands(t, map[string]string{
		"cat":     `printf '%s\n' '{"usb_scratch":{"mountpoint":"/mnt/scratch"}}'`,
		"findmnt": `printf 'SOURCE="/dev/sdz1" TARGET="/mnt/scratch" FSTYPE="ext4"\n'`,
		"blkid":   `exit 1`,
	})
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))

	scratch := collectUSBScratch()
	if scratch == nil || !scratch.MountHealthy || !scratch.BindHealthy || scratch.FS != "ext4" {
		t.Fatalf("unexpected default scratch health: %#v", scratch)
	}
	if source, fsType, mounted := mountInfo(""); source != "" || fsType != "" || mounted {
		t.Fatalf("empty mountInfo = %q %q %v", source, fsType, mounted)
	}
	if bindHealthy("", "/mnt/scratch") || bindHealthy("/bind", "") {
		t.Fatal("empty bind inputs should be unhealthy")
	}
	if got := resolveDeviceByUUID(""); got != "" {
		t.Fatalf("empty UUID resolved to %q", got)
	}
	if got := resolveDeviceByLabel(""); got != "" {
		t.Fatalf("empty label resolved to %q", got)
	}
	if got := blkidExport(""); len(got) != 0 {
		t.Fatalf("empty blkid export = %#v", got)
	}
	values := parseKeyValues(`BROKEN A="b" C=d`)
	if values["A"] != "b" || values["C"] != "d" {
		t.Fatalf("parseKeyValues = %#v", values)
	}
}

func fakeCollectorCommands(t *testing.T, scripts map[string]string) string {
	t.Helper()
	dir := t.TempDir()
	for name, body := range scripts {
		path := filepath.Join(dir, name)
		if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	return dir
}

func fakeSentinelCommands(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	write := func(name, body string) {
		path := filepath.Join(dir, name)
		if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	write("hostname", `printf 'titan-13\n'`)
	write("uname", `printf '6.6.63\n'`)
	write("k3s", `printf 'v1.31.5+k3s1\n'`)
	write("containerd", `printf '1.7.99\n'`)
	write("cat", `case "${1:-}" in
  /etc/os-release) printf 'PRETTY_NAME="Metis OS"\n' ;;
  /etc/metis/node.json) printf '%s\n' '{"usb_scratch":{"mountpoint":"/mnt/scratch","label":"titan-16-scratch","fs":"ext4","bind_targets":["/var/lib/rancher","/var/log"]}}' ;;
  *) printf 'PRETTY_NAME="Metis OS"\n' ;;
esac`)
	write("findmnt", `target=""
for ((i=1; i<=$#; i++)); do
  if [[ "${!i}" == "-T" ]]; then
    j=$((i + 1))
    target="${!j}"
    break
  fi
done
case "${target}" in
  /mnt/scratch) printf 'SOURCE="/dev/sdz1" TARGET="/mnt/scratch" FSTYPE="ext4"\n' ;;
  /var/lib/rancher) printf 'SOURCE="/mnt/scratch" TARGET="/var/lib/rancher" FSTYPE="none"\n' ;;
  /var/log) printf 'SOURCE="/mnt/scratch" TARGET="/var/log" FSTYPE="none"\n' ;;
  *) exit 1 ;;
esac`)
	write("blkid", `case "${1:-}" in
  -U) printf '/dev/sdz1\n' ;;
  -L) printf '/dev/sdz1\n' ;;
  -o) printf 'UUID=titan-16-uuid\nLABEL=titan-16-scratch\nTYPE=ext4\n' ;;
esac`)
	write("dpkg-query", `case "${@: -1}" in
  containerd) printf '1.7.99\n' ;;
  k3s) printf 'v1.31.5+k3s1\n' ;;
  nvidia-container-toolkit) printf '1.16.2\n' ;;
  linux-image-raspi) printf '6.6.63\n' ;;
  *) printf '1.0.0\n' ;;
esac`)
	write("rpm", `printf '1.0.0\n'`)
	write("nsenter", `printf 'nsenter-ok\n'`)
	return dir
}
