package exporter

import (
	"encoding/json"
	"strings"
	"testing"
	"time"

	"git.mchus.pro/mchus/logpile/internal/models"
)

func TestConvertToReanimator(t *testing.T) {
	tests := []struct {
		name    string
		input   *models.AnalysisResult
		wantErr bool
		errMsg  string
	}{
		{
			name:    "nil result",
			input:   nil,
			wantErr: true,
			errMsg:  "no data available",
		},
		{
			name: "no hardware",
			input: &models.AnalysisResult{
				Filename: "test.json",
			},
			wantErr: true,
			errMsg:  "no hardware data available",
		},
		{
			name: "no board serial",
			input: &models.AnalysisResult{
				Filename: "test.json",
				Hardware: &models.HardwareConfig{
					BoardInfo: models.BoardInfo{},
				},
			},
			wantErr: true,
			errMsg:  "board serial_number is required",
		},
		{
			name: "valid minimal data",
			input: &models.AnalysisResult{
				Filename:    "test.json",
				SourceType:  "api",
				Protocol:    "redfish",
				TargetHost:  "10.10.10.10",
				CollectedAt: time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC),
				Hardware: &models.HardwareConfig{
					BoardInfo: models.BoardInfo{
						Manufacturer: "Supermicro",
						ProductName:  "X12DPG-QT6",
						SerialNumber: "TEST123",
					},
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := ConvertToReanimator(tt.input)
			if tt.wantErr {
				if err == nil {
					t.Errorf("expected error containing %q, got nil", tt.errMsg)
				}
				return
			}
			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}
			if result == nil {
				t.Error("expected non-nil result")
				return
			}
			if result.Hardware.Board.SerialNumber != tt.input.Hardware.BoardInfo.SerialNumber {
				t.Errorf("board serial mismatch: got %q, want %q",
					result.Hardware.Board.SerialNumber,
					tt.input.Hardware.BoardInfo.SerialNumber)
			}
		})
	}
}

func TestInferCPUManufacturer(t *testing.T) {
	tests := []struct {
		model string
		want  string
	}{
		{"INTEL(R) XEON(R) GOLD 6530", "Intel"},
		{"Intel Core i9-12900K", "Intel"},
		{"AMD EPYC 7763", "AMD"},
		{"AMD Ryzen 9 5950X", "AMD"},
		{"ARM Cortex-A78", "ARM"},
		{"Ampere Altra Max", "Ampere"},
		{"Unknown CPU Model", ""},
	}

	for _, tt := range tests {
		t.Run(tt.model, func(t *testing.T) {
			got := inferCPUManufacturer(tt.model)
			if got != tt.want {
				t.Errorf("inferCPUManufacturer(%q) = %q, want %q", tt.model, got, tt.want)
			}
		})
	}
}

