package main

import "testing"

// ---- JSON path (smartmontools >= 7.0): ATA SSD with a reallocated sector ----

const ataJSON = `{
  "model_name": "Micron_1300_MTFDDAK512TDL",
  "serial_number": "21512A3B4C5D",
  "firmware_version": "M5MU000",
  "user_capacity": {"bytes": 512110190592},
  "rotation_rate": 0,
  "smart_status": {"passed": true},
  "power_on_time": {"hours": 26280},
  "power_cycle_count": 42,
  "temperature": {"current": 31},
  "ata_smart_attributes": {"table": [
    {"id": 5,  "name": "Reallocated_Sector_Ct", "value": 100, "worst": 100, "thresh": 10, "when_failed": "", "raw": {"value": 8, "string": "8"}},
    {"id": 9,  "name": "Power_On_Hours",         "value": 95,  "worst": 95,  "thresh": 0,  "when_failed": "", "raw": {"value": 26280, "string": "26280"}},
    {"id": 199,"name": "UDMA_CRC_Error_Count",   "value": 100, "worst": 100, "thresh": 0,  "when_failed": "", "raw": {"value": 3, "string": "3"}},
    {"id": 202,"name": "Percent_Lifetime_Remain","value": 88,  "worst": 88,  "thresh": 1,  "when_failed": "", "raw": {"value": 12, "string": "12"}}
  ]}
}`

