package system

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

func TestReadGPUsFromSysfs(t *testing.T) {
	root := t.TempDir()

	// card0 — AMD GPU with VRAM and utilization files
	pciDev := filepath.Join(root, "devices/pci0000:00/0000:00:02.0")
	mkdirAll(t, pciDev)
	write(t, pciDev, ".", "vendor", "0x1002")
	write(t, pciDev, ".", "device", "0x7480")
	write(t, pciDev, ".", "mem_info_vram_total", "8573157376")
	write(t, pciDev, ".", "mem_info_vram_used", "27267072")
	write(t, pciDev, ".", "gpu_busy_percent", "23")
	write(t, pciDev, ".", "mem_busy_percent", "15")

	driverDir := filepath.Join(root, "bus/pci/drivers/amdgpu")
	mkdirAll(t, driverDir)
	mustSymlink(t, driverDir, filepath.Join(pciDev, "driver"))

	cardTarget := filepath.Join(pciDev, "drm", "card0")
	mkdirAll(t, cardTarget)
	mustSymlink(t, pciDev, filepath.Join(cardTarget, "device"))
	mustSymlink(t, cardTarget, filepath.Join(root, "card0"))

	// Distractors
	write(t, root, ".", "card0-HDMI-1", "distract")
	write(t, root, ".", "renderD128", "distract")

	gpus := readGPUsFromSysfs(root)
	if len(gpus) != 1 {
		t.Fatalf("want 1 GPU, got %d: %+v", len(gpus), gpus)
	}
	if gpus[0].Vendor != "1002" {
		t.Errorf("vendor = %q, want 1002", gpus[0].Vendor)
	}
	if gpus[0].DeviceID != "7480" {
		t.Errorf("device_id = %q, want 7480", gpus[0].DeviceID)
	}
	if gpus[0].Driver != "amdgpu" {
		t.Errorf("driver = %q, want amdgpu", gpus[0].Driver)
	}
	if gpus[0].MemoryTotalBytes != 8573157376 {
		t.Errorf("MemoryTotalBytes = %d, want 8573157376", gpus[0].MemoryTotalBytes)
	}
	if gpus[0].MemoryUsedBytes != 27267072 {
		t.Errorf("MemoryUsedBytes = %d, want 27267072", gpus[0].MemoryUsedBytes)
	}
	if gpus[0].UtilizationPct != 23.0 {
		t.Errorf("UtilizationPct = %f, want 23.0", gpus[0].UtilizationPct)
	}
	if gpus[0].MemUtilizationPct != 15.0 {
		t.Errorf("MemUtilizationPct = %f, want 15.0", gpus[0].MemUtilizationPct)
	}
}

func TestReadGPUsFromSysfsNoEnrichment(t *testing.T) {
	root := t.TempDir()

	// i915 GPU with no VRAM or utilization files
	pciDev := filepath.Join(root, "devices/pci0000:00/0000:00:02.0")
	mkdirAll(t, pciDev)
	write(t, pciDev, ".", "vendor", "0x8086")
	write(t, pciDev, ".", "device", "0x46a6")

	driverDir := filepath.Join(root, "bus/pci/drivers/i915")
	mkdirAll(t, driverDir)
	mustSymlink(t, driverDir, filepath.Join(pciDev, "driver"))

	cardTarget := filepath.Join(pciDev, "drm", "card0")
	mkdirAll(t, cardTarget)
	mustSymlink(t, pciDev, filepath.Join(cardTarget, "device"))
	mustSymlink(t, cardTarget, filepath.Join(root, "card0"))

	gpus := readGPUsFromSysfs(root)
	if len(gpus) != 1 {
		t.Fatalf("want 1 GPU, got %d", len(gpus))
	}
	if gpus[0].MemoryTotalBytes != 0 || gpus[0].MemoryUsedBytes != 0 {
		t.Errorf("expected 0 VRAM for i915, got total=%d used=%d", gpus[0].MemoryTotalBytes, gpus[0].MemoryUsedBytes)
	}
	if gpus[0].UtilizationPct != 0 || gpus[0].MemUtilizationPct != 0 {
		t.Errorf("expected 0 utilization for i915, got gpu=%f mem=%f", gpus[0].UtilizationPct, gpus[0].MemUtilizationPct)
	}
}

