package main

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// readFixture loads a captured real-world sample from testdata/.
func readFixture(t *testing.T, name string) string {
	t.Helper()
	b, err := os.ReadFile(filepath.Join("testdata", name))
	if err != nil {
		t.Fatalf("read fixture %s: %v", name, err)
	}
	return string(b)
}

// ---- Real smartctl JSON: Samsung 870 EVO via sat+megaraid (smartmontools 7.0) ----

func TestParseSmartJSON_RealMegaraidSSD(t *testing.T) {
	d := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_ata_ssd_megaraid.json")), d)

	if d.Model != "Samsung SSD 870 EVO 2TB" {
		t.Errorf("model = %q", d.Model)
	}
	if d.Serial != "S624NS0RC00003M" {
		t.Errorf("serial = %q", d.Serial)
	}
	if d.Firmware != "SVT02B6Q" {
		t.Errorf("firmware = %q", d.Firmware)
	}
	if d.Rotation != "SSD" {
		t.Errorf("rotation = %q, want SSD", d.Rotation)
	}
	if d.Capacity != "2.00 TB" {
		t.Errorf("capacity = %q, want 2.00 TB", d.Capacity)
	}
	if d.SmartHealth != "PASSED" {
		t.Errorf("health = %q", d.SmartHealth)
	}
	if iv(d.PowerOnHours) != 33518 {
		t.Errorf("poh = %v, want 33518", d.PowerOnHours)
	}
	if iv(d.PowerCycleCount) != 7 {
		t.Errorf("power_cycles = %v, want 7", d.PowerCycleCount)
	}
	if iv(d.TempC) != 31 {
		t.Errorf("temp = %v, want 31", d.TempC)
	}
	// Healthy drive: all defect counters present and zero.
	if iv(d.Reallocated) != 0 || iv(d.UdmaCrc) != 0 || iv(d.ReportedUncorrect) != 0 {
		t.Errorf("defects: realloc=%v crc=%v reported=%v, want 0/0/0",
			d.Reallocated, d.UdmaCrc, d.ReportedUncorrect)
	}
	// Samsung Wear_Leveling_Count (ID177) value 93 -> 7% consumed.
	if d.WearSrc != "Samsung/ID177" || iv(d.WearPctConsumed) != 7 {
		t.Errorf("wear src=%q consumed=%v, want Samsung/ID177 / 7", d.WearSrc, d.WearPctConsumed)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	if d.Recommendation != "OK" {
		t.Errorf("rec = %q (score %d), want OK", d.Recommendation, d.RiskScore)
	}
	if iv(d.DefectTotal) != 0 {
		t.Errorf("defect_total = %v, want 0", d.DefectTotal)
	}
}

// ---- Real smartctl JSON: Micron 5400 via sat+megaraid, no controller CLI ----
// util01 (Ubuntu 24, smartmontools 7.4) has megaraid drives but no storcli/
// MegaCLI, so no controller data merges. The drive exposes ID173/202/233 wear
// attributes at once, and its model is a bare part number (no vendor prefix).
func TestParseSmartJSON_RealMicronSSD(t *testing.T) {
	d := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_ata_ssd_micron.json")), d)

	if d.Model != "MTFDDAK960TGA-1BC1ZABDA" {
		t.Errorf("model = %q", d.Model)
	}
	if isPseudoDevice(d) {
		t.Errorf("bare part-number model wrongly flagged as pseudo device")
	}
	if d.Rotation != "SSD" {
		t.Errorf("rotation = %q, want SSD", d.Rotation)
	}
	if d.Capacity != "0.96 TB" {
		t.Errorf("capacity = %q, want 0.96 TB", d.Capacity)
	}
	if d.SmartHealth != "PASSED" {
		t.Errorf("health = %q", d.SmartHealth)
	}
	if iv(d.PowerOnHours) != 20238 || iv(d.PowerCycleCount) != 12 {
		t.Errorf("poh=%v cycles=%v, want 20238/12", d.PowerOnHours, d.PowerCycleCount)
	}
	// ID173 wins the wearAttr precedence; VALUE 100 -> 0% consumed (fresh).
	if d.WearSrc != "Micron/ID173" || iv(d.WearPctConsumed) != 0 {
		t.Errorf("wear src=%q consumed=%v, want Micron/ID173 / 0", d.WearSrc, d.WearPctConsumed)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	if d.Recommendation != "OK" {
		t.Errorf("rec = %q (score %d), want OK", d.Recommendation, d.RiskScore)
	}
}

// ---- Real NVMe JSON: health log drives wear/identity (direct-attached SSD) ----
func TestParseSmartJSON_RealNVMe(t *testing.T) {
	d := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_nvme.json")), d)

	if d.Model != "Force MP510" {
		t.Errorf("model = %q", d.Model)
	}
	if d.Rotation != "NVMe" {
		t.Errorf("rotation = %q, want NVMe", d.Rotation)
	}
	if iv(d.PowerOnHours) != 42811 {
		t.Errorf("poh = %v, want 42811", d.PowerOnHours)
	}
	// NVMe percentage_used 6 -> 6% consumed.
	if d.WearSrc != "NVMe/percentage_used" || iv(d.WearPctConsumed) != 6 {
		t.Errorf("wear src=%q consumed=%v, want NVMe/percentage_used / 6", d.WearSrc, d.WearPctConsumed)
	}
	if iv(d.NvmeCriticalWarning) != 0 || iv(d.NvmeAvailSpare) != 100 {
		t.Errorf("nvme crit=%v spare=%v, want 0/100", d.NvmeCriticalWarning, d.NvmeAvailSpare)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	// Healthy NVMe, 42811h (>4yr -> age +4), wear 6% -> OK.
	if d.Recommendation != "OK" {
		t.Errorf("rec = %q (score %d), want OK", d.Recommendation, d.RiskScore)
	}
}

// ---- Real SAS SSD JSON: endurance + grown-defect/error-counter health ----
// SAMSUNG ARFX0920S5xnNTRI behind a SAS HBA (tama, smartmontools 7.5). SAS
// drives have no ATA attribute table; hard-defect signals come from the SCSI
// logs (grown defect list, error counter log, pending defects).
func TestParseSmartJSON_RealSASSSD(t *testing.T) {
	d := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_sas_ssd.json")), d)

	if d.Model != "SAMSUNG ARFX0920S5xnNTRI" || d.Serial != "S43YNF0K000001" {
		t.Errorf("identity model=%q serial=%q", d.Model, d.Serial)
	}
	if !strings.HasPrefix(d.Transport, "SAS") {
		t.Errorf("transport = %q, want SAS*", d.Transport)
	}
	if d.Rotation != "SSD" {
		t.Errorf("rotation = %q, want SSD", d.Rotation)
	}
	if d.SmartHealth != "PASSED" {
		t.Errorf("health = %q", d.SmartHealth)
	}
	if iv(d.PowerOnHours) != 2487 || iv(d.TempC) != 56 {
		t.Errorf("poh=%v temp=%v, want 2487/56", d.PowerOnHours, d.TempC)
	}
	// SCSI endurance indicator 0% -> 0% consumed.
	if d.WearSrc != "SCSI/endurance" || iv(d.WearPctConsumed) != 0 {
		t.Errorf("wear src=%q consumed=%v, want SCSI/endurance / 0", d.WearSrc, d.WearPctConsumed)
	}
	// Healthy SAS drive: grown defects, uncorrected errors, pending all zero
	// (and non-nil, since the SCSI logs were present).
	if iv(d.Reallocated) != 0 || iv(d.Uncorrectable) != 0 || iv(d.Pending) != 0 {
		t.Errorf("defects: grown=%v uncorrected=%v pending=%v, want 0/0/0",
			d.Reallocated, d.Uncorrectable, d.Pending)
	}

	d.HaveSmart = true
	finalizeDerived(d)
	if d.Recommendation != "OK" {
		t.Errorf("rec = %q (score %d), want OK", d.Recommendation, d.RiskScore)
	}
}

// ---- SAS hard-defect signals drive the score (synthetic, both JSON + text) ----
// The captured SAS drives are healthy; verify the SCSI error counter log and
// pending-defect count actually feed the scorer when nonzero.
func TestSASUncorrectedErrorsScored(t *testing.T) {
	const j = `{
	  "model_name": "SEAGATE ST4000NM",
	  "device": {"type": "scsi", "protocol": "SCSI"},
	  "smart_status": {"passed": true},
	  "scsi_grown_defect_list": 3,
	  "scsi_error_counter_log": {
	    "read":   {"total_uncorrected_errors": 2},
	    "write":  {"total_uncorrected_errors": 0},
	    "verify": {"total_uncorrected_errors": 1}
	  },
	  "scsi_pending_defects": {"count": 4}
	}`
	d := &Drive{}
	parseSmartJSON(loadJSON(j), d)
	if iv(d.Uncorrectable) != 3 { // 2 read + 1 verify
		t.Errorf("uncorrectable = %v, want 3", d.Uncorrectable)
	}
	if iv(d.Pending) != 4 {
		t.Errorf("pending = %v, want 4", d.Pending)
	}
	if iv(d.Reallocated) != 3 { // grown defect list
		t.Errorf("reallocated(grown) = %v, want 3", d.Reallocated)
	}
	d.HaveSmart = true
	finalizeDerived(d)
	if d.Recommendation != "REPLACE_NOW" {
		t.Errorf("rec = %q (score %d), want REPLACE_NOW", d.Recommendation, d.RiskScore)
	}

	// Text path: same signals from `smartctl -a` SAS output.
	const text = `Vendor:               SEAGATE
Product:              ST4000NM
SMART Health Status: OK
Elements in grown defect list: 3
Error counter log:
read:          0        0         0         0          0       2620.555           2
write:         0        0         0         0          0       2091.250           0
verify:        0        0         0         0          0         46.845           1
  Pending defect count:4 Pending Defects
`
	dt := &Drive{}
	parseSmartText(text, dt)
	if iv(dt.Uncorrectable) != 3 || iv(dt.Pending) != 4 || iv(dt.Reallocated) != 3 {
		t.Errorf("text path: uncorrected=%v pending=%v grown=%v, want 3/4/3",
			dt.Uncorrectable, dt.Pending, dt.Reallocated)
	}
}

// ---- iSCSI LUNs (LIO/IET) are dropped via the SCSI transport, not by model ----
func TestISCSILunFilter(t *testing.T) {
	d := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_iscsi_lio.json")), d)
	if d.Transport != "iSCSI" {
		t.Fatalf("transport = %q, want iSCSI", d.Transport)
	}
	if !isPseudoDevice(d) {
		t.Errorf("LIO-ORG iSCSI LUN not flagged as pseudo device")
	}
	// Transport alone is authoritative even if the model looks like a real drive.
	if !isPseudoDevice(&Drive{Model: "Samsung SSD 860 EVO 1TB", Transport: "iSCSI"}) {
		t.Errorf("iSCSI transport not honored over a real-looking model")
	}
	// LIO model token still catches it on the text path (no transport field).
	if !isPseudoDevice(&Drive{Model: "LIO-ORG k8s1"}) {
		t.Errorf("LIO-ORG model token not matched")
	}
	// A real SAS/SATA drive (transport SAS) is not filtered.
	if isPseudoDevice(&Drive{Model: "HGST HUH721010ALE604", Transport: "SAS"}) {
		t.Errorf("real SAS drive wrongly flagged as pseudo device")
	}
}

// ---- Pseudo devices must be filtered: iSCSI LUN + RAID controller VD ----

func TestPseudoDeviceFilter(t *testing.T) {
	iscsi := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_iscsi_virtual_disk.json")), iscsi)
	if iscsi.Model != "IET VIRTUAL-DISK" {
		t.Fatalf("iscsi model = %q", iscsi.Model)
	}
	if !isPseudoDevice(iscsi) {
		t.Errorf("IET VIRTUAL-DISK not flagged as pseudo device")
	}

	vd := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_raid_vd.json")), vd)
	if vd.Model != "BROADCOM MR9560-16i" {
		t.Fatalf("raid vd model = %q", vd.Model)
	}
	if !isPseudoDevice(vd) {
		t.Errorf("BROADCOM MR9560-16i VD not flagged as pseudo device")
	}

	avago := &Drive{}
	parseSmartJSON(loadJSON(readFixture(t, "smart_raid_vd_avago.json")), avago)
	if avago.Model != "AVAGO MR9363-4i" {
		t.Fatalf("avago vd model = %q", avago.Model)
	}
	if !isPseudoDevice(avago) {
		t.Errorf("AVAGO MR9363-4i VD not flagged as pseudo device")
	}

	// SMART-less RAID virtual disk identified by model (DELL RAID / PERC VDs).
	for _, model := range []string{"DELL RAID", "DELL PERC H730"} {
		if !isPseudoDevice(&Drive{Model: model}) {
			t.Errorf("RAID VD %q not flagged as pseudo device", model)
		}
	}

	// Real drives behind these controllers must NOT be filtered.
	for _, model := range []string{"Samsung SSD 870 EVO 2TB", "MICRON_M510DC_MTFDDAK960MBP",
		"HFS960G3H2X069N", "Dell DC NVMe PM9A3 RI U.2 960GB"} {
		if isPseudoDevice(&Drive{Model: model}) {
			t.Errorf("real drive %q wrongly flagged as pseudo device", model)
		}
	}
}

// ---- Real storcli: DID comes from the table row; sub-headers don't fragment ----

func TestParseStorcli_Real(t *testing.T) {
	drives := parseStorcli(readFixture(t, "storcli_show_all.txt"))
	if len(drives) != 4 {
		t.Fatalf("parsed %d drives, want 4", len(drives))
	}

	byDID := map[string]ctrlDrive{}
	for _, cd := range drives {
		if cd.DeviceID == "" {
			t.Errorf("drive at %s:%s has empty DID", cd.Enclosure, cd.Slot)
		}
		byDID[cd.DeviceID] = cd
	}

	// Slot->DID->OtherErr mapping pulled straight from the capture.
	wantOther := map[string]struct {
		slot     string
		otherErr int
	}{
		"22": {"0", 7}, "23": {"1", 6}, "20": {"2", 21}, "21": {"3", 12},
	}
	for did, want := range wantOther {
		cd, ok := byDID[did]
		if !ok {
			t.Errorf("DID %s missing", did)
			continue
		}
		if cd.Enclosure != "64" || cd.Slot != want.slot {
			t.Errorf("DID %s location %s:%s, want 64:%s", did, cd.Enclosure, cd.Slot, want.slot)
		}
		if cd.OtherErr != want.otherErr {
			t.Errorf("DID %s other_err=%d, want %d", did, cd.OtherErr, want.otherErr)
		}
		if cd.MediaErr != 0 || cd.Predictive != 0 || cd.SmartAlert {
			t.Errorf("DID %s media=%d pred=%d alert=%v, want 0/0/false",
				did, cd.MediaErr, cd.Predictive, cd.SmartAlert)
		}
		if cd.FwState != "Onln" {
			t.Errorf("DID %s fw_state=%q, want Onln", did, cd.FwState)
		}
	}
}

// ---- Real storcli, second version/enclosure: parser generalizes ----
// storcli v007.1907, enclosure 252, Micron SSDs (jarvis01-kvm92). Confirms the
// summary-header + table-row DID logic is not specific to one storcli build.
func TestParseStorcli_RealV2(t *testing.T) {
	drives := parseStorcli(readFixture(t, "storcli_show_all_v2.txt"))
	if len(drives) != 4 {
		t.Fatalf("parsed %d drives, want 4", len(drives))
	}
	// DID N lives in slot (N-4), enclosure 252; all Online with zero errors.
	for _, cd := range drives {
		if cd.DeviceID == "" {
			t.Errorf("drive at %s:%s has empty DID", cd.Enclosure, cd.Slot)
		}
		if cd.Enclosure != "252" {
			t.Errorf("DID %s enclosure=%q, want 252", cd.DeviceID, cd.Enclosure)
		}
		if cd.FwState != "Onln" {
			t.Errorf("DID %s fw_state=%q, want Onln", cd.DeviceID, cd.FwState)
		}
		if cd.MediaErr != 0 || cd.OtherErr != 0 || cd.Predictive != 0 {
			t.Errorf("DID %s errors media=%d other=%d pred=%d, want 0/0/0",
				cd.DeviceID, cd.MediaErr, cd.OtherErr, cd.Predictive)
		}
	}
	byDID := map[string]ctrlDrive{}
	for _, cd := range drives {
		byDID[cd.DeviceID] = cd
	}
	if cd, ok := byDID["4"]; !ok || cd.Slot != "0" {
		t.Errorf("DID 4 -> %s:%s, want 252:0", cd.Enclosure, cd.Slot)
	}
	if cd, ok := byDID["7"]; !ok || cd.Slot != "3" {
		t.Errorf("DID 7 -> %s:%s, want 252:3", cd.Enclosure, cd.Slot)
	}
}

// ---- Real perccli2 JSON: DID->PID, State vs Status, NVMe namespace counters ----
// perccli2 (8.x) is JSON-native; the text form adds a second status column that
// breaks positional parsing, so parsePerccli2 consumes the JSON. Fixture is a
// real `perccli2 /call/eall/sall show all J` (Dell PM9A3 NVMe behind a PERC).
func TestParsePerccli2_Real(t *testing.T) {
	drives := parsePerccli2(readFixture(t, "perccli2_show_all.json"))
	if len(drives) != 2 {
		t.Fatalf("parsed %d drives, want 2", len(drives))
	}
	byPID := map[string]ctrlDrive{}
	for _, cd := range drives {
		byPID[cd.DeviceID] = cd
	}
	cd, ok := byPID["275"]
	if !ok {
		t.Fatal("PID 275 missing")
	}
	if cd.Enclosure != "284" || cd.Slot != "0" {
		t.Errorf("PID 275 location %s:%s, want 284:0", cd.Enclosure, cd.Slot)
	}
	// FwState comes from Status (Online), NOT State (Conf).
	if cd.FwState != "Online" {
		t.Errorf("PID 275 fw_state=%q, want Online", cd.FwState)
	}
	if cd.Model != "Dell DC NVMe PM9A3 RI U.2 960GB" || cd.Serial != "S6JGNA0X000001" {
		t.Errorf("PID 275 model=%q serial=%q", cd.Model, cd.Serial)
	}
	if cd.Rotation != "NVMe" {
		t.Errorf("PID 275 rotation=%q, want NVMe", cd.Rotation)
	}
	if iv(cd.TempC) != 30 {
		t.Errorf("PID 275 temp=%v, want 30", cd.TempC)
	}
	// NVMe error counters live under "LU/NS Properties", not directly in detail.
	if cd.MediaErr != 0 || cd.OtherErr != 0 || cd.Predictive != 0 {
		t.Errorf("PID 275 errors media=%d other=%d pred=%d, want 0/0/0",
			cd.MediaErr, cd.OtherErr, cd.Predictive)
	}
	if cd, ok := byPID["276"]; !ok || cd.Slot != "1" || iv(cd.TempC) != 32 {
		t.Errorf("PID 276 -> %s:%s temp=%v, want 284:1 / 32", cd.Enclosure, cd.Slot, cd.TempC)
	}
}

// ---- Controller-only drive: surfaced from controller data, scored not NO_DATA ----
func TestPerccli2ControllerOnlyDrive(t *testing.T) {
	drives := parsePerccli2(readFixture(t, "perccli2_show_all.json"))
	byPID := map[string]ctrlDrive{}
	for _, cd := range drives {
		byPID[cd.DeviceID] = cd
	}
	cd := byPID["275"]

	// Healthy controller-only drive: identity + health from controller only.
	d := &Drive{}
	applyController(d, cd)
	d.HaveSmart = false
	finalizeDerived(d)
	if d.enclosureSlot() != "284:0" {
		t.Errorf("enclosure_slot = %q, want 284:0", d.enclosureSlot())
	}
	if d.Model != "Dell DC NVMe PM9A3 RI U.2 960GB" || d.Rotation != "NVMe" {
		t.Errorf("identity model=%q rotation=%q", d.Model, d.Rotation)
	}
	if d.Recommendation != "OK" {
		t.Errorf("healthy controller-only rec=%q (score %d), want OK", d.Recommendation, d.RiskScore)
	}

	// A failed controller drive is surfaced (scored), never dropped as NO_DATA.
	cd.FwState = "Failed"
	df := &Drive{}
	applyController(df, cd)
	df.HaveSmart = false
	finalizeDerived(df)
	if df.Recommendation == "NO_DATA" {
		t.Errorf("failed controller-only drive scored NO_DATA")
	}
	if df.RiskScore < 40 {
		t.Errorf("failed controller-only drive score=%d, want >=40", df.RiskScore)
	}
}

// ---- Controller merge by DID, where DID != slot (perccli v007.2616) ----
// On util01 the slot<->DID numbering is crossed (slot 0 = DID 1, slot 1 =
// DID 0). smartctl's "megaraid,N" index equals the controller DID, so matching
// controller data by DID (not slot) must still yield the correct enclosure:slot.
func TestStorcli_DIDMatchingCrossed(t *testing.T) {
	idx := map[string]ctrlDrive{}
	for _, cd := range parseStorcli(readFixture(t, "perccli_show_all.txt")) {
		mergeCtrl(idx, cd)
	}
	cases := map[string]string{"0": "252:1", "1": "252:0"} // megaraid index -> enclosure:slot.
	for did, wantLoc := range cases {
		cd, ok := idx[did]
		if !ok {
			t.Errorf("DID %s missing from index", did)
			continue
		}
		d := &Drive{DeviceID: did} // DeviceID is the smartctl megaraid,N index.
		applyController(d, cd)
		if d.enclosureSlot() != wantLoc {
			t.Errorf("megaraid,%s -> %q, want %q", did, d.enclosureSlot(), wantLoc)
		}
	}
}

// ---- Real MegaCLI: matches storcli on the same hardware ----

func TestParseMegacli_Real(t *testing.T) {
	drives := parseMegacliPDList(readFixture(t, "megacli_pdlist.txt"))
	if len(drives) != 4 {
		t.Fatalf("parsed %d drives, want 4", len(drives))
	}
	byDID := map[string]ctrlDrive{}
	for _, cd := range drives {
		byDID[cd.DeviceID] = cd
	}
	cd, ok := byDID["20"]
	if !ok {
		t.Fatal("DID 20 missing")
	}
	// DID 20 == slot 2 (cross-checks the storcli capture above).
	if cd.Enclosure != "64" || cd.Slot != "2" || cd.OtherErr != 21 {
		t.Errorf("DID 20 = %s:%s other=%d, want 64:2 / 21", cd.Enclosure, cd.Slot, cd.OtherErr)
	}
	if cd.FwState != "Online, Spun Up" {
		t.Errorf("DID 20 fw_state=%q", cd.FwState)
	}
	if iv(cd.TempC) != 31 {
		t.Errorf("DID 20 temp=%v, want 31", cd.TempC)
	}
	if cd.SmartAlert {
		t.Errorf("DID 20 smart alert set, want false")
	}
}

// ---- MegaCLI SMART-alert line uses "Drive has flagged a S.M.A.R.T alert" ----

func TestMegacliSmartAlertPhrasing(t *testing.T) {
	text := `Enclosure Device ID: 64
Slot Number: 5
Device Id: 99
Firmware state: Online, Spun Up
Drive has flagged a S.M.A.R.T alert : Yes
`
	drives := parseMegacliPDList(text)
	if len(drives) != 1 {
		t.Fatalf("parsed %d drives, want 1", len(drives))
	}
	if !drives[0].SmartAlert {
		t.Errorf("smart alert not detected from MegaCLI phrasing")
	}
}

// ---- Controller state scoring: storcli abbreviations are not faults ----

func TestFwStateScoring(t *testing.T) {
	cases := []struct {
		state   string
		wantPts bool // true => the +40 fw_state penalty should apply
	}{
		{"Onln", false},
		{"Online, Spun Up", false},
		{"GHS", false},
		{"JBOD", false},
		{"", false},
		{"Offln", true},
		{"Failed", true},
		{"Rebuild", true},
	}
	for _, c := range cases {
		d := &Drive{HaveSmart: true, Model: "X", SmartHealth: "PASSED", FwState: c.state}
		score, _, _ := scoreDrive(d)
		got := score >= 40
		if got != c.wantPts {
			t.Errorf("fw_state %q: penalized=%v (score %d), want %v", c.state, got, score, c.wantPts)
		}
	}
}
