package storage

import (
	"context"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
)

// recordingRunner records every command it is asked to run (and never execs anything). The
// adversarial matrix asserts that a rejected argument means ZERO commands were constructed —
// the validator is the wall, not the exec.
type recordingRunner struct {
	calls [][]string
	err   error
}

func (r *recordingRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
	r.calls = append(r.calls, append([]string{name}, args...))
	return nil, nil, r.err
}

// --- The headline: the arg-validator adversarial matrix. ---

func TestValidateUUID_AdversarialMatrix(t *testing.T) {
	good := []string{
		"0fc63daf-8483-4772-8e79-3d69d8477de4", // ext4
		"1234-ABCD",                            // FAT
		"deadbeefdeadbeef",                     // NTFS-ish 16 hex
	}
	for _, u := range good {
		if err := ValidateUUID(u); err != nil {
			t.Errorf("ValidateUUID(%q) rejected a valid UUID: %v", u, err)
		}
	}
	bad := []string{
		"",                                // empty
		"../../etc/shadow",                // traversal
		"abcd; rm -rf /",                  // shell metacharacters
		"abcd$(reboot)",                   // command substitution
		"abcd`reboot`",                    // backticks
		"abcd&whoami",                     // &
		"abcd|cat",                        // pipe
		"abcd\nreboot",                    // newline
		"abcd /dev/sda",                   // space + extra arg
		"g00dlooking-but-z-not-hex",       // non-hex
		"/dev/disk/by-uuid/abcd",          // a path, not a uuid
		strings.Repeat("a", maxUUIDLen+1), // too long
		"abcd\x00",                        // NUL
	}
	for _, u := range bad {
		if err := ValidateUUID(u); err == nil {
			t.Errorf("ValidateUUID(%q) ACCEPTED a hostile UUID", u)
		}
	}
}

func TestValidateMountPath_AdversarialMatrix(t *testing.T) {
	good := []string{"/mnt/usb-backup", "/srv/felhom/bulk", "/mnt/data_1"}
	for _, p := range good {
		if err := ValidateMountPath(p); err != nil {
			t.Errorf("ValidateMountPath(%q) rejected a valid path: %v", p, err)
		}
	}
	bad := []string{
		"",                      // empty
		"relative/path",         // not absolute
		"/",                     // bare root
		"/mnt/../etc",           // traversal
		"/mnt/./x",              // dot segment
		"/mnt/usb backup",       // space
		"/mnt/usb;reboot",       // metacharacter
		"/mnt/$(reboot)",        // command substitution
		"/mnt/x\nWhat=/dev/sda", // newline → unit-file injection attempt
		"/mnt/x\x00",            // NUL
		"/mnt/x`reboot`",        // backticks
	}
	for _, p := range bad {
		if err := ValidateMountPath(p); err == nil {
			t.Errorf("ValidateMountPath(%q) ACCEPTED a hostile path", p)
		}
	}
}

func TestValidateSMARTDevice_AdversarialMatrix(t *testing.T) {
	good := []string{"/dev/sda", "/dev/sdb", "/dev/nvme0n1", "/dev/vda"}
	for _, d := range good {
		if err := ValidateSMARTDevice(d); err != nil {
			t.Errorf("ValidateSMARTDevice(%q) rejected a valid device: %v", d, err)
		}
	}
	bad := []string{
		"/dev/sda1",            // a partition, not the whole disk (smartctl targets the disk)
		"/dev/../etc/shadow",   // traversal
		"/dev/sda;reboot",      // metacharacter
		"/dev/sda /dev/sdb",    // extra arg
		"/etc/passwd",          // not /dev
		"sda",                  // no /dev prefix
		"/dev/mapper/pve-root", // device-mapper not whitelisted
		"",                     // empty
	}
	for _, d := range bad {
		if err := ValidateSMARTDevice(d); err == nil {
			t.Errorf("ValidateSMARTDevice(%q) ACCEPTED a hostile device", d)
		}
	}
}

func TestValidateLVMName_AdversarialMatrix(t *testing.T) {
	for _, n := range []string{"pve", "data", "vg0", "vg.thin_pool"} {
		if err := ValidateLVMName(n); err != nil {
			t.Errorf("ValidateLVMName(%q) rejected a valid name: %v", n, err)
		}
	}
	for _, n := range []string{"", "-rf", "vg;reboot", "vg/pool extra", "vg\nx", "vg$(x)"} {
		if err := ValidateLVMName(n); err == nil {
			t.Errorf("ValidateLVMName(%q) ACCEPTED a hostile name", n)
		}
	}
}

