//go:build linux

package agent

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

	"github.com/henrygd/beszel/agent/utils"
	"github.com/henrygd/beszel/internal/entities/system"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNormalizeHexID(t *testing.T) {
	tests := []struct {
		in   string
		want string
	}{
		{"0x1002", "1002"},
		{"C2", "c2"},
		{"  15BF  ", "15bf"},
		{"0x15bf", "15bf"},
		{"", ""},
	}
	for _, tt := range tests {
		subName := tt.in
		if subName == "" {
			subName = "empty_string"
		}
		t.Run(subName, func(t *testing.T) {
			got := normalizeHexID(tt.in)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestCacheKeyForAmdgpu(t *testing.T) {
	tests := []struct {
		deviceID   string
		revisionID string
		want       string
	}{
		{"1114", "c2", "1114:c2"},
		{"15bf", "", "15bf"},
		{"1506", "c1", "1506:c1"},
	}
	for _, tt := range tests {
		got := cacheKeyForAmdgpu(tt.deviceID, tt.revisionID)
		assert.Equal(t, tt.want, got)
	}
}

func TestReadSysfsFloat(t *testing.T) {
	dir := t.TempDir()

	validPath := filepath.Join(dir, "val")
	require.NoError(t, os.WriteFile(validPath, []byte("  42.5  \n"), 0o644))
	got, err := readSysfsFloat(validPath)
	require.NoError(t, err)
	assert.Equal(t, 42.5, got)

	// Integer and scientific
	sciPath := filepath.Join(dir, "sci")
	require.NoError(t, os.WriteFile(sciPath, []byte("1e2"), 0o644))
	got, err = readSysfsFloat(sciPath)
	require.NoError(t, err)
	assert.Equal(t, 100.0, got)

	// Missing file
	_, err = readSysfsFloat(filepath.Join(dir, "missing"))
	require.Error(t, err)

	// Invalid content
	badPath := filepath.Join(dir, "bad")
	require.NoError(t, os.WriteFile(badPath, []byte("not a number"), 0o644))
	_, err = readSysfsFloat(badPath)
	require.Error(t, err)
}

func TestIsAmdGpu(t *testing.T) {
	dir := t.TempDir()
	deviceDir := filepath.Join(dir, "device")
	require.NoError(t, os.MkdirAll(deviceDir, 0o755))

	// AMD vendor 0x1002 -> true
	require.NoError(t, os.WriteFile(filepath.Join(deviceDir, "vendor"), []byte("0x1002\n"), 0o644))
	assert.True(t, isAmdGpu(dir), "vendor 0x1002 should be AMD")

	// Non-AMD vendor -> false
	require.NoError(t, os.WriteFile(filepath.Join(deviceDir, "vendor"), []byte("0x10de\n"), 0o644))
	assert.False(t, isAmdGpu(dir), "vendor 0x10de should not be AMD")

	// Missing vendor file -> false
	require.NoError(t, os.Remove(filepath.Join(deviceDir, "vendor")))
	assert.False(t, isAmdGpu(dir), "missing vendor file should be false")
}

func TestAmdgpuNameCacheRoundTrip(t *testing.T) {
	// Cache a name and retrieve it (unique key to avoid affecting other tests)
	deviceID, revisionID := "cachedev99", "00"
	cacheAmdgpuName(deviceID, revisionID, "AMD Test GPU 99 Graphics", true)

	name, found, done := getCachedAmdgpuName(deviceID, revisionID)
	assert.True(t, found)
	assert.True(t, done)
	assert.Equal(t, "AMD Test GPU 99", name)

	// Device-only key also stored
	name2, found2, _ := getCachedAmdgpuName(deviceID, "")
	assert.True(t, found2)
	assert.Equal(t, "AMD Test GPU 99", name2)

	// Cache a miss
	cacheMissingAmdgpuName("missedev99", "ab")
	_, found3, done3 := getCachedAmdgpuName("missedev99", "ab")
	assert.False(t, found3)
	assert.True(t, done3, "done should be true so caller skips file lookup")
}

func TestUpdateAmdGpuDataWithFakeSysfs(t *testing.T) {
	tests := []struct {
		name            string
		writeGTT        bool
		wantMemoryUsed  float64
		wantMemoryTotal float64
	}{
		{
			name:            "sums vram and gtt when gtt is present",
			writeGTT:        true,
			wantMemoryUsed:  utils.BytesToMegabytes(1073741824 + 536870912),
			wantMemoryTotal: utils.BytesToMegabytes(2147483648 + 4294967296),
		},
		{
			name:            "falls back to vram when gtt is missing",
			writeGTT:        false,
			wantMemoryUsed:  utils.BytesToMegabytes(1073741824),
			wantMemoryTotal: utils.BytesToMegabytes(2147483648),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			dir := t.TempDir()
			cardPath := filepath.Join(dir, "card0")
			devicePath := filepath.Join(cardPath, "device")
			hwmonPath := filepath.Join(devicePath, "hwmon", "hwmon0")
			require.NoError(t, os.MkdirAll(hwmonPath, 0o755))

			write := func(name, content string) {
				require.NoError(t, os.WriteFile(filepath.Join(devicePath, name), []byte(content), 0o644))
			}
			write("vendor", "0x1002")
			write("device", "0x1506")
			write("revision", "0xc1")
			write("gpu_busy_percent", "25")
			write("mem_info_vram_used", "1073741824")
			write("mem_info_vram_total", "2147483648")
			if tt.writeGTT {
				write("mem_info_gtt_used", "536870912")
				write("mem_info_gtt_total", "4294967296")
			}
			require.NoError(t, os.WriteFile(filepath.Join(hwmonPath, "temp1_input"), []byte("45000"), 0o644))
			require.NoError(t, os.WriteFile(filepath.Join(hwmonPath, "power1_input"), []byte("20000000"), 0o644))

			// Pre-cache name so getAmdGpuName returns a known value (it uses system amdgpu.ids path)
			cacheAmdgpuName("1506", "c1", "AMD Radeon 610M Graphics", true)

			gm := &GPUManager{GpuDataMap: make(map[string]*system.GPUData)}
			ok := gm.updateAmdGpuData(cardPath)
			require.True(t, ok)

			gpu, ok := gm.GpuDataMap["card0"]
			require.True(t, ok)
			assert.Equal(t, "AMD Radeon 610M", gpu.Name)
			assert.Equal(t, 25.0, gpu.Usage)
			assert.Equal(t, tt.wantMemoryUsed, gpu.MemoryUsed)
			assert.Equal(t, tt.wantMemoryTotal, gpu.MemoryTotal)
			assert.Equal(t, 45.0, gpu.Temperature)
			assert.Equal(t, 20.0, gpu.Power)
			assert.Equal(t, 1.0, gpu.Count)
		})
	}
}

func TestLookupAmdgpuNameInFile(t *testing.T) {
	idsPath := filepath.Join("test-data", "amdgpu.ids")

	tests := []struct {
		name       string
		deviceID   string
		revisionID string
		wantName   string
		wantExact  bool
		wantFound  bool
	}{
		{
			name:       "exact device and revision match",
			deviceID:   "1114",
			revisionID: "c2",
			wantName:   "AMD Radeon 860M Graphics",
			wantExact:  true,
			wantFound:  true,
		},
		{
			name:       "exact match 15BF revision 01 returns 760M",
			deviceID:   "15bf",
			revisionID: "01",
			wantName:   "AMD Radeon 760M Graphics",
			wantExact:  true,
			wantFound:  true,
		},
		{
			name:       "exact match 15BF revision 00 returns 780M",
			deviceID:   "15bf",
			revisionID: "00",
			wantName:   "AMD Radeon 780M Graphics",
			wantExact:  true,
			wantFound:  true,
		},
		{
			name:       "device-only match returns first entry for device",
			deviceID:   "1506",
			revisionID: "",
			wantName:   "AMD Radeon 610M",
			wantExact:  false,
			wantFound:  true,
		},
		{
			name:       "unknown device not found",
			deviceID:   "dead",
			revisionID: "00",
			wantName:   "",
			wantExact:  false,
			wantFound:  false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotName, gotExact, gotFound := lookupAmdgpuNameInFile(tt.deviceID, tt.revisionID, idsPath)
			assert.Equal(t, tt.wantName, gotName, "name")
			assert.Equal(t, tt.wantExact, gotExact, "exact")
			assert.Equal(t, tt.wantFound, gotFound, "found")
		})
	}
}

func TestGetAmdGpuNameFromIdsFile(t *testing.T) {
	// Test that getAmdGpuName resolves a name when we can't inject the ids path.
	// We only verify behavior when product_name is missing and device/revision
	// would be read from sysfs; the actual lookup uses /usr/share/libdrm/amdgpu.ids.
	// So this test focuses on normalizeAmdgpuName and that lookupAmdgpuNameInFile
	// returns the expected name for our test-data file.
	idsPath := filepath.Join("test-data", "amdgpu.ids")
	name, exact, found := lookupAmdgpuNameInFile("1435", "ae", idsPath)
	require.True(t, found)
	require.True(t, exact)
	assert.Equal(t, "AMD Custom GPU 0932", name)
	assert.Equal(t, "AMD Custom GPU 0932", normalizeAmdgpuName(name))

	// " Graphics" suffix is trimmed by normalizeAmdgpuName
	name2 := "AMD Radeon 860M Graphics"
	assert.Equal(t, "AMD Radeon 860M", normalizeAmdgpuName(name2))
}
