package main

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
)

// sampleDrive returns a populated drive for exercising the output encoders.
func sampleDrive() *Drive {
	d := &Drive{
		Hostname:        "kvm60",
		Model:           "Samsung SSD",
		Serial:          "S1",
		Firmware:        "1B6Q",
		SmartHealth:     "PASSED",
		Enclosure:       "64",
		Slot:            "3",
		WearSrc:         "nvme",
		TempC:           pInt(34),
		PowerOnHours:    pInt(17520),
		PowerCycleCount: pInt(12),
		WearPctConsumed: pInt(7),
		HostWrittenTB:   pF(12.5),
		SmartAlertCtrl:  true,
	}
	finalizeDerived(d)
	return d
}

// The Prometheus collector must emit numeric gauges named with the namespace
// prefix, carrying the shared identity label set with consistent cardinality.
func TestDriveExporterCollect(t *testing.T) {
	app = &App{config: defaultConfig()}
	app.config.Hostname = "kvm60"

	exp := NewDriveExporter()
	// Inject a fixed drive set so Collect runs without touching real hardware.
	exp.collect = func() ([]*Drive, int64) { return []*Drive{sampleDrive()}, 0 }
	reg := prometheus.NewRegistry()
	reg.MustRegister(exp)

	mfs, err := reg.Gather()
	if err != nil {
		t.Fatalf("gather: %v", err)
	}

	byName := map[string]*dto.MetricFamily{}
	for _, mf := range mfs {
		byName[mf.GetName()] = mf
	}

	// A representative int, float, and bool field must be present and typed.
	checks := map[string]float64{
		"drive_health_temp_c":            34,
		"drive_health_power_cycle_count": 12,
		"drive_health_host_written_tb":   12.5,
		"drive_health_smart_alert_ctrl":  1, // bool true -> 1
		"drive_health_risk_score":        float64(sampleDrive().RiskScore),
	}
	for name, want := range checks {
		mf, ok := byName[name]
		if !ok {
			t.Errorf("missing metric %s", name)
			continue
		}
		m := mf.GetMetric()[0]
		if got := m.GetGauge().GetValue(); got != want {
			t.Errorf("%s = %v, want %v", name, got, want)
		}
		// Identity labels must be attached.
		labels := map[string]string{}
		for _, l := range m.GetLabel() {
			labels[l.GetName()] = l.GetValue()
		}
		if labels["serial"] != "S1" || labels["hostname"] != "kvm60" || labels["enclosure_slot"] != "64:3" {
			t.Errorf("%s labels = %v", name, labels)
		}
	}
}

// The InfluxDB JSON encoder must produce one typed object per drive with tags,
// fields, and a microsecond timestamp.
func TestRecordsToInfluxJSON(t *testing.T) {
	out := recordsToInfluxJSON([]*Drive{sampleDrive()}, 1700000000000000000)
	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
	if len(lines) != 1 {
		t.Fatalf("got %d lines, want 1: %q", len(lines), out)
	}

	var obj struct {
		Name      string                 `json:"name"`
		Tags      map[string]string      `json:"tags"`
		Fields    map[string]interface{} `json:"fields"`
		Timestamp int64                  `json:"timestamp"`
	}
	if err := json.Unmarshal([]byte(lines[0]), &obj); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if obj.Name != influxMeasurement {
		t.Errorf("name = %q, want %q", obj.Name, influxMeasurement)
	}
	if obj.Tags["serial"] != "S1" || obj.Tags["model"] != "Samsung SSD" {
		t.Errorf("tags = %v", obj.Tags)
	}
	if obj.Timestamp != 1700000000000000 {
		t.Errorf("timestamp = %d, want microseconds", obj.Timestamp)
	}
	// int field decodes as a JSON number; bool field as a real bool.
	if v, ok := obj.Fields["temp_c"].(float64); !ok || v != 34 {
		t.Errorf("temp_c field = %v", obj.Fields["temp_c"])
	}
	if v, ok := obj.Fields["smart_alert_ctrl"].(bool); !ok || !v {
		t.Errorf("smart_alert_ctrl field = %v", obj.Fields["smart_alert_ctrl"])
	}
}
