package dell

import (
	"archive/zip"
	"bytes"
	"testing"

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

func TestDetectNestedTSRZip(t *testing.T) {
	inner := makeZipArchive(t, map[string][]byte{
		"tsr/metadata.json": []byte(`{"Make":"Dell Inc.","Model":"PowerEdge R750","ServiceTag":"G37Q064"}`),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_View.xml": []byte(`<CIM><MESSAGE><SIMPLEREQ/></MESSAGE></CIM>`),
	})

	p := &Parser{}
	score := p.Detect([]parser.ExtractedFile{
		{Path: "signature", Content: []byte("ok")},
		{Path: "TSR20241119143901_G37Q064.pl.zip", Content: inner},
	})
	if score < 80 {
		t.Fatalf("expected high detect score for nested TSR zip, got %d", score)
	}
}

func TestParseNestedTSRZip(t *testing.T) {
	const viewXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SystemView">
<PROPERTY NAME="Manufacturer"><VALUE>Dell Inc.</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>PowerEdge R750</VALUE></PROPERTY>
<PROPERTY NAME="ServiceTag"><VALUE>G37Q064</VALUE></PROPERTY>
<PROPERTY NAME="BIOSVersionString"><VALUE>2.19.1</VALUE></PROPERTY>
<PROPERTY NAME="LifecycleControllerVersion"><VALUE>7.00.30.00</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_CPUView">
<PROPERTY NAME="FQDD"><VALUE>CPU.Socket.1</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>Intel(R) Xeon(R) Gold 6330</VALUE></PROPERTY>
<PROPERTY NAME="Manufacturer"><VALUE>Intel</VALUE></PROPERTY>
<PROPERTY NAME="NumberOfEnabledCores"><VALUE>28</VALUE></PROPERTY>
<PROPERTY NAME="NumberOfEnabledThreads"><VALUE>56</VALUE></PROPERTY>
<PROPERTY NAME="CurrentClockSpeed"><VALUE>2000</VALUE></PROPERTY>
<PROPERTY NAME="MaxClockSpeed"><VALUE>3100</VALUE></PROPERTY>
<PROPERTY NAME="PPIN"><VALUE>ABCD</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_NICView">
<PROPERTY NAME="FQDD"><VALUE>NIC.Slot.1-1-1</VALUE></PROPERTY>
<PROPERTY NAME="ProductName"><VALUE>Broadcom 57414 Dual Port 10/25GbE SFP28 Adapter</VALUE></PROPERTY>
<PROPERTY NAME="VendorName"><VALUE>Broadcom</VALUE></PROPERTY>
<PROPERTY NAME="CurrentMACAddress"><VALUE>00:11:22:33:44:55</VALUE></PROPERTY>
<PROPERTY NAME="SerialNumber"><VALUE>NICSERIAL1</VALUE></PROPERTY>
<PROPERTY NAME="FamilyVersion"><VALUE>22.80.17</VALUE></PROPERTY>
<PROPERTY NAME="PCIVendorID"><VALUE>0x14e4</VALUE></PROPERTY>
<PROPERTY NAME="PCIDeviceID"><VALUE>0x16d7</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_PowerSupplyView">
<PROPERTY NAME="FQDD"><VALUE>PSU.Slot.1</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>D1400E-S0</VALUE></PROPERTY>
<PROPERTY NAME="Manufacturer"><VALUE>Dell</VALUE></PROPERTY>
<PROPERTY NAME="SerialNumber"><VALUE>PSUSERIAL1</VALUE></PROPERTY>
<PROPERTY NAME="FirmwareVersion"><VALUE>00.1A</VALUE></PROPERTY>
<PROPERTY NAME="TotalOutputPower"><VALUE>1400</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_VideoView">
<PROPERTY NAME="FQDD"><VALUE>Video.Slot.38-1</VALUE></PROPERTY>
<PROPERTY NAME="MarketingName"><VALUE>NVIDIA H100 PCIe</VALUE></PROPERTY>
<PROPERTY NAME="Description"><VALUE>GH100 [H100 PCIe]</VALUE></PROPERTY>
<PROPERTY NAME="Manufacturer"><VALUE>NVIDIA Corporation</VALUE></PROPERTY>
<PROPERTY NAME="PCIVendorID"><VALUE>10DE</VALUE></PROPERTY>
<PROPERTY NAME="PCIDeviceID"><VALUE>2331</VALUE></PROPERTY>
<PROPERTY NAME="BusNumber"><VALUE>74</VALUE></PROPERTY>
<PROPERTY NAME="DeviceNumber"><VALUE>0</VALUE></PROPERTY>
<PROPERTY NAME="FunctionNumber"><VALUE>0</VALUE></PROPERTY>
<PROPERTY NAME="SerialNumber"><VALUE>1793924039808</VALUE></PROPERTY>
<PROPERTY NAME="FirmwareVersion"><VALUE>96.00.AF.00.01</VALUE></PROPERTY>
<PROPERTY NAME="GPUGUID"><VALUE>bc681a6d4785dde08c21f49c46c05cc3</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	const swXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SoftwareIdentity">
<PROPERTY NAME="ElementName"><VALUE>NIC.Slot.1-1-1</VALUE></PROPERTY>
<PROPERTY NAME="VersionString"><VALUE>22.80.17</VALUE></PROPERTY>
<PROPERTY NAME="ComponentType"><VALUE>Network</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	const eventsXML = `<Log>
<Event AgentID="Lifecycle Controller" Category="System Health" Severity="Warning" Timestamp="2024-11-19T14:39:01-0800">
<MessageID>SYS1001</MessageID>
<Message>Link is down</Message>
<FQDD>NIC.Slot.1-1-1</FQDD>
</Event>
</Log>`

	const cimSensorXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_GPUSensor">
<PROPERTY NAME="DeviceID"><VALUE>Video.Slot.38-1</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryGPUTemperature"><VALUE>290</VALUE></PROPERTY>
<PROPERTY NAME="MemoryTemperature"><VALUE>440</VALUE></PROPERTY>
<PROPERTY NAME="PowerConsumption"><VALUE>295</VALUE></PROPERTY>
<PROPERTY NAME="ThermalAlertStatus"><VALUE>5</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="CIM_NumericSensor">
<PROPERTY NAME="ElementName"><VALUE>PS1 Voltage 1</VALUE></PROPERTY>
<PROPERTY NAME="CurrentReading"><VALUE>224.0</VALUE></PROPERTY>
<PROPERTY NAME="BaseUnits"><VALUE>5</VALUE></PROPERTY>
<PROPERTY NAME="UnitModifier"><VALUE>0</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>5</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	inner := makeZipArchive(t, map[string][]byte{
		"tsr/metadata.json": []byte(`{
			"Make":"Dell Inc.",
			"Model":"PowerEdge R750",
			"ServiceTag":"G37Q064",
			"FirmwareVersion":"7.00.30.00",
			"CollectionDateTime":"2024-11-19 14:39:01.000-0800"
		}`),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_View.xml":             []byte(viewXML),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_SoftwareIdentity.xml": []byte(swXML),
		"tsr/hardware/sysinfo/inventory/sysinfo_CIM_Sensor.xml":            []byte(cimSensorXML),
		"tsr/hardware/sysinfo/lcfiles/curr_lclog.xml":                      []byte(eventsXML),
	})

	p := &Parser{}
	result, err := p.Parse([]parser.ExtractedFile{
		{Path: "signature", Content: []byte("ok")},
		{Path: "TSR20241119143901_G37Q064.pl.zip", Content: inner},
	})
	if err != nil {
		t.Fatalf("parse failed: %v", err)
	}
	if result.Hardware == nil {
		t.Fatalf("expected hardware section")
	}

	if got := result.Hardware.BoardInfo.Manufacturer; got != "Dell Inc." {
		t.Fatalf("unexpected board manufacturer: %q", got)
	}
	if got := result.Hardware.BoardInfo.ProductName; got != "PowerEdge R750" {
		t.Fatalf("unexpected board product: %q", got)
	}
	if got := result.Hardware.BoardInfo.SerialNumber; got != "G37Q064" {
		t.Fatalf("unexpected service tag: %q", got)
	}

	if len(result.Hardware.CPUs) != 1 {
		t.Fatalf("expected 1 cpu, got %d", len(result.Hardware.CPUs))
	}
	if got := result.Hardware.CPUs[0].Model; got != "Intel(R) Xeon(R) Gold 6330" {
		t.Fatalf("unexpected cpu model: %q", got)
	}

	if len(result.Hardware.NetworkAdapters) != 1 {
		t.Fatalf("expected 1 network adapter, got %d", len(result.Hardware.NetworkAdapters))
	}
	adapter := result.Hardware.NetworkAdapters[0]
	if adapter.Vendor != "Broadcom" {
		t.Fatalf("unexpected nic vendor: %q", adapter.Vendor)
	}
	if adapter.Firmware != "22.80.17" {
		t.Fatalf("unexpected nic firmware: %q", adapter.Firmware)
	}
	if adapter.SerialNumber != "NICSERIAL1" {
		t.Fatalf("unexpected nic serial: %q", adapter.SerialNumber)
	}

	if len(result.Hardware.PowerSupply) != 1 {
		t.Fatalf("expected 1 psu, got %d", len(result.Hardware.PowerSupply))
	}
	psu := result.Hardware.PowerSupply[0]
	if psu.Model != "D1400E-S0" {
		t.Fatalf("unexpected psu model: %q", psu.Model)
	}
	if psu.Firmware != "00.1A" {
		t.Fatalf("unexpected psu firmware: %q", psu.Firmware)
	}

	if len(result.Hardware.Firmware) == 0 {
		t.Fatalf("expected firmware entries")
	}
	if len(result.Hardware.GPUs) != 1 {
		t.Fatalf("expected 1 gpu, got %d", len(result.Hardware.GPUs))
	}
	if got := result.Hardware.GPUs[0].Model; got != "NVIDIA H100 PCIe" {
		t.Fatalf("unexpected gpu model: %q", got)
	}
	if got := result.Hardware.GPUs[0].SerialNumber; got != "1793924039808" {
		t.Fatalf("unexpected gpu serial: %q", got)
	}
	if got := result.Hardware.GPUs[0].Temperature; got != 29 {
		t.Fatalf("unexpected gpu temperature: %d", got)
	}
	if len(result.Sensors) == 0 {
		t.Fatalf("expected sensors from CIM_Sensor")
	}
	if len(result.Events) != 1 {
		t.Fatalf("expected one lifecycle event, got %d", len(result.Events))
	}
	if got := string(result.Events[0].Severity); got != "warning" {
		t.Fatalf("unexpected event severity: %q", got)
	}
}

// TestParseDellPhysicalDiskEndurance verifies that RemainingRatedWriteEndurance from
// DCIM_PhysicalDiskView is parsed into Storage.RemainingEndurancePct.
func TestParseDellPhysicalDiskEndurance(t *testing.T) {
	const viewXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SystemView">
<PROPERTY NAME="Manufacturer"><VALUE>Dell Inc.</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>PowerEdge R6625</VALUE></PROPERTY>
<PROPERTY NAME="ServiceTag"><VALUE>8VS2LG4</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_PhysicalDiskView">
<PROPERTY NAME="FQDD"><VALUE>Disk.Bay.0:Enclosure.Internal.0-1:RAID.SL.3-1</VALUE></PROPERTY>
<PROPERTY NAME="Slot"><VALUE>0</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>HFS480G3H2X069N</VALUE></PROPERTY>
<PROPERTY NAME="SerialNumber"><VALUE>ESEAN5254I030B26B</VALUE></PROPERTY>
<PROPERTY NAME="SizeInBytes"><VALUE>479559942144</VALUE></PROPERTY>
<PROPERTY NAME="MediaType"><VALUE>Solid State Drive</VALUE></PROPERTY>
<PROPERTY NAME="BusProtocol"><VALUE>SATA</VALUE></PROPERTY>
<PROPERTY NAME="Revision"><VALUE>DZ03</VALUE></PROPERTY>
<PROPERTY NAME="RemainingRatedWriteEndurance"><VALUE>100</VALUE><DisplayValue>100 %</DisplayValue></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>1</VALUE><DisplayValue>OK</DisplayValue></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_PhysicalDiskView">
<PROPERTY NAME="FQDD"><VALUE>Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.3-1</VALUE></PROPERTY>
<PROPERTY NAME="Slot"><VALUE>1</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>TOSHIBA MG08ADA800E</VALUE></PROPERTY>
<PROPERTY NAME="SerialNumber"><VALUE>X1G0A0YXFVVG</VALUE></PROPERTY>
<PROPERTY NAME="SizeInBytes"><VALUE>8001563222016</VALUE></PROPERTY>
<PROPERTY NAME="MediaType"><VALUE>Hard Disk Drive</VALUE></PROPERTY>
<PROPERTY NAME="BusProtocol"><VALUE>SAS</VALUE></PROPERTY>
<PROPERTY NAME="Revision"><VALUE>0104</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	inner := makeZipArchive(t, map[string][]byte{
		"tsr/metadata.json": []byte(`{"Make":"Dell Inc.","Model":"PowerEdge R6625","ServiceTag":"8VS2LG4"}`),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_View.xml": []byte(viewXML),
	})

	p := &Parser{}
	result, err := p.Parse([]parser.ExtractedFile{
		{Path: "signature", Content: []byte("ok")},
		{Path: "TSR20260306141852_8VS2LG4.pl.zip", Content: inner},
	})
	if err != nil {
		t.Fatalf("parse failed: %v", err)
	}
	if len(result.Hardware.Storage) != 2 {
		t.Fatalf("expected 2 storage devices, got %d", len(result.Hardware.Storage))
	}

	ssd := result.Hardware.Storage[0]
	if ssd.RemainingEndurancePct == nil {
		t.Fatalf("SSD slot 0: expected RemainingEndurancePct to be set")
	}
	if *ssd.RemainingEndurancePct != 100 {
		t.Errorf("SSD slot 0: expected RemainingEndurancePct=100, got %d", *ssd.RemainingEndurancePct)
	}

	hdd := result.Hardware.Storage[1]
	if hdd.RemainingEndurancePct != nil {
		t.Errorf("HDD slot 1: expected RemainingEndurancePct absent, got %d", *hdd.RemainingEndurancePct)
	}
}

// TestParseDellInfiniBandView verifies that DCIM_InfiniBandView entries are parsed as
// NetworkAdapters (not PCIe devices) and that the corresponding SoftwareIdentity firmware
// entry with FQDD "InfiniBand.Slot.*" does not leak into hardware.firmware.
//
// Regression guard: PowerEdge R6625 (8VS2LG4) — "Mellanox Network Adapter" version
// "20.39.35.60" appeared in hardware.firmware because DCIM_InfiniBandView was ignored
// (device ended up only in PCIeDevices with model "16x or x16") and SoftwareIdentity
// FQDD "InfiniBand.Slot.1-1" was not filtered. (2026-03-15)
func TestParseDellInfiniBandView(t *testing.T) {
	const viewXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SystemView">
<PROPERTY NAME="Manufacturer"><VALUE>Dell Inc.</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>PowerEdge R6625</VALUE></PROPERTY>
<PROPERTY NAME="ServiceTag"><VALUE>8VS2LG4</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_InfiniBandView">
<PROPERTY NAME="FQDD"><VALUE>InfiniBand.Slot.1-1</VALUE></PROPERTY>
<PROPERTY NAME="DeviceDescription"><VALUE>InfiniBand in Slot 1 Port 1</VALUE></PROPERTY>
<PROPERTY NAME="CurrentMACAddress"><VALUE>00:1C:FD:D7:5A:E6</VALUE></PROPERTY>
<PROPERTY NAME="FamilyVersion"><VALUE>20.39.35.60</VALUE></PROPERTY>
<PROPERTY NAME="EFIVersion"><VALUE>14.32.17</VALUE></PROPERTY>
<PROPERTY NAME="PCIVendorID"><VALUE>15B3</VALUE></PROPERTY>
<PROPERTY NAME="PCIDeviceID"><VALUE>101B</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>0</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_PCIDeviceView">
<PROPERTY NAME="FQDD"><VALUE>InfiniBand.Slot.1-1</VALUE></PROPERTY>
<PROPERTY NAME="Description"><VALUE>MT28908 Family [ConnectX-6]</VALUE></PROPERTY>
<PROPERTY NAME="DeviceDescription"><VALUE>InfiniBand in Slot 1 Port 1</VALUE></PROPERTY>
<PROPERTY NAME="Manufacturer"><VALUE>Mellanox Technologies</VALUE></PROPERTY>
<PROPERTY NAME="PCIVendorID"><VALUE>15B3</VALUE></PROPERTY>
<PROPERTY NAME="PCIDeviceID"><VALUE>101B</VALUE></PROPERTY>
<PROPERTY NAME="DataBusWidth"><DisplayValue>16x or x16</DisplayValue></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_ControllerView">
<PROPERTY NAME="FQDD"><VALUE>RAID.SL.3-1</VALUE></PROPERTY>
<PROPERTY NAME="ProductName"><VALUE>PERC H755 Front</VALUE></PROPERTY>
<PROPERTY NAME="ControllerFirmwareVersion"><VALUE>52.30.0-6115</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>0</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	const swXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SoftwareIdentity">
<PROPERTY NAME="ElementName"><VALUE>Mellanox Network Adapter - 00:1C:FD:D7:5A:E6</VALUE></PROPERTY>
<PROPERTY NAME="FQDD"><VALUE>InfiniBand.Slot.1-1</VALUE></PROPERTY>
<PROPERTY NAME="VersionString"><VALUE>20.39.35.60</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SoftwareIdentity">
<PROPERTY NAME="ElementName"><VALUE>PERC H755 Front</VALUE></PROPERTY>
<PROPERTY NAME="FQDD"><VALUE>RAID.SL.3-1</VALUE></PROPERTY>
<PROPERTY NAME="VersionString"><VALUE>52.30.0-6115</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SoftwareIdentity">
<PROPERTY NAME="ElementName"><VALUE>BIOS</VALUE></PROPERTY>
<PROPERTY NAME="FQDD"><VALUE>BIOS.Setup.1-1</VALUE></PROPERTY>
<PROPERTY NAME="VersionString"><VALUE>1.15.3</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	inner := makeZipArchive(t, map[string][]byte{
		"tsr/metadata.json": []byte(`{"Make":"Dell Inc.","Model":"PowerEdge R6625","ServiceTag":"8VS2LG4"}`),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_View.xml":             []byte(viewXML),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_SoftwareIdentity.xml": []byte(swXML),
	})

	p := &Parser{}
	result, err := p.Parse([]parser.ExtractedFile{
		{Path: "signature", Content: []byte("ok")},
		{Path: "TSR20260306141852_8VS2LG4.pl.zip", Content: inner},
	})
	if err != nil {
		t.Fatalf("parse failed: %v", err)
	}

	// InfiniBand adapter must appear as a NetworkAdapter, not a PCIe device.
	if len(result.Hardware.NetworkAdapters) != 1 {
		t.Fatalf("expected 1 network adapter, got %d", len(result.Hardware.NetworkAdapters))
	}
	nic := result.Hardware.NetworkAdapters[0]
	if nic.Slot != "InfiniBand.Slot.1-1" {
		t.Errorf("unexpected NIC slot: %q", nic.Slot)
	}
	if nic.Firmware != "20.39.35.60" {
		t.Errorf("unexpected NIC firmware: %q", nic.Firmware)
	}
	if len(nic.MACAddresses) == 0 || nic.MACAddresses[0] != "00:1C:FD:D7:5A:E6" {
		t.Errorf("unexpected NIC MAC: %v", nic.MACAddresses)
	}
	// pci.ids enrichment: VendorID=0x15B3, DeviceID=0x101B → chip model + vendor name.
	if nic.Model != "MT28908 Family [ConnectX-6]" {
		t.Errorf("NIC model = %q, want MT28908 Family [ConnectX-6] (from pci.ids)", nic.Model)
	}
	if nic.Vendor != "Mellanox Technologies" {
		t.Errorf("NIC vendor = %q, want Mellanox Technologies (from pci.ids)", nic.Vendor)
	}

	// InfiniBand FQDD must NOT appear in PCIe devices.
	for _, pcie := range result.Hardware.PCIeDevices {
		if pcie.Slot == "InfiniBand.Slot.1-1" {
			t.Errorf("InfiniBand.Slot.1-1 must not appear in PCIeDevices")
		}
	}

	// Firmware entries from SoftwareIdentity and parseControllerView must carry the FQDD
	// as their Description so the exporter's isDeviceBoundFirmwareFQDD filter can remove them.
	fqddByName := make(map[string]string)
	for _, fw := range result.Hardware.Firmware {
		fqddByName[fw.DeviceName] = fw.Description
	}
	if desc := fqddByName["Mellanox Network Adapter"]; desc != "InfiniBand.Slot.1-1" {
		t.Errorf("Mellanox firmware Description = %q, want InfiniBand.Slot.1-1 for FQDD filter", desc)
	}
	if desc := fqddByName["PERC H755 Front"]; desc != "RAID.SL.3-1" {
		t.Errorf("PERC H755 Front firmware Description = %q, want RAID.SL.3-1 for FQDD filter", desc)
	}
}

// TestParseDellCPUAffinity verifies that CPUAffinity is parsed into NUMANode for
// NIC, PCIe, and controller views. "Not Applicable" must result in NUMANode=0.
func TestParseDellCPUAffinity(t *testing.T) {
	const viewXML = `<CIM><MESSAGE><SIMPLEREQ>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_SystemView">
<PROPERTY NAME="Manufacturer"><VALUE>Dell Inc.</VALUE></PROPERTY>
<PROPERTY NAME="Model"><VALUE>PowerEdge R750</VALUE></PROPERTY>
<PROPERTY NAME="ServiceTag"><VALUE>TESTST1</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_NICView">
<PROPERTY NAME="FQDD"><VALUE>NIC.Slot.2-1-1</VALUE></PROPERTY>
<PROPERTY NAME="ProductName"><VALUE>Some NIC</VALUE></PROPERTY>
<PROPERTY NAME="CPUAffinity"><VALUE>1</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>0</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_InfiniBandView">
<PROPERTY NAME="FQDD"><VALUE>InfiniBand.Slot.1-1</VALUE></PROPERTY>
<PROPERTY NAME="DeviceDescription"><VALUE>InfiniBand in Slot 1</VALUE></PROPERTY>
<PROPERTY NAME="CPUAffinity"><VALUE>2</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>0</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_ControllerView">
<PROPERTY NAME="FQDD"><VALUE>RAID.Slot.1-1</VALUE></PROPERTY>
<PROPERTY NAME="ProductName"><VALUE>PERC H755</VALUE></PROPERTY>
<PROPERTY NAME="CPUAffinity"><VALUE>Not Applicable</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>0</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
<VALUE.NAMEDINSTANCE><INSTANCE CLASSNAME="DCIM_PCIDeviceView">
<PROPERTY NAME="FQDD"><VALUE>Slot.7-1</VALUE></PROPERTY>
<PROPERTY NAME="Description"><VALUE>Some PCIe Card</VALUE></PROPERTY>
<PROPERTY NAME="CPUAffinity"><VALUE>2</VALUE></PROPERTY>
<PROPERTY NAME="PrimaryStatus"><VALUE>0</VALUE></PROPERTY>
</INSTANCE></VALUE.NAMEDINSTANCE>
</SIMPLEREQ></MESSAGE></CIM>`

	inner := makeZipArchive(t, map[string][]byte{
		"tsr/metadata.json": []byte(`{"Make":"Dell Inc.","Model":"PowerEdge R750","ServiceTag":"TESTST1"}`),
		"tsr/hardware/sysinfo/inventory/sysinfo_DCIM_View.xml": []byte(viewXML),
	})

	p := &Parser{}
	result, err := p.Parse([]parser.ExtractedFile{
		{Path: "signature", Content: []byte("ok")},
		{Path: "TSR_TESTST1.pl.zip", Content: inner},
	})
	if err != nil {
		t.Fatalf("parse failed: %v", err)
	}

	// NIC CPUAffinity=1 → NUMANode=1
	nicBySlot := make(map[string]int)
	for _, nic := range result.Hardware.NetworkAdapters {
		nicBySlot[nic.Slot] = nic.NUMANode
	}
	if nicBySlot["NIC.Slot.2-1-1"] != 1 {
		t.Errorf("NIC.Slot.2-1-1 NUMANode = %d, want 1", nicBySlot["NIC.Slot.2-1-1"])
	}
	if nicBySlot["InfiniBand.Slot.1-1"] != 2 {
		t.Errorf("InfiniBand.Slot.1-1 NUMANode = %d, want 2", nicBySlot["InfiniBand.Slot.1-1"])
	}

	// PCIe device CPUAffinity=2 → NUMANode=2; controller CPUAffinity="Not Applicable" → NUMANode=0
	pcieBySlot := make(map[string]int)
	for _, pcie := range result.Hardware.PCIeDevices {
		pcieBySlot[pcie.Slot] = pcie.NUMANode
	}
	if pcieBySlot["Slot.7-1"] != 2 {
		t.Errorf("Slot.7-1 NUMANode = %d, want 2", pcieBySlot["Slot.7-1"])
	}
	if pcieBySlot["RAID.Slot.1-1"] != 0 {
		t.Errorf("RAID.Slot.1-1 NUMANode = %d, want 0 (Not Applicable)", pcieBySlot["RAID.Slot.1-1"])
	}
}

func makeZipArchive(t *testing.T, files map[string][]byte) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	for name, content := range files {
		w, err := zw.Create(name)
		if err != nil {
			t.Fatalf("create zip entry %s: %v", name, err)
		}
		if _, err := w.Write(content); err != nil {
			t.Fatalf("write zip entry %s: %v", name, err)
		}
	}
	if err := zw.Close(); err != nil {
		t.Fatalf("close zip: %v", err)
	}
	return buf.Bytes()
}
