package lenovo_xcc

import (
	"testing"
	"time"

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

const exampleArchive = "/Users/mchusavitin/Documents/git/logpile/example/7D76CTO1WW_JF0002KT_xcc_mini-log_20260413-122150.zip"

func TestDetect_LenovoXCCMiniLog(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	score := p.Detect(files)
	if score < 80 {
		t.Errorf("expected Detect score >= 80 for XCC mini-log archive, got %d", score)
	}
}

func TestParse_LenovoXCCMiniLog_BasicSysInfo(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, err := p.Parse(files)
	if err != nil {
		t.Fatalf("Parse returned error: %v", err)
	}
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil result or hardware")
	}

	hw := result.Hardware
	if hw.BoardInfo.SerialNumber == "" {
		t.Error("BoardInfo.SerialNumber is empty")
	}
	if hw.BoardInfo.ProductName == "" {
		t.Error("BoardInfo.ProductName is empty")
	}
	t.Logf("BoardInfo: serial=%s model=%s uuid=%s", hw.BoardInfo.SerialNumber, hw.BoardInfo.ProductName, hw.BoardInfo.UUID)
}

func TestParse_LenovoXCCMiniLog_CPUs(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Hardware.CPUs) == 0 {
		t.Error("expected at least one CPU, got none")
	}
	for i, cpu := range result.Hardware.CPUs {
		t.Logf("CPU[%d]: socket=%d model=%q cores=%d threads=%d freq=%dMHz", i, cpu.Socket, cpu.Model, cpu.Cores, cpu.Threads, cpu.FrequencyMHz)
	}
}

func TestParse_LenovoXCCMiniLog_Memory(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Hardware.Memory) == 0 {
		t.Error("expected memory DIMMs, got none")
	}
	t.Logf("Memory: %d DIMMs", len(result.Hardware.Memory))
	for i, m := range result.Hardware.Memory {
		t.Logf("DIMM[%d]: slot=%s present=%v size=%dMB sn=%s", i, m.Slot, m.Present, m.SizeMB, m.SerialNumber)
	}
}

func TestParse_LenovoXCCMiniLog_Storage(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	t.Logf("Storage: %d disks", len(result.Hardware.Storage))
	for i, s := range result.Hardware.Storage {
		t.Logf("Disk[%d]: slot=%s model=%q size=%dGB sn=%s", i, s.Slot, s.Model, s.SizeGB, s.SerialNumber)
	}
}

func TestParse_LenovoXCCMiniLog_PCIeCards(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	t.Logf("PCIe cards: %d", len(result.Hardware.PCIeDevices))
	for i, c := range result.Hardware.PCIeDevices {
		t.Logf("Card[%d]: slot=%s desc=%q bdf=%s", i, c.Slot, c.Description, c.BDF)
	}
}

func TestParse_LenovoXCCMiniLog_PSUs(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Hardware.PowerSupply) == 0 {
		t.Error("expected PSUs, got none")
	}
	for i, p := range result.Hardware.PowerSupply {
		t.Logf("PSU[%d]: slot=%s wattage=%dW status=%s sn=%s", i, p.Slot, p.WattageW, p.Status, p.SerialNumber)
	}
}

func TestParse_LenovoXCCMiniLog_Sensors(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Sensors) == 0 {
		t.Error("expected sensors, got none")
	}
	t.Logf("Sensors: %d", len(result.Sensors))
}

func TestParse_LenovoXCCMiniLog_Events(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Events) == 0 {
		t.Error("expected events, got none")
	}
	t.Logf("Events: %d", len(result.Events))
	for i, e := range result.Events {
		if i >= 5 {
			break
		}
		t.Logf("Event[%d]: severity=%s ts=%s desc=%q", i, e.Severity, e.Timestamp.Format("2006-01-02T15:04:05"), e.Description)
	}
}

func TestParse_LenovoXCCMiniLog_FRU(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil {
		t.Fatal("Parse returned nil")
	}

	t.Logf("FRU: %d entries", len(result.FRU))
	for i, f := range result.FRU {
		t.Logf("FRU[%d]: desc=%q product=%q serial=%q", i, f.Description, f.ProductName, f.SerialNumber)
	}
}

func TestParse_LenovoXCCMiniLog_Firmware(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Hardware.Firmware) == 0 {
		t.Error("expected firmware entries, got none")
	}
	for i, f := range result.Hardware.Firmware {
		t.Logf("FW[%d]: name=%q version=%q buildtime=%q", i, f.DeviceName, f.Version, f.BuildTime)
	}
}

