package storage

import (
	"context"
	"strings"
	"testing"
)

// classifyClaim is the pure guard verdict — the load-bearing safety logic. Table-driven over every
// claim signal + the fail-safe, plus the two ALLOW cases (clean disk, and re-init of our own drive).
func TestClassifyClaim(t *testing.T) {
	base := func() claimFacts {
		return claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true,
			nodes: []claimNode{{name: "sdd", fstype: "", mountpoint: ""}, {name: "sdd1", fstype: "ntfs", mountpoint: ""}}}
	}
	cases := []struct {
		name       string
		mutate     func(f *claimFacts)
		wantUnclaimed bool
		reasonHas  string
	}{
		{"clean unclaimed disk (ntfs, unmounted)", func(f *claimFacts) {}, true, "unclaimed"},
		{"blank disk", func(f *claimFacts) { f.nodes = []claimNode{{name: "sdd"}} }, true, "unclaimed"},
		{"our own drive re-init (mounted under /mnt/felhom-drives)", func(f *claimFacts) {
			f.nodes = []claimNode{{name: "sdd"}, {name: "sdd1", fstype: "ext4", mountpoint: "/mnt/felhom-drives/felhom-flash"}}
		}, true, "unclaimed"},
		{"system/OS disk (non-data-bearing) — RED-PROOF vs DataBearing", func(f *claimFacts) {
			f.isSystem = true
			f.nodes = []claimNode{{name: "sda"}} // blank: DataBearing would say benign; the guard must still refuse
		}, false, "system/OS disk"},
		{"LVM PV (pvs)", func(f *claimFacts) { f.lvmPV = true }, false, "LVM physical volume"},
		{"LVM2_member via lsblk fstype", func(f *claimFacts) {
			f.nodes = []claimNode{{name: "sdd"}, {name: "sdd1", fstype: "LVM2_member"}}
		}, false, "LVM2_member"},
		{"ZFS pool member (zpool)", func(f *claimFacts) { f.zfsMember = true }, false, "ZFS pool member"},
		{"zfs_member via lsblk fstype", func(f *claimFacts) {
			f.nodes = []claimNode{{name: "sdd", fstype: "zfs_member"}}
		}, false, "zfs_member"},
		{"mdraid member", func(f *claimFacts) {
			f.nodes = []claimNode{{name: "sdd1", fstype: "linux_raid_member"}}
		}, false, "linux_raid_member"},
		{"active swap signature", func(f *claimFacts) {
			f.nodes = []claimNode{{name: "sdd1", fstype: "swap"}}
		}, false, "swap"},
		{"foreign mount (outside /mnt/felhom-drives)", func(f *claimFacts) {
			f.nodes = []claimNode{{name: "sdd1", fstype: "ext4", mountpoint: "/srv/data"}}
		}, false, "mounted at /srv/data"},
		{"read-only device", func(f *claimFacts) { f.readonly = true }, false, "read-only"},
		{"undeterminable topology", func(f *claimFacts) { f.wholeDiskOK = false }, false, "undeterminable"},
		{"fail-safe: gather error", func(f *claimFacts) { f.gatherErr = "lsblk failed" }, false, "could not determine"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			f := base()
			tc.mutate(&f)
			gotUnclaimed, reason := classifyClaim(f)
			if gotUnclaimed != tc.wantUnclaimed {
				t.Fatalf("unclaimed=%v want %v (reason %q)", gotUnclaimed, tc.wantUnclaimed, reason)
			}
			if !strings.Contains(reason, tc.reasonHas) {
				t.Errorf("reason %q missing %q", reason, tc.reasonHas)
			}
		})
	}
}

// TestClassifyClaim_EmptyNodesRefused is the audit-D2 negative test: a successful-but-EMPTY lsblk
// (`{"blockdevices":[]}` → zero nodes) previously skipped the member/mount loop entirely and returned
// (true,"unclaimed") — the one hole in the "undeterminable ⇒ claimed" fail-safe. It must refuse.
func TestClassifyClaim_EmptyNodesRefused(t *testing.T) {
	for _, nodes := range [][]claimNode{nil, {}} {
		f := claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true, nodes: nodes}
		unclaimed, reason := classifyClaim(f)
		if unclaimed {
			t.Fatalf("nodes=%v: empty topology classified UNCLAIMED (reason %q) — fail-safe hole", nodes, reason)
		}
		if !strings.Contains(reason, "empty block topology") {
			t.Errorf("nodes=%v: reason %q missing the empty-topology explanation", nodes, reason)
		}
	}
}