func TestNormalizedSerial(t *testing.T) {
	tests := []struct {
		name string
		in   string
		want string
	}{
		{
			name: "empty",
			in:   "",
			want: "",
		},
		{
			name: "n_a",
			in:   "N/A",
			want: "",
		},
		{
			name: "unknown",
			in:   "unknown",
			want: "",
		},
		{
			name: "normal",
			in:   "SN123",
			want: "SN123",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := normalizedSerial(tt.in)
			if got != tt.want {
				t.Errorf("normalizedSerial() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestInferStorageStatus(t *testing.T) {
	tests := []struct {
		name string
		stor models.Storage
		want string
	}{
		{
			name: "present",
			stor: models.Storage{
				Present: true,
			},
			want: "Unknown",
		},
		{
			name: "not present",
			stor: models.Storage{
				Present: false,
			},
			want: "Unknown",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := inferStorageStatus(tt.stor)
			if got != tt.want {
				t.Errorf("inferStorageStatus() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestNormalizeStatus_PassFail(t *testing.T) {
	if got := normalizeStatus("PASS", false); got != "OK" {
		t.Fatalf("expected PASS -> OK, got %q", got)
	}
	if got := normalizeStatus("FAIL", false); got != "Critical" {
		t.Fatalf("expected FAIL -> Critical, got %q", got)
	}
}

func TestConvertCPUs(t *testing.T) {
	cpus := []models.CPU{
		{
			Socket:       0,
			Model:        "INTEL(R) XEON(R) GOLD 6530",
			Cores:        32,
			Threads:      64,
			FrequencyMHz: 2100,
			MaxFreqMHz:   4000,
		},
		{
			Socket:       1,
			Model:        "AMD EPYC 7763",
			Cores:        64,
			Threads:      128,
			FrequencyMHz: 2450,
			MaxFreqMHz:   3500,
		},
	}

	result := convertCPUs(cpus, "2026-02-10T15:30:00Z")

	if len(result) != 2 {
		t.Fatalf("expected 2 CPUs, got %d", len(result))
	}

	if result[0].Manufacturer != "Intel" {
		t.Errorf("expected Intel manufacturer for first CPU, got %q", result[0].Manufacturer)
	}

	if result[1].Manufacturer != "AMD" {
		t.Errorf("expected AMD manufacturer for second CPU, got %q", result[1].Manufacturer)
	}

	if result[0].Status != "Unknown" {
		t.Errorf("expected Unknown status, got %q", result[0].Status)
	}
	if result[0].SerialNumber != "" {
		t.Errorf("expected empty CPU serial when source serial is absent, got %q", result[0].SerialNumber)
	}
}

func TestConvertMemory(t *testing.T) {
	memory := []models.MemoryDIMM{
		{
			Slot:         "CPU0_C0D0",
			Present:      true,
			SizeMB:       32768,
			Type:         "DDR5",
			SerialNumber: "TEST-MEM-001",
			Status:       "OK",
		},
		{
			Slot:    "CPU0_C1D0",
			Present: false,
		},
	}

	result := convertMemory(memory, "2026-02-10T15:30:00Z")

	if len(result) != 1 {
		t.Fatalf("expected 1 populated memory module, got %d", len(result))
	}

	if result[0].Status != "OK" {
		t.Errorf("expected OK status for first module, got %q", result[0].Status)
	}
}

func TestConvertMemory_KeepsInstalledDIMMWithUnknownSize(t *testing.T) {
	memory := []models.MemoryDIMM{
		{
			Slot:         "PROC 1 DIMM 3",
			Present:      true,
			SizeMB:       0,
			Manufacturer: "Hynix",
			PartNumber:   "HMCG88AEBRA115N",
			SerialNumber: "2B5F92C6",
			Status:       "OK",
		},
	}

	result := convertMemory(memory, "2026-03-30T10:00:00Z")

	if len(result) != 1 {
		t.Fatalf("expected 1 inventory-only DIMM, got %d", len(result))
	}
	if result[0].PartNumber != "HMCG88AEBRA115N" || result[0].SerialNumber != "2B5F92C6" || result[0].SizeMB != 0 {
		t.Fatalf("unexpected converted memory: %+v", result[0])
	}
}

func TestConvertToReanimator_CPUSerialIsNotSynthesizedAndSocketIsDeduped(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "cpu-dedupe.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Devices: []models.HardwareDevice{
				{
					Kind:   models.DeviceKindCPU,
					Slot:   "CPU1",
					Model:  "Xeon Platinum",
					Cores:  56,
					Status: "OK",
					Details: map[string]any{
						"socket": 1,
					},
				},
			},
			CPUs: []models.CPU{
				{Socket: 1, Model: "Xeon Platinum", Cores: 56, Status: "OK"},
				{Socket: 2, Model: "Xeon Platinum", Cores: 56, Status: "OK"},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.CPUs) != 2 {
		t.Fatalf("expected exactly two CPUs after socket dedupe, got %d", len(out.Hardware.CPUs))
	}
	for _, cpu := range out.Hardware.CPUs {
		if cpu.SerialNumber != "" {
			t.Fatalf("expected CPU serial to stay empty when source serial is absent, got %q", cpu.SerialNumber)
		}
	}
}

func TestConvertToReanimator_ExportsEventLogsAndOmitsPCIeBDFJSON(t *testing.T) {
	input := &models.AnalysisResult{
		Filename:    "events.json",
		CollectedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC),
		Events: []models.Event{
			{
				ID:          "0x0042",
				Timestamp:   time.Date(2026, 3, 15, 11, 59, 0, 0, time.UTC),
				Source:      "SEL",
				SensorName:  "CPU0_C0D0",
				Severity:    models.SeverityWarning,
				Description: "Correctable ECC error threshold exceeded",
				RawData:     "sel_record_id=42",
			},
			{
				Source:      "LOGPile",
				Severity:    models.SeverityWarning,
				Description: "internal warning should not leak to event_logs",
			},
		},
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Devices: []models.HardwareDevice{
				{
					Kind:         models.DeviceKindPCIe,
					Slot:         "",
					BDF:          "0000:18:00.0",
					DeviceClass:  "NetworkController",
					Manufacturer: "Mellanox",
					Model:        "ConnectX-6",
					Status:       "OK",
					Details: map[string]any{
						"manufactured_year_week": "2024-W07",
					},
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.EventLogs) != 1 {
		t.Fatalf("expected 1 exported event log, got %d", len(out.Hardware.EventLogs))
	}
	log := out.Hardware.EventLogs[0]
	if log.Source != "bmc" {
		t.Fatalf("expected SEL source to map to bmc, got %#v", log)
	}
	if log.ComponentRef != "CPU0_C0D0" {
		t.Fatalf("expected sensor name to map to component_ref, got %#v", log)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected 1 pcie device, got %d", len(out.Hardware.PCIeDevices))
	}
	if out.Hardware.PCIeDevices[0].Slot != "0000:18:00.0" {
		t.Fatalf("expected slot to fall back to BDF, got %#v", out.Hardware.PCIeDevices[0])
	}
	if out.Hardware.PCIeDevices[0].ManufacturedYearWeek != "2024-W07" {
		t.Fatalf("expected manufactured_year_week to be exported, got %#v", out.Hardware.PCIeDevices[0])
	}

	payload, err := json.Marshal(out)
	if err != nil {
		t.Fatalf("json.Marshal() failed: %v", err)
	}
	if strings.Contains(string(payload), `"bdf"`) {
		t.Fatalf("expected pcie bdf field to stay out of JSON payload: %s", payload)
	}
}

func TestConvertToReanimator_EventLogSourceMappingSupportsDellAndHostSyslog(t *testing.T) {
	input := &models.AnalysisResult{
		Filename:    "event-source-map.json",
		CollectedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC),
		Events: []models.Event{
			{
				ID:          "SYS1001",
				Timestamp:   time.Date(2026, 3, 15, 11, 58, 0, 0, time.UTC),
				Source:      "iDRAC",
				SensorName:  "NIC.Slot.1-1-1",
				Severity:    models.SeverityWarning,
				Description: "Link is down",
			},
			{
				ID:          "syslog_1",
				Timestamp:   time.Date(2026, 3, 15, 11, 59, 0, 0, time.UTC),
				Source:      "syslog",
				SensorName:  "systemd[1]",
				Severity:    models.SeverityInfo,
				Description: "Started Example Service",
			},
		},
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.EventLogs) != 2 {
		t.Fatalf("expected 2 event logs, got %d", len(out.Hardware.EventLogs))
	}
	if out.Hardware.EventLogs[0].Source != "bmc" {
		t.Fatalf("expected iDRAC event to map to bmc, got %#v", out.Hardware.EventLogs[0])
	}
	if out.Hardware.EventLogs[1].Source != "host" {
		t.Fatalf("expected syslog event to map to host, got %#v", out.Hardware.EventLogs[1])
	}
}

func TestConvertStorage(t *testing.T) {
	storage := []models.Storage{
		{
			Slot:         "OB01",
			Type:         "NVMe",
			Model:        "INTEL SSDPF2KX076T1",
			SerialNumber: "BTAX41900GF87P6DGN",
			Present:      true,
		},
		{
			Slot:         "OB02",
			Type:         "NVMe",
			Model:        "INTEL SSDPF2KX076T1",
			SerialNumber: "",
			Present:      true,
		},
	}

	result := convertStorage(storage, "2026-02-10T15:30:00Z")

	if len(result) != 2 {
		t.Fatalf("expected both inventory slots to be exported, got %d", len(result))
	}

	if result[0].Status != "Unknown" {
		t.Errorf("expected Unknown status, got %q", result[0].Status)
	}
	if result[1].SerialNumber != "" {
		t.Errorf("expected empty serial for second storage slot, got %q", result[1].SerialNumber)
	}
	if result[1].Present == nil || !*result[1].Present {
		t.Fatalf("expected present=true to be preserved for populated slot without serial")
	}
}

func TestConvertToReanimator_SkipsAMIVirtualStorageDevices(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "virtual-media.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Storage: []models.Storage{
				{
					Slot:         "USB_Device1_Port4",
					Type:         "HDD",
					Model:        "Virtual Cdrom Device",
					SerialNumber: "AAAABBBBCCCC1",
					Manufacturer: "American Megatrends Inc.",
					Interface:    "USB",
					Present:      true,
				},
				{
					Slot:         "OB01",
					Type:         "NVMe",
					Model:        "Memblaze PBlaze7",
					SerialNumber: "REAL-NVME-001",
					Manufacturer: "Memblaze",
					Interface:    "NVMe",
					Present:      true,
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.Storage) != 1 {
		t.Fatalf("expected only one real storage device to remain, got %d", len(out.Hardware.Storage))
	}
	if out.Hardware.Storage[0].SerialNumber != "REAL-NVME-001" {
		t.Fatalf("expected virtual AMI storage to be skipped, got %#v", out.Hardware.Storage)
	}
}

func TestConvertStorage_RemainingEndurance(t *testing.T) {
	pct100 := 100
	pct3 := 3
	storage := []models.Storage{
		{
			Slot:                  "0",
			Model:                 "HFS480G3H2X069N",
			SerialNumber:          "ESEAN5254I030B26B",
			Present:               true,
			RemainingEndurancePct: &pct100,
		},
		{
			Slot:         "1",
			Model:        "HFS480G3H2X069N",
			SerialNumber: "ESEAN5254I030B26C",
			Present:      true,
			// no endurance data
		},
		{
			Slot:                  "2",
			Model:                 "HFS480G3H2X069N",
			SerialNumber:          "ESEAN5254I030B26D",
			Present:               true,
			RemainingEndurancePct: &pct3,
		},
	}

	result := convertStorage(storage, "2026-03-15T00:00:00Z")
	if len(result) != 3 {
		t.Fatalf("expected 3 results, got %d", len(result))
	}
	if result[0].RemainingEndurancePct == nil || *result[0].RemainingEndurancePct != 100 {
		t.Errorf("slot 0: expected remaining_endurance_pct=100, got %v", result[0].RemainingEndurancePct)
	}
	if result[1].RemainingEndurancePct != nil {
		t.Errorf("slot 1: expected remaining_endurance_pct absent, got %v", *result[1].RemainingEndurancePct)
	}
	if result[2].RemainingEndurancePct == nil || *result[2].RemainingEndurancePct != 3 {
		t.Errorf("slot 2: expected remaining_endurance_pct=3, got %v", result[2].RemainingEndurancePct)
	}
}

func TestConvertPCIeDevices(t *testing.T) {
	hw := &models.HardwareConfig{
		PCIeDevices: []models.PCIeDevice{
			{
				Slot:         "PCIeCard1",
				VendorID:     32902,
				DeviceID:     2912,
				BDF:          "0000:18:00.0",
				DeviceClass:  "MassStorageController",
				Manufacturer: "Intel",
				PartNumber:   "RSP3DD080F",
				SerialNumber: "RAID-001",
			},
			{
				Slot:         "PCIeCard2",
				DeviceClass:  "NetworkController",
				Manufacturer: "Mellanox",
				SerialNumber: "", // Should be generated
			},
		},
		GPUs: []models.GPU{
			{
				Slot:         "GPU1",
				Model:        "NVIDIA A100",
				Manufacturer: "NVIDIA",
				SerialNumber: "GPU-001",
				Status:       "OK",
			},
		},
		NetworkAdapters: []models.NetworkAdapter{
			{
				Slot:         "NIC1",
				Model:        "ConnectX-6",
				Vendor:       "Mellanox",
				Present:      true,
				SerialNumber: "NIC-001",
			},
		},
	}

	result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")

	// Should have: 2 PCIe devices + 1 GPU + 1 NIC = 4 total
	if len(result) != 4 {
		t.Fatalf("expected 4 PCIe devices total, got %d", len(result))
	}

	// Missing serials must remain absent.
	if result[1].SerialNumber != "" {
		t.Errorf("expected empty serial for missing device serial, got %q", result[1].SerialNumber)
	}

	// Check GPU was included
	foundGPU := false
	for _, dev := range result {
		if dev.SerialNumber == "GPU-001" {
			foundGPU = true
			if dev.DeviceClass != "VideoController" {
				t.Errorf("expected GPU device_class VideoController, got %q", dev.DeviceClass)
			}
			break
		}
	}
	if !foundGPU {
		t.Error("expected GPU to be included in PCIe devices")
	}
}

func TestConvertPCIeDevices_NVSwitchWithoutSerialRemainsEmpty(t *testing.T) {
	hw := &models.HardwareConfig{
		Firmware: []models.FirmwareInfo{
			{
				DeviceName: "NVSwitch NVSWITCH1 (965-25612-0002-000)",
				Version:    "96.10.6D.00.01",
			},
		},
		PCIeDevices: []models.PCIeDevice{
			{
				Slot:        "NVSWITCH1",
				DeviceClass: "NVSwitch",
				BDF:         "0000:06:00.0",
				// SerialNumber empty on purpose; should remain empty.
			},
		},
	}

	result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")

	if len(result) != 1 {
		t.Fatalf("expected 1 PCIe device, got %d", len(result))
	}

	if result[0].SerialNumber != "" {
		t.Fatalf("expected empty NVSwitch serial, got %q", result[0].SerialNumber)
	}
	if result[0].Firmware != "96.10.6D.00.01" {
		t.Fatalf("expected NVSwitch firmware 96.10.6D.00.01, got %q", result[0].Firmware)
	}
}

func TestConvertToReanimator_MapsHGXNVSwitchFirmwareToPCIeDevice(t *testing.T) {
	input := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Firmware: []models.FirmwareInfo{
				{DeviceName: "HGX_FW_ERoT_NVSwitch_0", Version: "00.02.0192.0000_n00"},
				{DeviceName: "HGX_FW_NVSwitch_0", Version: "96.10.73.00.01"},
			},
			PCIeDevices: []models.PCIeDevice{
				{
					Slot:         "NVSwitch_0",
					DeviceClass:  "NVSwitch",
					Manufacturer: "NVIDIA",
					Details: map[string]any{
						"temperature_c": 31.59375,
					},
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected one NVSwitch PCIe device, got %d", len(out.Hardware.PCIeDevices))
	}
	got := out.Hardware.PCIeDevices[0]
	if got.Firmware != "96.10.73.00.01" {
		t.Fatalf("expected HGX NVSwitch firmware to map to device, got %#v", got)
	}
	if got.TemperatureC != 31.59375 {
		t.Fatalf("expected NVSwitch temperature to be exported, got %#v", got)
	}
}

func TestBuildNVSwitchFirmwareBySlot_SkipsERoTFirmware(t *testing.T) {
	got := buildNVSwitchFirmwareBySlot([]models.FirmwareInfo{
		{DeviceName: "HGX_FW_ERoT_NVSwitch_0", Version: "00.02.0192.0000_n00"},
		{DeviceName: "HGX_FW_NVSwitch_0", Version: "96.10.73.00.01"},
	})

	if len(got) != 1 {
		t.Fatalf("expected only main NVSwitch firmware to remain, got %#v", got)
	}
	if got["NVSWITCH_0"] != "96.10.73.00.01" {
		t.Fatalf("expected main NVSwitch firmware, got %#v", got)
	}
}

func TestConvertPCIeDevices_SkipsDisplayControllerDuplicates(t *testing.T) {
	hw := &models.HardwareConfig{
		PCIeDevices: []models.PCIeDevice{
			{
				Slot:        "#GPU0",
				DeviceClass: "3D Controller",
			},
		},
		GPUs: []models.GPU{
			{
				Slot:         "#GPU0",
				Model:        "B200 180GB HBM3e",
				Manufacturer: "NVIDIA",
				SerialNumber: "1655024043371",
				Status:       "OK",
			},
		},
	}

	result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
	if len(result) != 1 {
		t.Fatalf("expected only dedicated GPU record without duplicate display PCIe, got %d", len(result))
	}
	if result[0].DeviceClass != "VideoController" {
		t.Fatalf("expected GPU record with VideoController class, got %q", result[0].DeviceClass)
	}
	if result[0].Status != "OK" {
		t.Fatalf("expected GPU status OK, got %q", result[0].Status)
	}
}

func TestConvertPCIeDevices_PreservesAllGPUsWithGenericSlot(t *testing.T) {
	// Supermicro HGX BMC reports all GPU PCIe devices with Name "PCIe Device" —
	// a generic label that is not a unique hardware position. All 8 GPUs must
	// be preserved; dedup by generic slot name must not collapse them into one.
	gpus := make([]models.GPU, 8)
	serials := []string{
		"1654925165720", "1654925166160", "1654925165942", "1654925165271",
		"1654925165719", "1654925165252", "1654925165304", "1654925165587",
	}
	for i, sn := range serials {
		gpus[i] = models.GPU{
			Slot:         "PCIe Device",
			Model:        "B200 180GB HBM3e",
			Manufacturer: "NVIDIA",
			SerialNumber: sn,
			PartNumber:   "2901-886-A1",
			Status:       "OK",
		}
	}
	hw := &models.HardwareConfig{GPUs: gpus}
	result := convertPCIeDevices(hw, "2026-04-13T10:00:00Z")
	if len(result) != 8 {
		t.Fatalf("expected 8 GPU entries (one per serial), got %d", len(result))
	}
	seen := make(map[string]bool)
	for _, r := range result {
		if seen[r.SerialNumber] {
			t.Fatalf("duplicate serial %q in PCIe result", r.SerialNumber)
		}
		seen[r.SerialNumber] = true
		if r.DeviceClass != "VideoController" {
			t.Fatalf("expected VideoController device class, got %q", r.DeviceClass)
		}
	}
}

func TestConvertPCIeDevices_MapsGPUStatusHistory(t *testing.T) {
	hw := &models.HardwareConfig{
		GPUs: []models.GPU{
			{
				Slot:         "#GPU6",
				Model:        "B200 180GB HBM3e",
				Manufacturer: "NVIDIA",
				SerialNumber: "1655024043204",
				Status:       "Critical",
				StatusHistory: []models.StatusHistoryEntry{
					{
						Status:    "Critical",
						ChangedAt: time.Date(2026, 1, 12, 15, 5, 18, 0, time.UTC),
						Details:   "BIOS miss F_GPU6",
					},
				},
				ErrorDescription: "BIOS miss F_GPU6",
			},
		},
	}

	result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
	if len(result) != 1 {
		t.Fatalf("expected 1 converted GPU, got %d", len(result))
	}

	if len(result[0].StatusHistory) != 1 {
		t.Fatalf("expected 1 history entry, got %d", len(result[0].StatusHistory))
	}
	if result[0].StatusHistory[0].ChangedAt != "2026-01-12T15:05:18Z" {
		t.Fatalf("unexpected history changed_at: %q", result[0].StatusHistory[0].ChangedAt)
	}
}

func TestConvertPowerSupplies(t *testing.T) {
	psus := []models.PSU{
		{
			Slot:         "0",
			Present:      true,
			Model:        "GW-CRPS3000LW",
			Vendor:       "Great Wall",
			WattageW:     3000,
			SerialNumber: "PSU-001",
			Status:       "OK",
		},
		{
			Slot:         "1",
			Present:      false,
			SerialNumber: "", // Not present, should be skipped
		},
	}

	result := convertPowerSupplies(psus, "2026-02-10T15:30:00Z")

	if len(result) != 1 {
		t.Fatalf("expected 1 PSU (skipped empty), got %d", len(result))
	}

	if result[0].Status != "OK" {
		t.Errorf("expected OK status, got %q", result[0].Status)
	}
}

func TestConvertBoardNormalizesNULL(t *testing.T) {
	board := convertBoard(models.BoardInfo{
		Manufacturer: " NULL ",
		ProductName:  "null",
		SerialNumber: "TEST123",
	})

	if board.Manufacturer != "" {
		t.Fatalf("expected empty manufacturer, got %q", board.Manufacturer)
	}
	if board.ProductName != "" {
		t.Fatalf("expected empty product_name, got %q", board.ProductName)
	}
}

func TestSourceTypeOmittedWhenInvalidOrEmpty(t *testing.T) {
	result, err := ConvertToReanimator(&models.AnalysisResult{
		Filename:   "redfish://10.0.0.1",
		SourceType: "archive",
		TargetHost: "10.0.0.1",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "TEST123"},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	payload, err := json.Marshal(result)
	if err != nil {
		t.Fatalf("marshal failed: %v", err)
	}
	if !strings.Contains(string(payload), `"source_type":"logfile"`) {
		t.Fatalf("expected archive source_type to map to logfile, got %s", string(payload))
	}
}

func TestTargetHostOmittedWhenUnavailable(t *testing.T) {
	result, err := ConvertToReanimator(&models.AnalysisResult{
		Filename:   "test.json",
		SourceType: "api",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "TEST123"},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	payload, err := json.Marshal(result)
	if err != nil {
		t.Fatalf("marshal failed: %v", err)
	}
	if strings.Contains(string(payload), `"target_host"`) {
		t.Fatalf("expected target_host to be omitted when unavailable, got %s", string(payload))
	}
}

func TestInferTargetHost(t *testing.T) {
	tests := []struct {
		name       string
		targetHost string
		filename   string
		want       string
	}{
		{
			name:       "explicit target host wins",
			targetHost: "10.0.0.10",
			filename:   "redfish://10.0.0.20",
			want:       "10.0.0.10",
		},
		{
			name:     "hostname from URL",
			filename: "redfish://10.10.10.103",
			want:     "10.10.10.103",
		},
		{
			name:     "ip extracted from archive name",
			filename: "nvidia_bug_report_192.168.12.34.tar.gz",
			want:     "192.168.12.34",
		},
		{
			name:     "no host available",
			filename: "test.json",
			want:     "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := inferTargetHost(tt.targetHost, tt.filename)
			if got != tt.want {
				t.Fatalf("inferTargetHost() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestConvertToReanimator_DeduplicatesAllSections(t *testing.T) {
	input := &models.AnalysisResult{
		Filename:    "dup-test.json",
		CollectedAt: time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC),
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Firmware: []models.FirmwareInfo{
				{DeviceName: "BMC", Version: "1.0"},
				{DeviceName: "BMC", Version: "1.1"},
			},
			CPUs: []models.CPU{
				{Socket: 0, Model: "CPU-A"},
				{Socket: 0, Model: "CPU-A-DUP"},
			},
			Memory: []models.MemoryDIMM{
				{Slot: "DIMM_A1", Present: true, SizeMB: 32768, SerialNumber: "MEM-1", Status: "OK"},
				{Slot: "DIMM_A1", Present: true, SizeMB: 32768, SerialNumber: "MEM-1-DUP", Status: "OK"},
			},
			Storage: []models.Storage{
				{Slot: "U.2-1", SerialNumber: "SSD-1", Model: "Disk1", Present: true},
				{Slot: "U.2-2", SerialNumber: "SSD-1", Model: "Disk1-dup", Present: true},
			},
			PCIeDevices: []models.PCIeDevice{
				{Slot: "#GPU0", DeviceClass: "3D Controller", BDF: "17:00.0"},
				{Slot: "SLOT-NIC1", DeviceClass: "NetworkController", BDF: "18:00.0"},
				{Slot: "SLOT-NIC1", DeviceClass: "NetworkController", BDF: "18:00.1"},
			},
			GPUs: []models.GPU{
				{Slot: "#GPU0", Model: "B200 180GB HBM3e", SerialNumber: "GPU-1", Status: "OK"},
			},
			PowerSupply: []models.PSU{
				{Slot: "0", Present: true, SerialNumber: "PSU-1", Status: "OK"},
				{Slot: "1", Present: true, SerialNumber: "PSU-1", Status: "OK"},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}

	if len(out.Hardware.Firmware) != 1 {
		t.Fatalf("expected deduped firmware len=1, got %d", len(out.Hardware.Firmware))
	}
	if len(out.Hardware.CPUs) != 1 {
		t.Fatalf("expected cpus len=1 after socket dedupe, got %d", len(out.Hardware.CPUs))
	}
	if len(out.Hardware.Memory) != 1 {
		t.Fatalf("expected memory len=1 after slot dedupe, got %d", len(out.Hardware.Memory))
	}
	if len(out.Hardware.Storage) != 1 {
		t.Fatalf("expected deduped storage len=1, got %d", len(out.Hardware.Storage))
	}
	if len(out.Hardware.PowerSupplies) != 1 {
		t.Fatalf("expected deduped psu len=1, got %d", len(out.Hardware.PowerSupplies))
	}
	if len(out.Hardware.PCIeDevices) != 2 {
		t.Fatalf("expected pcie len=2 after final pcie-class dedupe, got %d", len(out.Hardware.PCIeDevices))
	}

	gpuCount := 0
	for _, dev := range out.Hardware.PCIeDevices {
		if dev.Slot == "#GPU0" {
			gpuCount++
		}
	}
	if gpuCount != 1 {
		t.Fatalf("expected one merged #GPU0 record, got %d", gpuCount)
	}
}

func TestConvertToReanimator_StatusFallbackUsesCollectedAt(t *testing.T) {
	collectedAt := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC)
	input := &models.AnalysisResult{
		Filename:    "status-fallback.json",
		CollectedAt: collectedAt,
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Storage: []models.Storage{
				{
					Slot:         "U.2-1",
					Model:        "PM9A3",
					SerialNumber: "SSD-001",
					Present:      true,
					Status:       "OK",
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.Storage) != 1 {
		t.Fatalf("expected 1 storage entry, got %d", len(out.Hardware.Storage))
	}

	wantTs := collectedAt.UTC().Format(time.RFC3339)
	got := out.Hardware.Storage[0]
	if got.StatusCheckedAt != wantTs {
		t.Fatalf("expected status_checked_at=%q, got %q", wantTs, got.StatusCheckedAt)
	}
}

func TestConvertToReanimator_ExportsStorageInventoryWithoutSerial(t *testing.T) {
	collectedAt := time.Date(2026, 4, 1, 9, 0, 0, 0, time.UTC)
	input := &models.AnalysisResult{
		Filename:    "nvme-inventory.json",
		CollectedAt: collectedAt,
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Storage: []models.Storage{
				{
					Slot:         "OB01",
					Type:         "NVMe",
					Model:        "PM9A3",
					SerialNumber: "SSD-001",
					Present:      true,
				},
				{
					Slot:    "OB02",
					Type:    "NVMe",
					Model:   "PM9A3",
					Present: true,
				},
				{
					Slot:    "OB03",
					Type:    "NVMe",
					Model:   "PM9A3",
					Present: false,
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.Storage) != 3 {
		t.Fatalf("expected 3 storage entries including inventory slots without serial, got %d", len(out.Hardware.Storage))
	}
	if out.Hardware.Storage[1].Slot != "OB02" || out.Hardware.Storage[1].SerialNumber != "" {
		t.Fatalf("expected OB02 storage slot without serial to survive export, got %#v", out.Hardware.Storage[1])
	}
	if out.Hardware.Storage[2].Present == nil || *out.Hardware.Storage[2].Present {
		t.Fatalf("expected OB03 to preserve present=false, got %#v", out.Hardware.Storage[2])
	}
}

func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "fw-filter-test.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			CPUs: []models.CPU{
				{Socket: 0, Model: "Intel Xeon Gold"},
			},
			Firmware: []models.FirmwareInfo{
				{DeviceName: "BIOS", Version: "1.0.0"},
				{DeviceName: "BMC", Version: "2.0.0"},
				{DeviceName: "GPU GPUSXM1 (692-2G520-0280-501)", Version: "96.00.D0.00.03"},
				{DeviceName: "NVSwitch NVSWITCH0 (965-25612-0002-000)", Version: "96.10.6D.00.01"},
				{DeviceName: "NIC #CPU1_PCIE9 (MCX512A-ACAT)", Version: "28.38.1900"},
				{DeviceName: "CPU0 Microcode", Version: "0x2b000643"},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}

	if len(out.Hardware.Firmware) != 2 {
		t.Fatalf("expected only machine-level firmware entries, got %d", len(out.Hardware.Firmware))
	}

	got := map[string]string{}
	for _, fw := range out.Hardware.Firmware {
		got[fw.DeviceName] = fw.Version
	}

	if got["BIOS"] != "1.0.0" {
		t.Fatalf("expected BIOS firmware to be kept")
	}
	if got["BMC"] != "2.0.0" {
		t.Fatalf("expected BMC firmware to be kept")
	}
	if _, exists := got["GPU GPUSXM1 (692-2G520-0280-501)"]; exists {
		t.Fatalf("expected GPU firmware to be excluded from hardware.firmware")
	}
	if _, exists := got["NVSwitch NVSWITCH0 (965-25612-0002-000)"]; exists {
		t.Fatalf("expected NVSwitch firmware to be excluded from hardware.firmware")
	}
	if len(out.Hardware.CPUs) != 1 {
		t.Fatalf("expected 1 CPU entry, got %d", len(out.Hardware.CPUs))
	}
	if out.Hardware.CPUs[0].Firmware != "0x2b000643" {
		t.Fatalf("expected CPU firmware field to carry microcode, got %q", out.Hardware.CPUs[0].Firmware)
	}
}

// TestConvertToReanimator_FirmwareExcludesDellFQDDEntries verifies that Dell TSR
// SoftwareIdentity firmware entries whose Description contains a device-bound FQDD
// (InfiniBand.Slot.*, RAID.SL.*, etc.) are filtered from hardware.firmware.
//
// Regression guard: PowerEdge R6625 (8VS2LG4) — "Mellanox Network Adapter" (FQDD
// InfiniBand.Slot.1-1) and "PERC H755 Front" (FQDD RAID.SL.3-1) leaked into
// hardware.firmware. (2026-03-15)
func TestConvertToReanimator_FirmwareExcludesDellFQDDEntries(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "dell-fw-filter-test.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "8VS2LG4"},
			Firmware: []models.FirmwareInfo{
				// system-level — must be kept
				{DeviceName: "BIOS", Version: "1.15.3", Description: "system bios"},
				{DeviceName: "iDRAC", Version: "7.20.80.50", Description: "idrac card"},
				{DeviceName: "Lifecycle Controller", Version: "7.20.80.50", Description: "idrac lifecycle"},
				// device-bound via FQDD — must be filtered
				{DeviceName: "Mellanox Network Adapter", Version: "20.39.35.60", Description: "InfiniBand.Slot.1-1"},
				{DeviceName: "PERC H755 Front", Version: "52.30.0-6115", Description: "RAID.SL.3-1"},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}

	got := make(map[string]string, len(out.Hardware.Firmware))
	for _, fw := range out.Hardware.Firmware {
		got[fw.DeviceName] = fw.Version
	}

	for _, keep := range []string{"BIOS", "iDRAC", "Lifecycle Controller"} {
		if _, ok := got[keep]; !ok {
			t.Errorf("expected %q in hardware.firmware, but it was missing", keep)
		}
	}
	for _, drop := range []string{"Mellanox Network Adapter", "PERC H755 Front"} {
		if _, ok := got[drop]; ok {
			t.Errorf("%q must not appear in hardware.firmware (device-bound FQDD)", drop)
		}
	}
}

func TestConvertToReanimator_UsesCanonicalDevices(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "canonical.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Devices: []models.HardwareDevice{
				{
					Kind:         models.DeviceKindCPU,
					Slot:         "CPU0",
					Model:        "INTEL(R) XEON(R)",
					Cores:        32,
					Threads:      64,
					FrequencyMHz: 2100,
				},
				{
					Kind:         models.DeviceKindStorage,
					Slot:         "U.2-1",
					Model:        "Disk1",
					SerialNumber: "SSD-1",
					Present:      boolPtr(true),
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.CPUs) != 1 {
		t.Fatalf("expected cpu from hardware.devices, got %d", len(out.Hardware.CPUs))
	}
	if len(out.Hardware.Storage) != 1 {
		t.Fatalf("expected storage from hardware.devices, got %d", len(out.Hardware.Storage))
	}
}

func TestConvertToReanimator_MergesCanonicalAndLegacyDevices(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "merged.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Devices: []models.HardwareDevice{
				{
					Kind:        models.DeviceKindPCIe,
					Slot:        "PCIe 3",
					Model:       "RAID Controller",
					DeviceClass: "raid_controller",
					Status:      "ok",
				},
			},
			CPUs: []models.CPU{
				{Socket: 0, Model: "Xeon Platinum", SerialNumber: "CPU-001"},
			},
			Memory: []models.MemoryDIMM{
				{Slot: "DIMM0", Location: "DIMM0", Present: true, SizeMB: 32768, Type: "DDR5", SerialNumber: "MEM-001"},
			},
			Storage: []models.Storage{
				{Slot: "U.2-1", Type: "NVMe", Model: "Drive1", SerialNumber: "SSD-001", Present: true},
			},
			PowerSupply: []models.PSU{
				{Slot: "PSU0", Model: "PSU", SerialNumber: "PSU-001", Present: true},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.CPUs) != 1 {
		t.Fatalf("expected cpu from legacy inventory to survive canonical merge, got %d", len(out.Hardware.CPUs))
	}
	if len(out.Hardware.Memory) != 1 {
		t.Fatalf("expected memory from legacy inventory to survive canonical merge, got %d", len(out.Hardware.Memory))
	}
	if len(out.Hardware.Storage) != 1 {
		t.Fatalf("expected storage from legacy inventory to survive canonical merge, got %d", len(out.Hardware.Storage))
	}
	if len(out.Hardware.PowerSupplies) != 1 {
		t.Fatalf("expected psu from legacy inventory to survive canonical merge, got %d", len(out.Hardware.PowerSupplies))
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected supplemental canonical pcie device to remain present, got %d", len(out.Hardware.PCIeDevices))
	}
}

func TestConvertToReanimator_DoesNotMergeStorageIntoPCIeBySharedSerial(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "nvme-redfish.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Storage: []models.Storage{
				{
					Slot:         "Disk.Bay.0",
					Type:         "NVMe",
					Model:        "MZQL21T9HCJR-00A07",
					SerialNumber: "S64GNNFX612200",
					Manufacturer: "Samsung",
					Firmware:     "GDC5A02Q",
					Present:      true,
				},
			},
			PCIeDevices: []models.PCIeDevice{
				{
					Slot:         "NVMeSSD1",
					BDF:          "0000:81:00.0",
					DeviceClass:  "MassStorageController",
					Description:  "MZQL21T9HCJR-00A07",
					SerialNumber: "S64GNNFX612200",
					Manufacturer: "Samsung",
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.Storage) != 1 {
		t.Fatalf("expected storage record to survive shared-serial canonical merge, got %d", len(out.Hardware.Storage))
	}
	if out.Hardware.Storage[0].Slot != "Disk.Bay.0" {
		t.Fatalf("expected storage slot Disk.Bay.0, got %q", out.Hardware.Storage[0].Slot)
	}
	if len(out.Hardware.PCIeDevices) != 0 {
		t.Fatalf("expected NVMe storage endpoint to be excluded from pcie export, got %d records", len(out.Hardware.PCIeDevices))
	}
}

func TestConvertToReanimator_LeavesStorageControllersInPCIe(t *testing.T) {
	input := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
			PCIeDevices: []models.PCIeDevice{
				{
					Slot:         "PCIe Slot 3",
					BDF:          "0000:5e:00.0",
					DeviceClass:  "MassStorageController",
					Description:  "MegaRAID Controller",
					PartNumber:   "PERC H755",
					SerialNumber: "RAID-001",
					Manufacturer: "Dell",
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected RAID controller to remain in pcie export, got %d", len(out.Hardware.PCIeDevices))
	}
}

func TestConvertToReanimator_PCIePlaceholderModelFallsBackToPCIIDs(t *testing.T) {
	input := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
			Devices: []models.HardwareDevice{
				{
					Kind:         models.DeviceKindNetwork,
					Slot:         "NIC1",
					Model:        "Network Device View",
					VendorID:     0x15b3,
					DeviceID:     0x101d,
					Manufacturer: "Mellanox",
					Present:      boolPtr(true),
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected one pcie export, got %d", len(out.Hardware.PCIeDevices))
	}
	if strings.EqualFold(out.Hardware.PCIeDevices[0].Model, "Network Device View") {
		t.Fatalf("expected placeholder model to be replaced, got %q", out.Hardware.PCIeDevices[0].Model)
	}
	if out.Hardware.PCIeDevices[0].SerialNumber != "" {
		t.Fatalf("expected missing pcie serial to stay empty, got %q", out.Hardware.PCIeDevices[0].SerialNumber)
	}
}

func TestConvertToReanimator_SkipsPlaceholderNetworkPCIeRecords(t *testing.T) {
	input := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
			Devices: []models.HardwareDevice{
				{
					Kind:   models.DeviceKindNetwork,
					Slot:   "1",
					Status: "Unknown",
				},
				{
					Kind:         models.DeviceKindNetwork,
					Slot:         "NIC2",
					Model:        "ConnectX-7",
					Manufacturer: "NVIDIA",
					Present:      boolPtr(true),
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected only one meaningful pcie-class device, got %d", len(out.Hardware.PCIeDevices))
	}
	if out.Hardware.PCIeDevices[0].Slot != "NIC2" {
		t.Fatalf("expected placeholder numeric-slot NIC to be skipped, got %+v", out.Hardware.PCIeDevices)
	}
}

func TestConvertToReanimator_ExportsSensorsAndPSUTelemetry(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "vitals.json",
		Sensors: []models.SensorReading{
			{Name: "FAN1", Type: "fan", Value: 4200, Unit: "RPM", Status: "OK"},
			{Name: "12V Rail", Type: "voltage", Value: 12.1, Unit: "V", Status: "OK"},
			{Name: "CPU0 Temp", Type: "temperature", Value: 71, Unit: "C", Status: "Warning"},
			{Name: "Humidity", Type: "humidity", Value: 38.5, Unit: "%", Status: "OK"},
		},
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Devices: []models.HardwareDevice{
				{
					Kind:         models.DeviceKindGPU,
					Slot:         "#GPU0",
					Model:        "B200 180GB HBM3e",
					SerialNumber: "GPU-001",
					BDF:          "0000:17:00.0",
				},
				{
					Kind:         models.DeviceKindPSU,
					Slot:         "PSU0",
					SerialNumber: "PSU-001",
					Present:      boolPtr(true),
					InputPowerW:  1400,
					OutputPowerW: 1300,
					InputVoltage: 229.5,
					TemperatureC: 44,
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}

	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected one pcie device, got %d", len(out.Hardware.PCIeDevices))
	}
	pcie := out.Hardware.PCIeDevices[0]
	if pcie.TemperatureC != 0 {
		t.Fatalf("expected canonical GPU telemetry to stay off the component unless sourced from details/gpu path, got %.2f", pcie.TemperatureC)
	}

	if len(out.Hardware.PowerSupplies) != 1 {
		t.Fatalf("expected one PSU, got %d", len(out.Hardware.PowerSupplies))
	}
	psu := out.Hardware.PowerSupplies[0]
	if psu.InputPowerW != 1400 {
		t.Fatalf("expected PSU input power 1400W, got %.2f", psu.InputPowerW)
	}
	if psu.TemperatureC != 44 {
		t.Fatalf("expected PSU temperature 44C, got %.2f", psu.TemperatureC)
	}
	if out.Hardware.Sensors == nil {
		t.Fatalf("expected sensors section")
	}
	if len(out.Hardware.Sensors.Fans) != 1 || out.Hardware.Sensors.Fans[0].RPM != 4200 {
		t.Fatalf("expected fan sensor export, got %#v", out.Hardware.Sensors.Fans)
	}
	if len(out.Hardware.Sensors.Power) != 1 || out.Hardware.Sensors.Power[0].VoltageV != 12.1 {
		t.Fatalf("expected power sensor export, got %#v", out.Hardware.Sensors.Power)
	}
	if len(out.Hardware.Sensors.Temperatures) != 1 || out.Hardware.Sensors.Temperatures[0].Celsius != 71 {
		t.Fatalf("expected temperature sensor export, got %#v", out.Hardware.Sensors.Temperatures)
	}
	if len(out.Hardware.Sensors.Other) != 1 || out.Hardware.Sensors.Other[0].Unit != "%" {
		t.Fatalf("expected other sensor export, got %#v", out.Hardware.Sensors.Other)
	}
}

func TestConvertToReanimator_SkipsSensorsWithoutNumericReadings(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "sensor-gaps.json",
		Sensors: []models.SensorReading{
			{Name: "CPU0 Temp", Type: "temperature", Status: "OK", RawValue: "N/A"},
			{Name: "PSU1 Power", Type: "power", Status: "OK", RawValue: ""},
			{Name: "Fan1", Type: "fan", Status: "OK", RawValue: "not present"},
			{Name: "Humidity", Type: "humidity", Status: "OK", RawValue: "unknown"},
		},
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if out.Hardware.Sensors != nil {
		t.Fatalf("expected sensors to be omitted when all readings are non-numeric, got %+v", out.Hardware.Sensors)
	}
}

func TestConvertToReanimator_MergesSiblingPowerSensors(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "power-sensors.json",
		Sensors: []models.SensorReading{
			{Name: "Power Supply Bay 8_InputPower", Type: "power", Value: 231, Unit: "W", Status: "OK"},
			{Name: "Power Supply Bay 8_InputVoltage", Type: "voltage", Value: 228, Unit: "V", Status: "OK"},
		},
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if out.Hardware.Sensors == nil || len(out.Hardware.Sensors.Power) != 1 {
		t.Fatalf("expected one merged power sensor, got %#v", out.Hardware.Sensors)
	}
	got := out.Hardware.Sensors.Power[0]
	if got.Name != "Power Supply Bay 8" {
		t.Fatalf("expected merged sensor name, got %q", got.Name)
	}
	if got.PowerW != 231 || got.VoltageV != 228 {
		t.Fatalf("expected merged power/voltage readings, got %#v", got)
	}
	if got.Location != "" {
		t.Fatalf("expected sensor location to be omitted, got %#v", got)
	}
}

func TestConvertToReanimator_MergesInspurPSUInputAndOutputSensors(t *testing.T) {
	input := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
		},
		Sensors: []models.SensorReading{
			{Name: "PSU0_VIN", Type: "voltage", Value: 224, Unit: "V", Status: "OK"},
			{Name: "PSU0_PIN", Type: "power", Value: 120, Unit: "W", Status: "OK"},
			{Name: "PSU0_VOUT", Type: "voltage", Value: 12, Unit: "V", Status: "OK"},
			{Name: "PSU0_POUT", Type: "power", Value: 88, Unit: "W", Status: "OK"},
			{Name: "PSU0_OutputPower", Type: "power", Value: 95, Unit: "W", Status: "OK"},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if out.Hardware.Sensors == nil || len(out.Hardware.Sensors.Power) != 2 {
		t.Fatalf("expected 2 grouped PSU power sensors, got %#v", out.Hardware.Sensors)
	}

	byName := map[string]ReanimatorPowerSensor{}
	for _, item := range out.Hardware.Sensors.Power {
		byName[item.Name] = item
	}
	if got, ok := byName["PSU0"]; !ok || got.VoltageV != 224 || got.PowerW != 120 {
		t.Fatalf("expected PSU0 input group with VIN/PIN merged, got %#v", byName)
	}
	if got, ok := byName["PSU0_Output"]; !ok || got.VoltageV != 12 || got.PowerW != 95 {
		t.Fatalf("expected PSU0 output group with VOUT/POUT merged, got %#v", byName)
	}
}

func TestConvertToReanimator_PreservesCanonicalDedupWithoutDeviceVitals(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "dedup-vitals.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			PCIeDevices: []models.PCIeDevice{
				{
					Slot:         "#GPU0",
					BDF:          "0000:17:00.0",
					DeviceClass:  "3D Controller",
					PartNumber:   "Generic Display",
					Manufacturer: "NVIDIA",
					SerialNumber: "GPU-SN-001",
				},
			},
			GPUs: []models.GPU{
				{
					Slot:         "#GPU0",
					BDF:          "0000:17:00.0",
					Model:        "B200 180GB HBM3e",
					Manufacturer: "NVIDIA",
					SerialNumber: "GPU-SN-001",
					Temperature:  67,
					Power:        330,
					Status:       "OK",
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected deduped one pcie entry, got %d", len(out.Hardware.PCIeDevices))
	}
	got := out.Hardware.PCIeDevices[0]
	if got.DeviceClass != "VideoController" {
		t.Fatalf("expected GPU to export as VideoController, got %q", got.DeviceClass)
	}
	if got.TemperatureC != 67 {
		t.Fatalf("expected deduped GPU temperature 67C, got %.2f", got.TemperatureC)
	}
	if got.PowerW != 330 {
		t.Fatalf("expected deduped GPU power 330W, got %.2f", got.PowerW)
	}
}

func TestConvertToReanimator_DedupesLooseCanonicalNICAndPCIeEntries(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "loose-dedup.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			PCIeDevices: []models.PCIeDevice{
				{
					Slot:         "Slot 4",
					DeviceClass:  "NetworkController",
					VendorID:     0x15b3,
					DeviceID:     0x1021,
					Manufacturer: "Mellanox",
					PartNumber:   "MCX623106AC-CDAT",
				},
			},
			NetworkAdapters: []models.NetworkAdapter{
				{
					Slot:         "Slot 4",
					Model:        "ConnectX-6",
					VendorID:     0x15b3,
					DeviceID:     0x1021,
					Vendor:       "Mellanox",
					MACAddresses: []string{"00:11:22:33:44:55"},
					PortCount:    2,
					Present:      true,
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 1 {
		t.Fatalf("expected one merged loose-key pcie entry, got %d", len(out.Hardware.PCIeDevices))
	}
	got := out.Hardware.PCIeDevices[0]
	if got.Model == "" {
		t.Fatalf("expected merged pcie entry to retain a model, got empty")
	}
	if len(got.MACAddresses) != 1 || got.MACAddresses[0] != "00:11:22:33:44:55" {
		t.Fatalf("expected MACs from NIC side after loose merge, got %#v", got.MACAddresses)
	}
}

func TestConvertToReanimator_ExportsContractV24Telemetry(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "contract-v24.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Devices: []models.HardwareDevice{
				{
					Kind:  models.DeviceKindCPU,
					Slot:  "CPU0",
					Model: "INTEL(R) XEON(R) GOLD 6530",
					Details: map[string]any{
						"socket":                    0,
						"temperature_c":             61.5,
						"power_w":                   182.0,
						"throttled":                 false,
						"correctable_error_count":   int64(4),
						"uncorrectable_error_count": int64(1),
						"life_remaining_pct":        98.5,
						"life_used_pct":             1.5,
					},
				},
				{
					Kind:         models.DeviceKindMemory,
					Slot:         "DIMM_A1",
					SerialNumber: "MEM-001",
					Present:      boolPtr(true),
					SizeMB:       32768,
					Type:         "DDR5",
					Details: map[string]any{
						"temperature_c":                 43.0,
						"correctable_ecc_error_count":   int64(2),
						"uncorrectable_ecc_error_count": int64(0),
						"life_remaining_pct":            99.0,
						"spare_blocks_remaining_pct":    97.0,
						"performance_degraded":          false,
					},
				},
				{
					Kind:         models.DeviceKindStorage,
					Slot:         "U.2-1",
					SerialNumber: "SSD-001",
					Model:        "PM9A3",
					Present:      boolPtr(true),
					Details: map[string]any{
						"temperature_c":         38.5,
						"power_on_hours":        int64(12450),
						"unsafe_shutdowns":      int64(3),
						"written_bytes":         int64(9876543210),
						"life_remaining_pct":    91.0,
						"available_spare_pct":   88.0,
						"offline_uncorrectable": int64(0),
					},
				},
				{
					Kind:         models.DeviceKindPCIe,
					Slot:         "PCIeCard2",
					SerialNumber: "NIC-001",
					DeviceClass:  "EthernetController",
					NUMANode:     1,
					Details: map[string]any{
						"temperature_c":       48.5,
						"power_w":             18.2,
						"life_remaining_pct":  95.0,
						"ecc_corrected_total": int64(12),
						"battery_health_pct":  87.0,
						"sfp_temperature_c":   36.2,
						"sfp_tx_power_dbm":    -1.8,
						"sfp_rx_power_dbm":    -2.1,
						"sfp_bias_ma":         5.5,
					},
				},
				{
					Kind:         models.DeviceKindPSU,
					Slot:         "PSU0",
					SerialNumber: "PSU-001",
					Present:      boolPtr(true),
					Details: map[string]any{
						"life_remaining_pct": 97.0,
						"life_used_pct":      3.0,
					},
					TemperatureC: 39,
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}

	if got := out.Hardware.CPUs[0]; got.TemperatureC != 61.5 || got.PowerW != 182.0 || got.Throttled == nil || *got.Throttled {
		t.Fatalf("unexpected CPU telemetry: %#v", got)
	}
	if got := out.Hardware.Memory[0]; got.TemperatureC != 43.0 || got.CorrectableECCErrorCount != 2 || got.PerformanceDegraded == nil || *got.PerformanceDegraded {
		t.Fatalf("unexpected memory telemetry: %#v", got)
	}
	if got := out.Hardware.Storage[0]; got.TemperatureC != 38.5 || got.PowerOnHours != 12450 || got.LifeRemainingPct != 91.0 {
		t.Fatalf("unexpected storage telemetry: %#v", got)
	}
	if got := out.Hardware.PCIeDevices[0]; got.NUMANode != 1 || got.TemperatureC != 48.5 || got.PowerW != 18.2 || got.SFPTemperatureC != 36.2 {
		t.Fatalf("unexpected PCIe telemetry: %#v", got)
	}
	if got := out.Hardware.PowerSupplies[0]; got.TemperatureC != 39 || got.LifeRemainingPct != 97.0 || got.LifeUsedPct != 3.0 {
		t.Fatalf("unexpected PSU telemetry: %#v", got)
	}
}

func TestConvertToReanimator_UnifiesEthernetAndNetworkControllers(t *testing.T) {
	input := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
			Devices: []models.HardwareDevice{
				{
					Kind:         models.DeviceKindPCIe,
					Slot:         "PCIe1",
					DeviceClass:  "EthernetController",
					Present:      boolPtr(true),
					SerialNumber: "ETH-001",
				},
				{
					Kind:         models.DeviceKindNetwork,
					Slot:         "NIC1",
					Model:        "Ethernet Adapter",
					Present:      boolPtr(true),
					SerialNumber: "NIC-001",
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 2 {
		t.Fatalf("expected two pcie-class exports, got %d", len(out.Hardware.PCIeDevices))
	}
	for _, dev := range out.Hardware.PCIeDevices {
		if dev.DeviceClass != "NetworkController" {
			t.Fatalf("expected unified NetworkController class, got %+v", dev)
		}
	}
}

func TestConvertToReanimator_PreservesLegacyStorageAndPSUDetails(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "legacy-details.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			Storage: []models.Storage{
				{
					Slot:         "Drive0",
					Type:         "NVMe",
					Model:        "NVMe SSD",
					SerialNumber: "SSD-001",
					Present:      true,
					Details: map[string]any{
						"temperature_c":      38.5,
						"power_on_hours":     int64(12450),
						"life_remaining_pct": 91.0,
					},
				},
			},
			PowerSupply: []models.PSU{
				{
					Slot:         "PSU0",
					Model:        "PSU",
					SerialNumber: "PSU-001",
					Present:      true,
					Details: map[string]any{
						"temperature_c":      41.0,
						"life_remaining_pct": 96.0,
					},
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if got := out.Hardware.Storage[0]; got.TemperatureC != 38.5 || got.PowerOnHours != 12450 || got.LifeRemainingPct != 91.0 {
		t.Fatalf("expected storage details from legacy model to survive canonical conversion, got %+v", got)
	}
	if got := out.Hardware.PowerSupplies[0]; got.TemperatureC != 41.0 || got.LifeRemainingPct != 96.0 {
		t.Fatalf("expected psu details from legacy model to survive canonical conversion, got %+v", got)
	}
}

func TestConvertToReanimator_PreservesLegacyPCIeAndNICDetails(t *testing.T) {
	input := &models.AnalysisResult{
		Filename: "legacy-pcie-details.json",
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
			PCIeDevices: []models.PCIeDevice{
				{
					Slot:         "PCIe 1",
					BDF:          "0000:17:00.0",
					VendorID:     0x10de,
					DeviceID:     0x2331,
					PartNumber:   "H100",
					Manufacturer: "NVIDIA",
					SerialNumber: "GPU-001",
					Details: map[string]any{
						"temperature_c":       48.5,
						"power_w":             315.0,
						"ecc_corrected_total": int64(12),
						"battery_health_pct":  87.0,
					},
				},
			},
			NetworkAdapters: []models.NetworkAdapter{
				{
					Slot:         "Slot 4",
					BDF:          "0000:18:00.0",
					VendorID:     0x15b3,
					DeviceID:     0x1021,
					Model:        "ConnectX-6",
					SerialNumber: "NIC-001",
					Present:      true,
					Details: map[string]any{
						"sfp_temperature_c": 34.0,
						"sfp_tx_power_dbm":  -1.8,
						"sfp_rx_power_dbm":  -2.1,
					},
				},
			},
		},
	}

	out, err := ConvertToReanimator(input)
	if err != nil {
		t.Fatalf("ConvertToReanimator() failed: %v", err)
	}
	if len(out.Hardware.PCIeDevices) != 2 {
		t.Fatalf("expected two pcie-class devices, got %d", len(out.Hardware.PCIeDevices))
	}
	foundGPU := false
	foundNIC := false
	for _, dev := range out.Hardware.PCIeDevices {
		switch dev.SerialNumber {
		case "GPU-001":
			foundGPU = true
			if dev.TemperatureC != 48.5 || dev.PowerW != 315.0 || dev.ECCCorrectedTotal != 12 || dev.BatteryHealthPct != 87.0 {
				t.Fatalf("expected GPU telemetry preserved, got %+v", dev)
			}
		case "NIC-001":
			foundNIC = true
			if dev.SFPTemperatureC != 34.0 || dev.SFPTXPowerDBm != -1.8 || dev.SFPRXPowerDBm != -2.1 {
				t.Fatalf("expected NIC sfp telemetry preserved, got %+v", dev)
			}
		}
	}
	if !foundGPU || !foundNIC {
		t.Fatalf("expected both gpu and nic pcie-class exports, got %+v", out.Hardware.PCIeDevices)
	}
}

func boolPtr(v bool) *bool { return &v }

// TestIsDeviceBoundFirmwareName verifies that device-bound firmware entries from
// Supermicro Redfish FirmwareInventory are correctly identified and excluded from
// hardware.firmware.
//
// Regression guard: names like "GPU1 System Slot0" and "NIC1 System Slot0 ..." were
// not caught because the old check required "gpu " / "nic " (with space), while
// Supermicro places a digit immediately after the type prefix. (2026-03-12)
func TestIsDeviceBoundFirmwareName(t *testing.T) {
	cases := []struct {
		name string
		want bool
	}{
		// Supermicro Redfish — device-bound, must be excluded
		{"GPU1 System Slot0", true},
		{"GPU8 System Slot0", true},
		{"NIC1 System Slot0 AOM-DP805-IO", true},
		{"NIC9 System Slot8 MCX75310AAS-NEAT", true},
		{"NVMeController1", true},
		{"Power supply 1", true},
		{"Power supply 6", true},
		{"Software Inventory", true},
		{"software inventory", true}, // case-insensitive
		// Generic / legacy names already covered before this fix
		{"GPU SomeDevice", true},
		{"NIC OnboardLAN", true},
		{"PSU1", true},
		{"NVMe Drive", true},
		// HGX FW ID patterns (in case Id is used as name)
		{"HGX_FW_GPU_SXM_1", true},
		{"HGX_FW_ERoT_NVSwitch_0", true},
		{"HGX_InfoROM_GPU_SXM_2", true},
		// System-level firmware — must NOT be excluded
		{"BIOS", false},
		{"BMC", false},
		{"BMC Backup", false},
		{"Capsule BIOS", false},
		{"Capsule ME", false},
		{"CPLD Motherboard Golden", false},
		{"CPLD AOMboard", false},
		{"FrontFanboard CPLD", false},
		{"Motherboard PCIeSwitch 1", false}, // board-integrated, no device record
		{"SecureBoot", false},
		{"BIOS ME", false},
	}
	for _, tc := range cases {
		got := isDeviceBoundFirmwareName(tc.name)
		if got != tc.want {
			t.Errorf("isDeviceBoundFirmwareName(%q) = %v, want %v", tc.name, got, tc.want)
		}
	}
}

// TestIsDeviceBoundFirmwareFQDD verifies that Dell TSR SoftwareIdentity FQDD strings
// (stored in FirmwareInfo.Description) correctly identify device-bound entries.
//
// Regression guard: "InfiniBand.Slot.1-1" (Mellanox ConnectX-6) and "RAID.SL.3-1"
// (PERC H755 Front) were not filtered because only "raid.backplane." was listed and
// "infiniband." was absent. Both firmware entries leaked into hardware.firmware on
// PowerEdge R6625 (8VS2LG4). (2026-03-15)
func TestIsDeviceBoundFirmwareFQDD(t *testing.T) {
	cases := []struct {
		desc string
		want bool
	}{
		// Dell TSR SoftwareIdentity FQDDs — device-bound, must be excluded
		{"InfiniBand.Slot.1-1", true},       // Mellanox ConnectX-6
		{"InfiniBand.Slot.2-1", true},       // any InfiniBand slot
		{"RAID.SL.3-1", true},               // PERC H755 Front
		{"RAID.Integrated.1-1", true},       // embedded RAID controller
		{"RAID.Backplane.Firmware.0", true}, // backplane (previously covered)
		{"NIC.Integrated.1-1-1", true},      // embedded NIC
		{"NIC.Slot.1-1-1", true},            // slotted NIC
		{"PSU.Slot.1", true},                // PSU
		{"Disk.Bay.0:Enclosure.Internal.0-1:RAID.SL.3-1", true},
		{"GPU.Slot.1-1", true},
		{"FC.Slot.1-1", true}, // Fibre Channel HBA
		// System-level descriptions — must NOT be excluded
		{"system bios", false},
		{"idrac lifecycle", false},
		{"idrac card", false},
		{"storage controller", false}, // legacy description before fqdd fix
		{"", false},
	}
	for _, tc := range cases {
		got := isDeviceBoundFirmwareFQDD(tc.desc)
		if got != tc.want {
			t.Errorf("isDeviceBoundFirmwareFQDD(%q) = %v, want %v", tc.desc, got, tc.want)
		}
	}
}
