package guesthook

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

const sampleConf = `arch: amd64
cores: 2
hostname: demo-felhom
memory: 12288
mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/docker,backup=1,size=256G
mp1: /mnt/felhom-usb/felhom-data,mp=/mnt/felhom-usb
mp2: /mnt/felhom-flash/felhom-data,mp=/mnt/felhom-flash
mp9: /var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1
net0: name=eth0,bridge=vmbr0
rootfs: local-lvm:vm-9201-disk-0,size=32G
swap: 4096
unprivileged: 1
`

func TestParseConfMounts(t *testing.T) {
	got := ParseConfMounts(sampleConf)
	want := map[string]string{
		"mp0":    "local-lvm:vm-9201-disk-1",
		"mp1":    "/mnt/felhom-usb/felhom-data",
		"mp2":    "/mnt/felhom-flash/felhom-data",
		"mp9":    "/var/lib/felhom-agent/guests/9201/bootstrap",
		"rootfs": "local-lvm:vm-9201-disk-0",
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("ParseConfMounts mismatch:\n got=%v\nwant=%v", got, want)
	}
	// net0/arch/etc. (non-mount keys) must NOT leak in.
	if _, bad := got["net0"]; bad {
		t.Fatalf("net0 was parsed as a mount source")
	}
}

// TestMissingBindSources is the load-bearing selector test. The flash drive is absent (its felhom-data
// source missing); usb is present; mp0/rootfs are STORAGE volumes (must never be selected); mp9's
// bootstrap source is present. Only the flash source may be returned.
//
// COMPANION GUARD — this test FAILS on the two trivial impls the spec warns about:
//   - "return nothing" (the pre-fix no-op hook) → flash not selected → guest still bricks → FAIL.
//   - "return every source" (mkdir everything) → would include the present usb bind AND the
//     local-lvm storage volumes (creating bogus dirs that shadow real data) → FAIL.
func TestMissingBindSources(t *testing.T) {
	mounts := ParseConfMounts(sampleConf)
	present := map[string]bool{
		"/mnt/felhom-usb/felhom-data":                  true, // usb attached
		"/var/lib/felhom-agent/guests/9201/bootstrap":  true, // bootstrap always present
		// "/mnt/felhom-flash/felhom-data" is ABSENT (drive unplugged)
	}
	got := MissingBindSources(mounts, func(p string) bool { return present[p] })
	want := []string{"/mnt/felhom-flash/felhom-data"}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("MissingBindSources mismatch:\n got=%v\nwant=%v", got, want)
	}

	// Explicit companion assertions (pin both failure directions independently of want):
	for _, p := range got {
		if p == "/mnt/felhom-usb/felhom-data" {
			t.Fatalf("selected a PRESENT bind source — over-eager (would shadow live data)")
		}
		if p == "local-lvm:vm-9201-disk-1" || p == "local-lvm:vm-9201-disk-0" {
			t.Fatalf("selected a STORAGE VOLUME source — must only heal host-path binds")
		}
	}
	if len(got) == 0 {
		t.Fatalf("selected nothing — the absent flash bind would brick the guest (pre-fix no-op)")
	}
}

// TestHealCreatesOnlyMissingBind drives the real filesystem path in a temp dir: a present bind source is
// left untouched, an absent one is created (so the guest boots), a storage volume is never created.
func TestHealCreatesOnlyMissingBind(t *testing.T) {
	root := t.TempDir()
	presentSrc := filepath.Join(root, "usb", "felhom-data")
	absentSrc := filepath.Join(root, "flash", "felhom-data")
	if err := os.MkdirAll(presentSrc, 0o755); err != nil {
		t.Fatal(err)
	}
	conf := "" +
		"mp0: local-lvm:vm-9-disk-0,mp=/var/lib/docker,backup=1\n" +
		"mp1: " + presentSrc + ",mp=/mnt/usb\n" +
		"mp2: " + absentSrc + ",mp=/mnt/flash\n" +
		"rootfs: local-lvm:vm-9-disk-1,size=32G\n"
	confPath := filepath.Join(root, "9.conf")
	if err := os.WriteFile(confPath, []byte(conf), 0o644); err != nil {
		t.Fatal(err)
	}

	created, err := Heal(confPath)
	if err != nil {
		t.Fatalf("Heal: %v", err)
	}
	if !reflect.DeepEqual(created, []string{absentSrc}) {
		t.Fatalf("Heal created %v, want [%s]", created, absentSrc)
	}
	if _, err := os.Stat(absentSrc); err != nil {
		t.Fatalf("absent bind source not created — guest would still brick: %v", err)
	}
	// A storage-volume mp must never produce a bogus host directory.
	if _, err := os.Stat(filepath.Join(root, "local-lvm:vm-9-disk-0")); err == nil {
		t.Fatalf("a storage volume source was materialised as a directory")
	}
}

// TestHealMissingConfNeverErrors — a hook must never block a start, even for an unreadable config.
func TestHealMissingConfNeverErrors(t *testing.T) {
	created, err := Heal(filepath.Join(t.TempDir(), "does-not-exist.conf"))
	if err != nil || created != nil {
		t.Fatalf("Heal on missing conf: created=%v err=%v (want nil,nil)", created, err)
	}
}