func TestParse_LenovoXCCMiniLog_VROCVolumes(t *testing.T) {
	files, err := parser.ExtractArchive(exampleArchive)
	if err != nil {
		t.Skipf("example archive not available: %v", err)
	}

	p := &Parser{}
	result, _ := p.Parse(files)
	if result == nil || result.Hardware == nil {
		t.Fatal("Parse returned nil")
	}

	if len(result.Hardware.Volumes) == 0 {
		t.Error("expected at least one VROC volume, got none")
	}
	for i, v := range result.Hardware.Volumes {
		t.Logf("Volume[%d]: id=%s controller=%q raid=%s size=%dGB status=%s drives=%v",
			i, v.ID, v.Controller, v.RAIDLevel, v.SizeGB, v.Status, v.Drives)
		if v.RAIDLevel == "" {
			t.Errorf("Volume[%d]: RAIDLevel is empty", i)
		}
		if v.Status == "" {
			t.Errorf("Volume[%d]: Status is empty", i)
		}
	}
}

func TestParseVolumes_IntelVROC(t *testing.T) {
	content := []byte(`{
		"identifier": "storage.id",
		"items": [{
			"volumes": [{
				"id": 1,
				"name": "",
				"drives": "M.2 Drive 0, M.2 Drive 1",
				"rdlvlstr": "RAID 1",
				"capacityStr": "893.750 GiB",
				"status": 3,
				"statusStr": "Optimal"
			}],
			"totalCapacityStr": "893.750 GiB"
		}]
	}`)

	vols := parseVolumes(content)
	if len(vols) != 1 {
		t.Fatalf("expected 1 volume, got %d", len(vols))
	}
	v := vols[0]
	if v.ID != "1" {
		t.Errorf("expected ID=1, got %q", v.ID)
	}
	if v.RAIDLevel != "RAID 1" {
		t.Errorf("expected RAIDLevel=RAID 1, got %q", v.RAIDLevel)
	}
	if v.Status != "Optimal" {
		t.Errorf("expected Status=Optimal, got %q", v.Status)
	}
	if v.Controller != "Intel VROC" {
		t.Errorf("expected Controller=Intel VROC, got %q", v.Controller)
	}
	if len(v.Drives) != 2 {
		t.Errorf("expected 2 drives, got %d: %v", len(v.Drives), v.Drives)
	}
	if v.SizeGB < 900 || v.SizeGB > 1000 {
		t.Errorf("expected SizeGB ~960, got %d", v.SizeGB)
	}
}

func TestParseDIMMs_UnqualifiedDIMMAddsWarningEvent(t *testing.T) {
	content := []byte(`{
		"items": [{
			"memory": [{
				"memory_name": "DIMM A1",
				"memory_status": "Unqualified DIMM",
				"memory_type": "DDR5",
				"memory_capacity": 32
			}]
		}]
	}`)

	memory, events := parseDIMMs(content)
	if len(memory) != 1 {
		t.Fatalf("expected 1 DIMM, got %d", len(memory))
	}
	if len(events) != 1 {
		t.Fatalf("expected 1 warning event, got %d", len(events))
	}
	if events[0].Severity != models.SeverityWarning {
		t.Fatalf("expected warning severity, got %q", events[0].Severity)
	}
	if events[0].SensorName != "DIMM A1" {
		t.Fatalf("unexpected sensor name: %q", events[0].SensorName)
	}
}

func TestSeverity_UnqualifiedDIMMMessageBecomesWarning(t *testing.T) {
	if got := xccSeverity("I", "System found Unqualified DIMM in slot DIMM A1"); got != models.SeverityWarning {
		t.Fatalf("expected warning severity, got %q", got)
	}
}

func TestApplyDIMMWarningsFromEvents_UpdatesDIMMStatusForExport(t *testing.T) {
	result := &models.AnalysisResult{
		Events: []models.Event{
			{
				Timestamp:   time.Date(2026, 4, 13, 11, 37, 38, 0, time.UTC),
				Severity:    models.SeverityWarning,
				Description: "Unqualified DIMM 3 has been detected, the DIMM serial number is 80CE042328460C5D88-V20.",
			},
		},
		Hardware: &models.HardwareConfig{
			Memory: []models.MemoryDIMM{
				{
					Slot:         "DIMM 3",
					Present:      true,
					SerialNumber: "80CE042328460C5D88",
					Status:       "Normal",
				},
			},
		},
	}

	applyDIMMWarningsFromEvents(result)

	dimm := result.Hardware.Memory[0]
	if dimm.Status != "Warning" {
		t.Fatalf("expected DIMM status Warning, got %q", dimm.Status)
	}
	if dimm.ErrorDescription == "" || dimm.ErrorDescription != result.Events[0].Description {
		t.Fatalf("expected DIMM error description to be populated, got %q", dimm.ErrorDescription)
	}
	if dimm.StatusChangedAt == nil || !dimm.StatusChangedAt.Equal(result.Events[0].Timestamp) {
		t.Fatalf("expected status_changed_at from event timestamp, got %#v", dimm.StatusChangedAt)
	}
	if len(dimm.StatusHistory) != 1 || dimm.StatusHistory[0].Status != "Warning" {
		t.Fatalf("expected warning status history entry, got %#v", dimm.StatusHistory)
	}
}