// TestClassifyClaim_TargetAbsentFromTree (audit D2): lsblk returned SOME tree, but the target
// whole-disk is not in it — the loop inspected the wrong device's signals. Undeterminable → refuse.
func TestClassifyClaim_TargetAbsentFromTree(t *testing.T) {
	f := claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true,
		nodes: []claimNode{{name: "sdc"}, {name: "sdc1", fstype: "ntfs"}}} // benign signals, wrong disk
	unclaimed, reason := classifyClaim(f)
	if unclaimed {
		t.Fatalf("target-absent tree classified UNCLAIMED (reason %q)", reason)
	}
	if !strings.Contains(reason, "absent from block topology") {
		t.Errorf("reason %q missing the target-absent explanation", reason)
	}
}

func TestParseLsblkNodes(t *testing.T) {
	out := []byte(`{"blockdevices":[{"name":"sdd","fstype":null,"mountpoint":null,"children":[{"name":"sdd1","fstype":"ntfs","mountpoint":null}]}]}`)
	nodes, err := parseLsblkNodes(out)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(nodes) != 2 || nodes[0].name != "sdd" || nodes[1].name != "sdd1" || nodes[1].fstype != "ntfs" {
		t.Fatalf("nodes = %+v", nodes)
	}
	if _, err := parseLsblkNodes([]byte("not json")); err == nil {
		t.Error("expected a parse error on garbage")
	}
}

// --- Format guard integration: the guard is wired into Format and actually gates mkfs. ---

// newGuardOps builds a SudoHostOps with stubbed reads: a scriptRunner for lsblk/mkfs, a fakeHostReader
// for SystemDisks, Pvs/Zpool empty (→ binaryPresent=false → skipped), and readWholeDiskRO stubbed.
func newGuardOps(sr *scriptRunner, mounts []Mount) *SudoHostOps {
	return &SudoHostOps{
		runner: sr,
		// Pvs/Zpool empty ⇒ binaryPresent=false ⇒ skipped (deterministic across OSes; lsblk carries the signals).
		bins:   Binaries{Lsblk: "/usr/bin/lsblk", MkfsGuarded: "/usr/local/sbin/felhom-mkfs-guarded"},
		host:   &fakeHostReader{mounts: mounts},
		logger: quietLogger(),
	}
}

func mkfsCalled(sr *scriptRunner) bool {
	for _, c := range sr.calls {
		if strings.Contains(c[0], "mkfs") {
			return true
		}
	}
	return false
}

func TestFormatGuard_RefusesSystemDisk(t *testing.T) {
	defer stubRO(false)()
	sr := &scriptRunner{out: map[string][]byte{
		"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sda","children":[{"name":"sda2","mountpoint":"/"}]}]}`),
	}}
	// / on /dev/sda2 → SystemDisks resolves {/dev/sda}; formatting /dev/sda must be refused.
	ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
	err := ops.Format(context.Background(), "/dev/sda", "ext4")
	if err == nil || !strings.Contains(err.Error(), "system/OS disk") {
		t.Fatalf("want system-disk refusal, got %v", err)
	}
	if mkfsCalled(sr) {
		t.Fatal("mkfs was invoked on a claimed device — guard failed")
	}
}

func TestFormatGuard_RefusesLVMMember(t *testing.T) {
	defer stubRO(false)()
	sr := &scriptRunner{out: map[string][]byte{
		"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdd","children":[{"name":"sdd1","fstype":"LVM2_member"}]}]}`),
	}}
	ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
	err := ops.Format(context.Background(), "/dev/sdd", "ext4")
	if err == nil || !strings.Contains(err.Error(), "LVM2_member") {
		t.Fatalf("want LVM-member refusal, got %v", err)
	}
	if mkfsCalled(sr) {
		t.Fatal("mkfs invoked on an LVM member — guard failed")
	}
}

func TestFormatGuard_AllowsUnclaimed(t *testing.T) {
	defer stubRO(false)()
	sr := &scriptRunner{out: map[string][]byte{
		"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdd","children":[{"name":"sdd1","fstype":"ntfs"}]}]}`),
	}}
	// /dev/sdd is not the system disk, not a member, not mounted → guard passes → mkfs runs.
	ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
	if err := ops.Format(context.Background(), "/dev/sdd", "ext4"); err != nil {
		t.Fatalf("unclaimed disk must format, got %v", err)
	}
	if !mkfsCalled(sr) {
		t.Fatal("mkfs was NOT invoked on an unclaimed disk — guard over-refused")
	}
}

// stubRO overrides the /sys read-only probe for the duration of a test; the returned func restores it.
func stubRO(ro bool) func() {
	orig := readWholeDiskRO
	readWholeDiskRO = func(string) (bool, error) { return ro, nil }
	return func() { readWholeDiskRO = orig }
}