// TestHostOps_RejectsHostileArgsBeforeExec is the proof that validation happens BEFORE any
// command is constructed: a hostile UUID / mount path / device → error AND zero runner calls.
func TestHostOps_RejectsHostileArgsBeforeExec(t *testing.T) {
	ctx := context.Background()

	t.Run("EnsureMount hostile UUID", func(t *testing.T) {
		rr := &recordingRunner{}
		ops := newTestHostOps(rr)
		err := ops.EnsureMount(ctx, MountSpec{Name: "x", UUID: "abcd; rm -rf /", Where: "/mnt/x"})
		if err == nil {
			t.Fatal("expected rejection")
		}
		if len(rr.calls) != 0 {
			t.Fatalf("a hostile UUID must be refused before any exec; got calls %v", rr.calls)
		}
	})

	t.Run("EnsureMount traversal mountpoint", func(t *testing.T) {
		rr := &recordingRunner{}
		ops := newTestHostOps(rr)
		err := ops.EnsureMount(ctx, MountSpec{Name: "x", UUID: "1234-ABCD", Where: "/mnt/../etc"})
		if err == nil || len(rr.calls) != 0 {
			t.Fatalf("traversal mountpoint must be refused before exec; err=%v calls=%v", err, rr.calls)
		}
	})

	t.Run("EnsureMount injection via mount options", func(t *testing.T) {
		rr := &recordingRunner{}
		ops := newTestHostOps(rr)
		err := ops.EnsureMount(ctx, MountSpec{Name: "x", UUID: "1234-ABCD", Where: "/mnt/x", Options: "ro\nWhat=/dev/sda"})
		if err == nil || len(rr.calls) != 0 {
			t.Fatalf("newline-injecting options must be refused before exec; err=%v calls=%v", err, rr.calls)
		}
	})

	t.Run("SMART hostile device", func(t *testing.T) {
		rr := &recordingRunner{}
		ops := newTestHostOps(rr)
		_, err := ops.SMART(ctx, "/dev/sda;reboot")
		if err == nil || len(rr.calls) != 0 {
			t.Fatalf("hostile smart device must be refused before exec; err=%v calls=%v", err, rr.calls)
		}
	})

	t.Run("ThinPoolMetadata hostile vg", func(t *testing.T) {
		rr := &recordingRunner{}
		ops := newTestHostOps(rr)
		if _, ok := ops.ThinPoolMetadata(ctx, "vg;reboot", "data"); ok {
			t.Fatal("hostile vg must return ok=false")
		}
		if len(rr.calls) != 0 {
			t.Fatalf("hostile vg must be refused before exec; calls=%v", rr.calls)
		}
	})
}

// newTestHostOps builds a SudoHostOps over a recording runner with a temp stage dir (so the
// EnsureMount staging write — which happens AFTER validation — has somewhere to go in the
// rare valid-path test; hostile-path tests never reach it).
func newTestHostOps(rr proxmox.Runner) *SudoHostOps {
	return NewSudoHostOps(SudoHostOpsConfig{
		Runner:   rr,
		UnitDir:  "/tmp/felhom-test-units",
		StageDir: testStageDir(),
		Logger:   quietLogger(),
	})
}

func TestSystemdEscapePath(t *testing.T) {
	cases := map[string]string{
		"/mnt/usb-backup": "mnt-usb\\x2dbackup",
		"/var/lib/vz":     "var-lib-vz",
		"/srv/data":       "srv-data",
		"/":               "-",
		"/etc/foo.conf":   "etc-foo.conf",
	}
	for in, want := range cases {
		if got := systemdEscapePath(in); got != want {
			t.Errorf("systemdEscapePath(%q) = %q, want %q", in, got, want)
		}
	}
	// The unit name is derived deterministically and ends in .mount.
	name, err := UnitNameForMount("/mnt/usb-backup")
	if err != nil || !strings.HasSuffix(name, ".mount") {
		t.Errorf("UnitNameForMount = %q, %v", name, err)
	}
}
