package storage

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

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

// scriptRunner returns fixed stdout per binary name (for SMART/lvs parsing tests) and
// records calls.
type scriptRunner struct {
	out   map[string][]byte // binary name -> stdout
	calls [][]string
	err   error
}

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

func testStageDir() string { return filepath.Join(os.TempDir(), "felhom-test-units") }

func TestHostOps_MountLifecycle(t *testing.T) {
	ctx := context.Background()
	stage := t.TempDir()
	unitDir := t.TempDir()
	rr := &recordingRunner{}
	ops := NewSudoHostOps(SudoHostOpsConfig{
		Runner:   rr,
		Bins:     Binaries{Systemctl: "/usr/bin/systemctl", Install: "/usr/bin/install"},
		UnitDir:  unitDir,
		StageDir: stage,
		Logger:   quietLogger(),
	})

	// A hyphen-free mountpoint so the systemd-escaped unit filename has no backslash — the
	// backslash escaping is covered by TestSystemdEscapePath; here we just need a filename
	// that stages on the test OS (Windows treats '\' as a path separator). Production is Linux.
	spec := MountSpec{Name: "usb-backup", UUID: "0fc63daf-8483-4772-8e79-3d69d8477de4", Where: "/srv/felhom/bulk", FSType: "ext4"}
	if err := ops.EnsureMount(ctx, spec); err != nil {
		t.Fatalf("EnsureMount: %v", err)
	}

	// Expect: install (stage→unitDir), daemon-reload, enable --now -- <unit>.
	if len(rr.calls) != 3 {
		t.Fatalf("expected 3 commands, got %d: %v", len(rr.calls), rr.calls)
	}
	if rr.calls[0][0] != "/usr/bin/install" || !contains(rr.calls[0], "0644") {
		t.Errorf("call[0] not the install: %v", rr.calls[0])
	}
	if !contains(rr.calls[1], "daemon-reload") {
		t.Errorf("call[1] not daemon-reload: %v", rr.calls[1])
	}
	if !contains(rr.calls[2], "enable") || !contains(rr.calls[2], "--now") {
		t.Errorf("call[2] not enable --now: %v", rr.calls[2])
	}

	// The staged unit file is keyed by UUID and uses the validated mountpoint.
	unitName, _ := UnitNameForMount(spec.Where)
	content, err := os.ReadFile(filepath.Join(stage, unitName))
	if err != nil {
		t.Fatalf("staged unit not written: %v", err)
	}
	cs := string(content)
	if !strings.Contains(cs, "What=/dev/disk/by-uuid/"+spec.UUID) {
		t.Errorf("unit missing by-uuid What=: %s", cs)
	}
	if !strings.Contains(cs, "Where=/srv/felhom/bulk") || !strings.Contains(cs, "Type=ext4") {
		t.Errorf("unit missing Where/Type: %s", cs)
	}
	if !strings.Contains(cs, "WantedBy=multi-user.target") {
		t.Errorf("unit not enabled-persistent: %s", cs)
	}

	// Unmount (detach) = stop + disable.
	rr.calls = nil
	if err := ops.Unmount(ctx, spec.Where); err != nil {
		t.Fatalf("Unmount: %v", err)
	}
	if len(rr.calls) != 2 || !contains(rr.calls[0], "stop") || !contains(rr.calls[1], "disable") {
		t.Fatalf("Unmount should stop+disable: %v", rr.calls)
	}
}

