package server

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

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

func TestBuildHardwareDevices_DedupSerialThenBDF(t *testing.T) {
	hw := &models.HardwareConfig{
		PCIeDevices: []models.PCIeDevice{
			{Slot: "A1", SerialNumber: "SER-1", BDF: "0000:01:00.0", DeviceClass: "NetworkController"},
			{Slot: "A2", SerialNumber: "SER-1", BDF: "0000:02:00.0", DeviceClass: "NetworkController"},
			{Slot: "B1", SerialNumber: "", BDF: "0000:03:00.0", DeviceClass: "NetworkController"},
			{Slot: "B2", SerialNumber: "", BDF: "0000:03:00.0", DeviceClass: "NetworkController"},
			{Slot: "C1", SerialNumber: "", BDF: "", DeviceClass: "NetworkController"},
			{Slot: "C2", SerialNumber: "", BDF: "", DeviceClass: "NetworkController"},
		},
	}

	devices := BuildHardwareDevices(hw)
	// 1 board + (SER-1 dedup -> 1) + (BDF 03 dedup -> 1) + (C1,C2 keep both) = 5
	if len(devices) != 5 {
		t.Fatalf("expected 5 devices after dedupe, got %d", len(devices))
	}

	bySlot := map[string]bool{}
	for _, d := range devices {
		bySlot[d.Slot] = true
	}
	if !bySlot["A1"] && !bySlot["A2"] {
		t.Fatalf("expected one serial-deduped A* device")
	}
	if bySlot["B1"] && bySlot["B2"] {
		t.Fatalf("expected B1/B2 to dedupe by bdf")
	}
	if !bySlot["C1"] || !bySlot["C2"] {
		t.Fatalf("expected C1 and C2 to remain without serial/bdf")
	}
}

func TestBuildHardwareDevices_SkipsEmptyMemorySlots(t *testing.T) {
	hw := &models.HardwareConfig{
		Memory: []models.MemoryDIMM{
			{Slot: "A1", Present: true, SizeMB: 32768, SerialNumber: "DIMM-1"},
			{Slot: "A2", Present: false, SizeMB: 0, SerialNumber: "DIMM-2"},
		},
	}

	devices := BuildHardwareDevices(hw)
	memoryCount := 0
	for _, d := range devices {
		if d.Kind == models.DeviceKindMemory {
			memoryCount++
			if d.Slot == "A2" {
				t.Fatalf("empty memory slot should not be included")
			}
		}
	}
	if memoryCount != 1 {
		t.Fatalf("expected 1 installed memory record, got %d", memoryCount)
	}
}

func TestBuildHardwareDevices_MemorySameSerialDifferentSlots_NotDeduped(t *testing.T) {
	hw := &models.HardwareConfig{
		Memory: []models.MemoryDIMM{
			{Slot: "Node0_Dimm1", Location: "Node0_Bank0", Present: true, SizeMB: 16384, SerialNumber: "238F7649", PartNumber: "M393B2G70BH0-"},
			{Slot: "Node0_Dimm3", Location: "Node0_Bank0", Present: true, SizeMB: 16384, SerialNumber: "238F7649", PartNumber: "M393B2G70BH0-"},
		},
	}

	devices := BuildHardwareDevices(hw)
	memorySlots := make(map[string]bool)
	for _, d := range devices {
		if d.Kind != models.DeviceKindMemory {
			continue
		}
		memorySlots[d.Slot] = true
	}

	if len(memorySlots) != 2 {
		t.Fatalf("expected 2 memory devices, got %d", len(memorySlots))
	}
	if !memorySlots["Node0_Dimm1"] || !memorySlots["Node0_Dimm3"] {
		t.Fatalf("expected both Node0_Dimm1 and Node0_Dimm3 to remain")
	}
}

func TestBuildHardwareDevices_ZeroSizeMemoryWithInventoryIsIncluded(t *testing.T) {
	hw := &models.HardwareConfig{
		Memory: []models.MemoryDIMM{
			{
				Slot:         "PROC 1 DIMM 3",
				Location:     "PROC 1 DIMM 3",
				Present:      true,
				SizeMB:       0,
				Manufacturer: "Hynix",
				SerialNumber: "2B5F92C6",
				PartNumber:   "HMCG88AEBRA115N",
				Status:       "ok",
			},
		},
	}

	devices := BuildHardwareDevices(hw)
	memoryCount := 0
	for _, d := range devices {
		if d.Kind != models.DeviceKindMemory {
			continue
		}
		memoryCount++
		if d.Slot != "PROC 1 DIMM 3" || d.PartNumber != "HMCG88AEBRA115N" || d.SerialNumber != "2B5F92C6" {
			t.Fatalf("unexpected memory device: %+v", d)
		}
	}
	if memoryCount != 1 {
		t.Fatalf("expected 1 installed zero-size memory record, got %d", memoryCount)
	}
}

func TestBuildHardwareDevices_NetworkAdapterPreservesPCIeMetadata(t *testing.T) {
	hw := &models.HardwareConfig{
		NetworkAdapters: []models.NetworkAdapter{
			{
				Slot:         "1",
				Location:     "OCP",
				Present:      true,
				BDF:          "0000:27:00.0",
				Model:        "ConnectX-6 Lx",
				VendorID:     0x15b3,
				DeviceID:     0x101f,
				SerialNumber: "NIC-001",
				Firmware:     "26.39.2048",
				MACAddresses: []string{"44:1A:4C:16:E8:03", "44:1A:4C:16:E8:04"},
				LinkWidth:    16,
				LinkSpeed:    "32 GT/s",
				NUMANode:     1,
				Status:       "ok",
			},
		},
	}

	devices := BuildHardwareDevices(hw)
	for _, d := range devices {
		if d.Kind != models.DeviceKindNetwork {
			continue
		}
		if d.BDF != "0000:27:00.0" || d.LinkWidth != 16 || d.LinkSpeed != "32 GT/s" || d.NUMANode != 1 {
			t.Fatalf("expected network PCIe metadata to be preserved, got %+v", d)
		}
		return
	}
	t.Fatal("expected network device in canonical inventory")
}