func TestParseBasicSysInfo_CleansPlaceholderValuesAndSetsTargetHost(t *testing.T) {
	result := &models.AnalysisResult{Hardware: &models.HardwareConfig{}}
	content := []byte(`{
		"items": [{
			"machine_name": " sr650v3-node01 ",
			"machine_typemodel": " 7D76CTO1WW ",
			"serial_number": " Not Specified ",
			"uuid": "N/A"
		}]
	}`)

	parseBasicSysInfo(content, result)

	if result.TargetHost != "sr650v3-node01" {
		t.Fatalf("unexpected target host: %q", result.TargetHost)
	}
	if result.Hardware.BoardInfo.ProductName != "7D76CTO1WW" {
		t.Fatalf("unexpected product name: %q", result.Hardware.BoardInfo.ProductName)
	}
	if result.Hardware.BoardInfo.SerialNumber != "" {
		t.Fatalf("expected serial number to be cleaned, got %q", result.Hardware.BoardInfo.SerialNumber)
	}
	if result.Hardware.BoardInfo.UUID != "" {
		t.Fatalf("expected UUID to be cleaned, got %q", result.Hardware.BoardInfo.UUID)
	}
}

func TestEnrichBoardFromFRU_SystemBoardManufacturerOnly(t *testing.T) {
	result := &models.AnalysisResult{
		Hardware: &models.HardwareConfig{},
		FRU: []models.FRUInfo{
			{Description: "Power Supply 1", Manufacturer: "Ignore Me"},
			{Description: "System Board", Manufacturer: " Lenovo "},
		},
	}

	enrichBoardFromFRU(result)

	if result.Hardware.BoardInfo.Manufacturer != "Lenovo" {
		t.Fatalf("unexpected manufacturer: %q", result.Hardware.BoardInfo.Manufacturer)
	}
}

func TestEnrichPSUsFromSensors_AssignsTelemetryBySlot(t *testing.T) {
	psus := []models.PSU{
		{Slot: "1"},
		{Slot: "2"},
	}
	sensors := []models.SensorReading{
		{Name: "PSU1 Input Power", Value: 430},
		{Name: "Power Supply 1 Output Power", Value: 390},
		{Name: "PWS1 AC Voltage", Value: 230.5},
		{Name: "PSU2 Input Power", Value: 0},
		{Name: "PSU3 Input Power", Value: 999},
		{Name: "Fan 1", Value: 12000},
	}

	got := enrichPSUsFromSensors(psus, sensors)

	if got[0].InputPowerW != 430 {
		t.Fatalf("unexpected PSU1 input power: %d", got[0].InputPowerW)
	}
	if got[0].OutputPowerW != 390 {
		t.Fatalf("unexpected PSU1 output power: %d", got[0].OutputPowerW)
	}
	if got[0].InputVoltage != 230.5 {
		t.Fatalf("unexpected PSU1 input voltage: %v", got[0].InputVoltage)
	}
	if got[1].InputPowerW != 0 || got[1].OutputPowerW != 0 || got[1].InputVoltage != 0 {
		t.Fatalf("unexpected telemetry assigned to PSU2: %+v", got[1])
	}
}

func TestMapDiskHealthStatus(t *testing.T) {
	tests := []struct {
		name     string
		code     int
		stateStr string
		want     string
	}{
		{name: "normal", code: 2, stateStr: "Online", want: "OK"},
		{name: "warning", code: 1, stateStr: "Online", want: "Warning"},
		{name: "predictive failure", code: 4, stateStr: "Online", want: "Warning"},
		{name: "critical", code: 3, stateStr: "Failed", want: "Critical"},
		{name: "fallback state", code: 0, stateStr: "Rebuilding", want: "Rebuilding"},
		{name: "unknown", code: 0, stateStr: "", want: "Unknown"},
	}

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

func TestClassifySensorType(t *testing.T) {
	tests := []struct {
		name string
		in   string
		unit string
		want string
	}{
		{name: "unit rpm", in: "Fan 1", unit: "RPM", want: "fan"},
		{name: "unit celsius", in: "CPU Temp", unit: "C", want: "temperature"},
		{name: "unit watts", in: "PSU1 Input Power", unit: "W", want: "power"},
		{name: "unit volts", in: "PWS1 AC Voltage", unit: "V", want: "voltage"},
		{name: "unit amps", in: "PSU1 Current", unit: "A", want: "current"},
		{name: "name fallback", in: "GPU Temp", unit: "", want: "temperature"},
		{name: "other", in: "Presence", unit: "", want: "other"},
	}

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

func TestCleanXCCValue(t *testing.T) {
	tests := []struct {
		in   string
		want string
	}{
		{in: " Lenovo ", want: "Lenovo"},
		{in: "N/A", want: ""},
		{in: " not specified ", want: ""},
		{in: "-", want: ""},
	}

	for _, tt := range tests {
		if got := cleanXCCValue(tt.in); got != tt.want {
			t.Fatalf("cleanXCCValue(%q) = %q, want %q", tt.in, got, tt.want)
		}
	}
}