func TestReadGPUsFromSysfsMissingDir(t *testing.T) {
	gpus := readGPUsFromSysfs("/nonexistent/drm")
	if gpus != nil {
		t.Errorf("expected nil, got %+v", gpus)
	}
}

func TestReadGPUsFromSysfsSkipsNonGPU(t *testing.T) {
	root := t.TempDir()
	write(t, root, ".", "renderD128", "x")
	write(t, root, ".", "card0-HDMI-1", "x")
	gpus := readGPUsFromSysfs(root)
	if len(gpus) != 0 {
		t.Errorf("expected 0 GPUs, got %d", len(gpus))
	}
}

func TestReadHwmonTemps(t *testing.T) {
	root := t.TempDir()
	// k10temp: CPU, labelled Tctl.
	write(t, root, "hwmon0", "name", "k10temp")
	write(t, root, "hwmon0", "temp1_input", "62125")
	write(t, root, "hwmon0", "temp1_label", "Tctl")
	// amdgpu: GPU, no label -> falls back to chip name. Also a sensor reading
	// an implausible value that must be dropped.
	write(t, root, "hwmon1", "name", "amdgpu")
	write(t, root, "hwmon1", "temp1_input", "56000")
	write(t, root, "hwmon1", "temp2_input", "-150") // disabled placeholder
	// empty input -> skipped without error.
	write(t, root, "hwmon2", "name", "cros_ec")
	write(t, root, "hwmon2", "temp1_input", "")

	got := readHwmonTemps(root)
	if len(got) != 2 {
		t.Fatalf("want 2 temps, got %d: %+v", len(got), got)
	}
	if got[0].Chip != "k10temp" || got[0].Label != "Tctl" || got[0].Celsius != 62.125 {
		t.Errorf("cpu sensor wrong: %+v", got[0])
	}
	if got[1].Chip != "amdgpu" || got[1].Label != "amdgpu" || got[1].Celsius != 56 {
		t.Errorf("gpu sensor wrong (label should fall back to chip): %+v", got[1])
	}
}

func mkdirAll(t *testing.T, path string) {
	t.Helper()
	if err := os.MkdirAll(path, 0o755); err != nil {
		t.Fatal(err)
	}
}

func mustSymlink(t *testing.T, target, link string) {
	t.Helper()
	if err := os.Symlink(target, link); err != nil {
		t.Fatal(err)
	}
}

func write(t *testing.T, root, chip, file, val string) {
	t.Helper()
	dir := filepath.Join(root, chip)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, file), []byte(val), 0o644); err != nil {
		t.Fatal(err)
	}
}

func TestParseMeminfo(t *testing.T) {
	data := []byte(`MemTotal:       16000 kB
MemFree:         2000 kB
MemAvailable:    8000 kB
SwapTotal:       4000 kB
SwapFree:        3000 kB
`)
	m := parseMeminfo(data)
	const kb = 1024
	if m.TotalBytes != 16000*kb {
		t.Errorf("TotalBytes = %d", m.TotalBytes)
	}
	if m.AvailableBytes != 8000*kb {
		t.Errorf("AvailableBytes = %d", m.AvailableBytes)
	}
	if m.UsedBytes != (16000-8000)*kb {
		t.Errorf("UsedBytes = %d, want %d", m.UsedBytes, (16000-8000)*kb)
	}
	if m.SwapTotalBytes != 4000*kb || m.SwapFreeBytes != 3000*kb {
		t.Errorf("swap parsed wrong: %+v", m)
	}
}

func TestParseLoadavg(t *testing.T) {
	l := parseLoadavg("0.42 0.55 0.61 1/234 5678")
	if l.Load1 != 0.42 || l.Load5 != 0.55 || l.Load15 != 0.61 {
		t.Errorf("got %+v", l)
	}
	bad := parseLoadavg("garbage")
	if bad.Load1 != 0 || bad.Load5 != 0 || bad.Load15 != 0 {
		t.Error("short input should yield zero LoadInfo")
	}
}