func TestBuildSpecification_ZeroSizeMemoryWithInventoryIsShown(t *testing.T) {
	hw := &models.HardwareConfig{
		Memory: []models.MemoryDIMM{
			{
				Slot:         "PROC 1 DIMM 3",
				Present:      true,
				SizeMB:       0,
				Manufacturer: "Hynix",
				PartNumber:   "HMCG88AEBRA115N",
				SerialNumber: "2B5F92C6",
				Status:       "ok",
			},
		},
	}

	spec := buildSpecification(hw)
	for _, line := range spec {
		if line.Category == "Память" && line.Name == "Hynix HMCG88AEBRA115N (size unknown)" && line.Quantity == 1 {
			return
		}
	}

	t.Fatalf("expected memory spec line for zero-size identified DIMM, got %+v", spec)
}

func TestBuildHardwareDevices_DuplicateSerials_AreAnnotated(t *testing.T) {
	hw := &models.HardwareConfig{
		Memory: []models.MemoryDIMM{
			{Slot: "A1", Location: "BANK0", Present: true, SizeMB: 16384, SerialNumber: "SN-1"},
			{Slot: "A2", Location: "BANK1", Present: true, SizeMB: 16384, SerialNumber: "SN-1"},
		},
	}

	devices := BuildHardwareDevices(hw)
	var serials []string
	for _, d := range devices {
		if d.Kind == models.DeviceKindMemory {
			serials = append(serials, d.SerialNumber)
		}
	}
	if len(serials) != 2 {
		t.Fatalf("expected 2 memory devices, got %d", len(serials))
	}
	if serials[0] != "SN-1 (DUP#1)" || serials[1] != "SN-1 (DUP#2)" {
		t.Fatalf("unexpected annotated serials: %+v", serials)
	}
}

func TestBuildHardwareDevices_DedupCrossKindByBDF(t *testing.T) {
	hw := &models.HardwareConfig{
		PCIeDevices: []models.PCIeDevice{
			{
				Slot:         "SL0CP0_001",
				BDF:          "02:00.0",
				DeviceClass:  "DisplayController",
				VendorID:     0x1a03,
				DeviceID:     0x2000,
				PartNumber:   "ASPEED Graphics Family",
				Manufacturer: "ASPEED Technology, Inc.",
			},
		},
		GPUs: []models.GPU{
			{
				Slot:         "SL0CP0_001",
				BDF:          "02:00.0",
				Model:        "ASPEED Graphics Family",
				Manufacturer: "ASPEED Technology, Inc.",
				VendorID:     0x1a03,
				DeviceID:     0x2000,
			},
		},
	}

	devices := BuildHardwareDevices(hw)
	count := 0
	for _, d := range devices {
		if d.BDF == "02:00.0" {
			count++
		}
	}
	if count != 1 {
		t.Fatalf("expected 1 canonical device for bdf 02:00.0, got %d", count)
	}
}

func TestBuildHardwareDevices_SkipsFirmwareOnlyNumericSlots(t *testing.T) {
	hw := &models.HardwareConfig{
		PCIeDevices: []models.PCIeDevice{
			{Slot: "0", DeviceClass: "Unknown", Manufacturer: "-", PartNumber: "-", Description: "-"},
			{Slot: "1", DeviceClass: "Other", Manufacturer: "unknown", PartNumber: "N/A", Description: "NULL"},
		},
	}

	devices := BuildHardwareDevices(hw)
	for _, d := range devices {
		if d.Kind == models.DeviceKindPCIe && (d.Slot == "0" || d.Slot == "1") {
			t.Fatalf("firmware-only numeric-slot pcie record must be filtered, got slot %q", d.Slot)
		}
	}
}

func TestBuildHardwareDevices_NetworkDevicesUseUnifiedControllerClass(t *testing.T) {
	hw := &models.HardwareConfig{
		NetworkAdapters: []models.NetworkAdapter{
			{
				Slot:    "NIC1",
				Model:   "Ethernet Adapter",
				Vendor:  "Intel",
				Present: true,
			},
		},
	}

	devices := BuildHardwareDevices(hw)
	for _, d := range devices {
		if d.Kind != models.DeviceKindNetwork {
			continue
		}
		if d.DeviceClass != "NetworkController" {
			t.Fatalf("expected unified network controller class, got %+v", d)
		}
		return
	}
	t.Fatalf("expected one canonical network device")
}

func TestHandleGetConfig_ReturnsCanonicalHardware(t *testing.T) {
	srv := &Server{}
	srv.SetResult(&models.AnalysisResult{
		Hardware: &models.HardwareConfig{
			BoardInfo: models.BoardInfo{ProductName: "X", SerialNumber: "SN-1"},
			CPUs:      []models.CPU{{Socket: 0, Model: "CPU"}},
		},
	})

	req := httptest.NewRequest(http.MethodGet, "/api/config", nil)
	w := httptest.NewRecorder()
	srv.handleGetConfig(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}

	var payload map[string]any
	if err := json.NewDecoder(w.Body).Decode(&payload); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	hardware, ok := payload["hardware"].(map[string]any)
	if !ok {
		t.Fatalf("expected hardware object")
	}
	if _, ok := hardware["devices"]; !ok {
		t.Fatalf("expected hardware.devices in config response")
	}
	if _, ok := hardware["cpus"]; ok {
		t.Fatalf("did not expect legacy hardware.cpus in config response")
	}
}