func TestHostOps_SMART_SATA(t *testing.T) {
	sata := []byte(`{
	  "smart_status": {"passed": true},
	  "temperature": {"current": 38},
	  "power_on_time": {"hours": 12345},
	  "ata_smart_attributes": {"table": [
	    {"id": 5, "name": "Reallocated_Sector_Ct", "raw": {"value": 0}},
	    {"id": 197, "name": "Current_Pending_Sector", "raw": {"value": 2}},
	    {"id": 198, "name": "Offline_Uncorrectable", "raw": {"value": 1}}
	  ]}
	}`)
	ops := &SudoHostOps{runner: &scriptRunner{out: map[string][]byte{"/usr/sbin/smartctl": sata}}, bins: Binaries{}.withDefaults(), logger: quietLogger()}
	s, err := ops.SMART(context.Background(), "/dev/sda")
	if err != nil {
		t.Fatal(err)
	}
	if s.Health != hub.SmartPassed {
		t.Errorf("health = %q, want PASSED", s.Health)
	}
	if got := deref(s.TemperatureC); got != 38 {
		t.Errorf("temp = %d", got)
	}
	if deref(s.ReallocatedSectors) != 0 || deref(s.PendingSectors) != 2 || deref(s.OfflineUncorrectable) != 1 {
		t.Errorf("SATA counters wrong: %+v", s)
	}
	if s.MediaErrors != nil || s.PercentageUsed != nil {
		t.Errorf("NVMe counters must be nil for a SATA disk")
	}
}

func TestHostOps_SMART_NVMe(t *testing.T) {
	nvme := []byte(`{
	  "smart_status": {"passed": true},
	  "nvme_smart_health_information_log": {
	    "critical_warning": 0,
	    "media_errors": 5,
	    "percentage_used": 7,
	    "temperature": 41
	  }
	}`)
	ops := &SudoHostOps{runner: &scriptRunner{out: map[string][]byte{"/usr/sbin/smartctl": nvme}}, bins: Binaries{}.withDefaults(), logger: quietLogger()}
	s, _ := ops.SMART(context.Background(), "/dev/nvme0n1")
	if s.Health != hub.SmartPassed {
		t.Errorf("health = %q", s.Health)
	}
	if deref(s.CriticalWarning) != 0 || deref(s.MediaErrors) != 5 || deref(s.PercentageUsed) != 7 {
		t.Errorf("NVMe counters wrong: %+v", s)
	}
	if deref(s.TemperatureC) != 41 {
		t.Errorf("nvme temp = %v", s.TemperatureC)
	}
	if s.ReallocatedSectors != nil {
		t.Errorf("SATA counters must be nil for an NVMe disk")
	}
}

func TestHostOps_SMART_Unsupported(t *testing.T) {
	// A USB-SATA bridge that exposes no SMART: smartctl returns minimal JSON (no
	// smart_status) and a nonzero exit. We degrade to UNKNOWN, not an error.
	ops := &SudoHostOps{
		runner: &scriptRunner{out: map[string][]byte{"/usr/sbin/smartctl": []byte(`{"device":{"name":"/dev/sdc"}}`)}, err: errExit(2)},
		bins:   Binaries{}.withDefaults(), logger: quietLogger(),
	}
	s, err := ops.SMART(context.Background(), "/dev/sdc")
	if err != nil {
		t.Fatalf("unsupported SMART must degrade, not error: %v", err)
	}
	if s.Health != hub.SmartUnknown {
		t.Errorf("health = %q, want UNKNOWN", s.Health)
	}
}

func TestHostOps_ThinPoolMetadata(t *testing.T) {
	lvs := []byte(`{"report":[{"lv":[{"lv_name":"data","data_percent":"42.00","metadata_percent":"10.50"}]}]}`)
	ops := &SudoHostOps{runner: &scriptRunner{out: map[string][]byte{"/usr/sbin/lvs": lvs}}, bins: Binaries{}.withDefaults(), logger: quietLogger()}
	frac, ok := ops.ThinPoolMetadata(context.Background(), "pve", "data")
	if !ok {
		t.Fatal("expected metadata fraction")
	}
	if frac < 0.104 || frac > 0.106 {
		t.Errorf("metadata fraction = %v, want ~0.105", frac)
	}
}

func contains(ss []string, want string) bool {
	for _, s := range ss {
		if s == want {
			return true
		}
	}
	return false
}

func deref(p *int) int {
	if p == nil {
		return -1
	}
	return *p
}

// errExit is a stand-in for a nonzero exit error from the runner.
type errExitT int

func (e errExitT) Error() string { return "exit status nonzero" }
func errExit(code int) error     { return errExitT(code) }