func TestParseSmartJSON_ATA(t *testing.T) {
	d := &Drive{}
	parseSmartJSON(loadJSON(ataJSON), d)

	if d.Model != "Micron_1300_MTFDDAK512TDL" {
		t.Errorf("model = %q", d.Model)
	}
	if d.Rotation != "SSD" {
		t.Errorf("rotation = %q, want SSD", d.Rotation)
	}
	if iv(d.Reallocated) != 8 {
		t.Errorf("reallocated = %v, want 8", d.Reallocated)
	}
	if iv(d.UdmaCrc) != 3 {
		t.Errorf("udma_crc = %v, want 3", d.UdmaCrc)
	}
	if iv(d.PowerOnHours) != 26280 {
		t.Errorf("poh = %v, want 26280", d.PowerOnHours)
	}
	if d.WearSrc != "Intel/ID202" || iv(d.WearPctConsumed) != 12 {
		t.Errorf("wear src=%q consumed=%v, want Intel/ID202 / 12", d.WearSrc, d.WearPctConsumed)
	}
	if d.SmartHealth != "PASSED" {
		t.Errorf("health = %q", d.SmartHealth)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	// reallocated 8 -> min(40+40,100)=80; udma 3 -> 9; total 89 -> REPLACE_SOON.
	if d.Recommendation != "REPLACE_SOON" {
		t.Errorf("rec = %q (score %d), want REPLACE_SOON", d.Recommendation, d.RiskScore)
	}
	if iv(d.DefectTotal) != 8 {
		t.Errorf("defect_total = %v, want 8", d.DefectTotal)
	}
}

// ---- Text path (CentOS 6/7, smartmontools 5.x/6.x: no JSON) ----

const ataText = `smartctl 6.5 2016-05-07 r4318 [x86_64-linux] (local build)

=== START OF INFORMATION SECTION ===
Device Model:     INTEL SSDSC2BB480G6
Serial Number:    BTWA12345678480BGN
Firmware Version: G2010140
User Capacity:    480,103,981,056 bytes [480 GB]
Rotation Rate:    Solid State Device

=== START OF READ SMART DATA SECTION ===
SMART overall-health self-assessment test result: PASSED

ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
  5 Reallocated_Sector_Ct   0x0032   100   100   000    Old_age   Always       -       0
  9 Power_On_Hours          0x0032   100   100   000    Old_age   Always       -       51000
197 Current_Pending_Sector  0x0012   100   100   000    Old_age   Always       -       5
233 Media_Wearout_Indicator 0x0032   072   072   000    Old_age   Always       -       0
`

func TestParseSmartText_ATA(t *testing.T) {
	d := &Drive{}
	parseSmartText(ataText, d)

	if d.Model != "INTEL SSDSC2BB480G6" {
		t.Errorf("model = %q", d.Model)
	}
	if d.SmartHealth != "PASSED" {
		t.Errorf("health = %q", d.SmartHealth)
	}
	if iv(d.Pending) != 5 {
		t.Errorf("pending = %v, want 5", d.Pending)
	}
	if iv(d.PowerOnHours) != 51000 {
		t.Errorf("poh = %v, want 51000", d.PowerOnHours)
	}
	if d.WearSrc != "Generic/ID233" || iv(d.WearPctConsumed) != 28 {
		t.Errorf("wear src=%q consumed=%v, want Generic/ID233 / 28", d.WearSrc, d.WearPctConsumed)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	// pending 5 -> min(50+25,100)=75 -> REPLACE_SOON.
	if d.Recommendation != "REPLACE_SOON" {
		t.Errorf("rec = %q (score %d), want REPLACE_SOON", d.Recommendation, d.RiskScore)
	}
}

// ---- NVMe text path ----

const nvmeText = `smartctl 7.2 2020-12-30 r5155 [x86_64-linux]

=== START OF INFORMATION SECTION ===
Model Number:                       Samsung SSD 980 PRO 1TB
Serial Number:                      S5GXNX0R123456
Firmware Version:                   5B2QGXA7
=== START OF SMART DATA SECTION ===
SMART overall-health self-assessment test result: PASSED

SMART/Health Information (NVMe Log 0x02)
Critical Warning:                   0x04
Temperature:                        40 Celsius
Available Spare:                    8%
Available Spare Threshold:          10%
Percentage Used:                    96%
Power On Hours:                     30,123
Power Cycles:                       210
Media and Data Integrity Errors:    0
`

func TestParseSmartText_NVMe(t *testing.T) {
	d := &Drive{}
	parseSmartText(nvmeText, d)

	if d.Rotation != "NVMe" {
		t.Errorf("rotation = %q, want NVMe", d.Rotation)
	}
	if d.NvmeCriticalWarning == nil || *d.NvmeCriticalWarning != 4 {
		t.Errorf("critical_warning = %v, want 4", d.NvmeCriticalWarning)
	}
	if iv(d.NvmeAvailSpare) != 8 || iv(d.NvmeAvailSpareThresh) != 10 {
		t.Errorf("spare=%v thresh=%v, want 8/10", d.NvmeAvailSpare, d.NvmeAvailSpareThresh)
	}
	if iv(d.WearPctConsumed) != 96 {
		t.Errorf("wear consumed = %v, want 96", d.WearPctConsumed)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	// crit warning +60, spare<=thresh +40, wear96 +80 -> >=100 REPLACE_NOW.
	if d.Recommendation != "REPLACE_NOW" {
		t.Errorf("rec = %q (score %d), want REPLACE_NOW", d.Recommendation, d.RiskScore)
	}
}

// ---- NO_DATA: nothing readable, no controller flags ----

func TestNoData(t *testing.T) {
	d := &Drive{HaveSmart: false}
	finalizeDerived(d)
	if d.Recommendation != "NO_DATA" {
		t.Errorf("rec = %q, want NO_DATA", d.Recommendation)
	}
	if d.DefectTotal != nil {
		t.Errorf("defect_total = %v, want nil (blank)", d.DefectTotal)
	}
}

// ---- MegaCLI PDList parsing + controller-driven scoring ----

const megacliText = `
Enclosure Device ID: 64
Slot Number: 3
Device Id: 11
WWN: 5000C500A1B2C3D4
Media Error Count: 369
Other Error Count: 2
Predictive Failure Count: 1
Drive has flagged a S.M.A.R.T alert : Yes
Firmware state: Online, Spun Up
Inquiry Data: BTWA12345678 INTELSSDSC2BB480G6 G2010140
Drive Temperature: 35C (95.00 F)

Enclosure Device ID: 64
Slot Number: 0
Device Id: 8
Firmware state: Online, Spun Up
Media Error Count: 0
`

func TestMegacliAndScore(t *testing.T) {
	drives := parseMegacliPDList(megacliText)
	if len(drives) != 2 {
		t.Fatalf("parsed %d drives, want 2", len(drives))
	}
	idx := map[string]ctrlDrive{}
	for _, cd := range drives {
		idx[cd.DeviceID] = cd
	}
	cd, ok := idx["11"]
	if !ok {
		t.Fatal("device 11 not found")
	}
	if cd.MediaErr != 369 || cd.Predictive != 1 || !cd.SmartAlert {
		t.Errorf("dev11 media=%d pred=%d alert=%v", cd.MediaErr, cd.Predictive, cd.SmartAlert)
	}
	if cd.Enclosure != "64" || cd.Slot != "3" {
		t.Errorf("dev11 location %s:%s, want 64:3", cd.Enclosure, cd.Slot)
	}

	// Controller-only drive (no smartctl): predictive + alert -> not NO_DATA.
	d := &Drive{HaveSmart: false}
	applyController(d, cd)
	finalizeDerived(d)
	// predictive +70, alert +50, media 369 +30 = 150 -> REPLACE_NOW.
	if d.Recommendation != "REPLACE_NOW" {
		t.Errorf("rec = %q (score %d), want REPLACE_NOW", d.Recommendation, d.RiskScore)
	}
	if d.enclosureSlot() != "64:3" {
		t.Errorf("enclosure_slot = %q", d.enclosureSlot())
	}
}

// ---- MegaCLI Inquiry Data identity fallback (real-world layouts) ----

// applyController's inquiry fallback must anchor serial=first / firmware=last and
// treat the (space-containing) remainder as the model. These samples are the real
// MegaCLI "Inquiry Data" forms documented alongside the parser; the naive
// serial/model/firmware positional split mis-handled them.
func TestApplyControllerInquiry(t *testing.T) {
	cases := []struct {
		name                          string
		inquiry                       string
		wantSerial, wantModel, wantFw string
	}{
		{
			"model with internal space",
			"50026B727A005DED    KINGSTON SEDC400S37480G                 SAFM02.H",
			"50026B727A005DED", "KINGSTON SEDC400S37480G", "SAFM02.H",
		},
		{
			"two tokens: serial + firmware only",
			"ZRT0CQ55ST12000NM000J-2TY103                    SN02",
			"ZRT0CQ55ST12000NM000J-2TY103", "", "SN02",
		},
		{
			"clean three tokens",
			"BTWA12345678 INTELSSDSC2BB480G6 G2010140",
			"BTWA12345678", "INTELSSDSC2BB480G6", "G2010140",
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			d := &Drive{}
			applyController(d, ctrlDrive{Inquiry: c.inquiry})
			if d.Serial != c.wantSerial || d.Model != c.wantModel || d.Firmware != c.wantFw {
				t.Errorf("got serial=%q model=%q fw=%q; want serial=%q model=%q fw=%q",
					d.Serial, d.Model, d.Firmware, c.wantSerial, c.wantModel, c.wantFw)
			}
		})
	}
}

// ---- Output smoke: CSV header + influx line shape ----

func TestOutputShapes(t *testing.T) {
	d := &Drive{Hostname: "kvm60", Model: "X", Serial: "S1", SmartHealth: "PASSED"}
	finalizeDerived(d)
	csv := recordsToCSV([]*Drive{d})
	if len(csv) == 0 || csv[:len("collected_at")] != "collected_at" {
		t.Errorf("csv header malformed: %.40q", csv)
	}
	inf := recordsToInflux([]*Drive{d}, 1700000000000000000)
	if len(inf) < len(influxMeasurement) || inf[:len(influxMeasurement)] != influxMeasurement {
		t.Errorf("influx line malformed: %.60q", inf)
	}
}