func TestCpuFreqMHz(t *testing.T) {
	root := t.TempDir()
	// Hardware limits live under cpu0 only.
	write(t, root, "cpu0/cpufreq", "cpuinfo_min_freq", "400000")  // 400 MHz
	write(t, root, "cpu0/cpufreq", "cpuinfo_max_freq", "5137000") // 5137 MHz
	// Per-core current frequencies (kHz). Core 2 has the peak.
	write(t, root, "cpu0/cpufreq", "scaling_cur_freq", "2500000")
	write(t, root, "cpu1/cpufreq", "scaling_cur_freq", "1100000")
	write(t, root, "cpu2/cpufreq", "scaling_cur_freq", "3200000")
	write(t, root, "cpu3/cpufreq", "scaling_cur_freq", "2800000")

	min, max, cur := cpuFreqMHz(root)
	if min != 400 {
		t.Errorf("min = %d, want 400", min)
	}
	if max != 5137 {
		t.Errorf("max = %d, want 5137", max)
	}
	if cur != 3200 {
		t.Errorf("cur = %d, want 3200 (peak across cores)", cur)
	}

	// Absent cpufreq → all zeros.
	emptyRoot := t.TempDir()
	min, max, cur = cpuFreqMHz(emptyRoot)
	if min != 0 || max != 0 || cur != 0 {
		t.Errorf("absent cpufreq: got min=%d max=%d cur=%d, want all 0", min, max, cur)
	}
}

func TestCPUModel(t *testing.T) {
	x86 := "processor\t: 0\nmodel name\t: AMD Ryzen 7 7840U\ncpu MHz\t: 3000\n"
	if got := cpuModel(x86); got != "AMD Ryzen 7 7840U" {
		t.Errorf("x86: got %q", got)
	}
	arm := "processor\t: 0\nModel\t: Raspberry Pi 5 Model B\n"
	if got := cpuModel(arm); got != "Raspberry Pi 5 Model B" {
		t.Errorf("arm fallback: got %q", got)
	}
	if got := cpuModel("processor\t: 0\n"); got != "" {
		t.Errorf("no model: got %q", got)
	}
}

func TestReadProcStat(t *testing.T) {
	content := `cpu  100 20 30 400 10 5 3 2 0 0
cpu0 50 10 15 200 5 3 1 1 0 0
cpu1 50 10 15 200 5 2 2 1 0 0
intr 12345
`
	f := filepath.Join(t.TempDir(), "stat")
	if err := os.WriteFile(f, []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
	cores := readProcStat(f)
	if len(cores) != 2 {
		t.Fatalf("want 2 cores, got %d", len(cores))
	}
	// cpu0: user=50 nice=10 sys=15 idle=200 iowait=5 irq=3 softirq=1 steal=1 guest=0 guest_nice=0
	// total = 50+10+15+200+5+3+1+1+0+0 = 285
	// idle  = 200 + 5 = 205
	if cores[0].core != 0 {
		t.Errorf("core index = %d, want 0", cores[0].core)
	}
	if cores[0].total != 285 {
		t.Errorf("core0 total = %d, want 285", cores[0].total)
	}
	if cores[0].idle != 205 {
		t.Errorf("core0 idle = %d, want 205", cores[0].idle)
	}
}

func TestComputeUsage(t *testing.T) {
	prev := []cpuCoreTicks{
		{core: 0, total: 1000, idle: 800},
		{core: 1, total: 1000, idle: 900},
	}
	cur := []cpuCoreTicks{
		{core: 0, total: 1100, idle: 850}, // 100 total delta, 50 idle delta → 50% usage
		{core: 1, total: 1200, idle: 950}, // 200 total delta, 50 idle delta → 75% usage
	}
	usage := computeUsage(prev, cur)
	if len(usage) != 2 {
		t.Fatalf("want 2 entries, got %d", len(usage))
	}
	if usage[0].Core != 0 || usage[0].UsagePct != 50.0 {
		t.Errorf("core0: got %+v, want 50%%", usage[0])
	}
	if usage[1].Core != 1 || usage[1].UsagePct != 75.0 {
		t.Errorf("core1: got %+v, want 75%%", usage[1])
	}

	// Empty prev → empty result.
	if got := computeUsage(nil, cur); len(got) != 0 {
		t.Errorf("nil prev should yield empty, got %d", len(got))
	}
}
