package ankersolix

import (
	"slices"
	"testing"
)

func TestProjectSnapshotNamesTheChargerState(t *testing.T) {
	snap := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"voltageL1":      230.1,
		"currentL1":      16.0,
		"powerTotal":     3680.0,
		"sessionSeconds": 3725.0,
		"sessionWh":      12500.0,
		"status":         2.0,
		"ocppStatus":     2.0,
		"cpSignal":       5.0,
		"phaseMode":      1.0,
	})

	if snap.Serial != "SN1" || snap.Model != "A5191" {
		t.Errorf("snapshot identifies %s/%s", snap.Serial, snap.Model)
	}
	if snap.StatusDesc != stateCharging {
		t.Errorf("statusDesc = %q, want %q", snap.StatusDesc, stateCharging)
	}
	if snap.OcppStatusDesc != "connected" {
		t.Errorf("ocppStatusDesc = %q, want connected", snap.OcppStatusDesc)
	}
	// The control-pilot names are the charger's own, shared with the Modbus map.
	if snap.CPSignalDesc != cpSignalNames[5] {
		t.Errorf("cpSignalDesc = %q, want %q", snap.CPSignalDesc, cpSignalNames[5])
	}
	if snap.VoltageL1 == nil || *snap.VoltageL1 != 230.1 {
		t.Errorf("voltageL1 = %v", snap.VoltageL1)
	}
	// A quantity the charger did not send stays nil, so a view can tell "not
	// reported" from "zero" — an unplugged charger really does read 0 A.
	if snap.VoltageL2 != nil {
		t.Errorf("voltageL2 = %v, want nil for a value that was not sent", *snap.VoltageL2)
	}
	if snap.Settings != nil {
		t.Errorf("settings = %+v, want none until a settings message arrives", snap.Settings)
	}
	if snap.Local != nil {
		t.Errorf("local = %+v, want none until a settings message arrives", snap.Local)
	}
}

// The mode is the one thing the cloud REST view can only approximate: it has no
// boost flag and no countdowns, so a charger waiting out a start delay reads to
// it as simply started. Over MQTT those fields exist, and the mode must use them.
func TestProjectSnapshotDerivesModeFromTheMqttOnlySignals(t *testing.T) {
	waiting := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"status":                1.0, // preparing
		"startCountdownSeconds": 45.0,
	})
	if waiting.Mode != modeWaitStart {
		t.Errorf("mode = %q, want %q while a start delay is running", waiting.Mode, modeWaitStart)
	}
	if !slices.Contains(waiting.ModeOptions, modeSkipDelay) {
		t.Errorf("modeOptions = %v, want the delay to be skippable", waiting.ModeOptions)
	}

	plugging := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"status":               1.0,
		"plugCountdownSeconds": 60.0,
	})
	if plugging.Mode != modeWaitPlug {
		t.Errorf("mode = %q, want %q while it waits for a plug", plugging.Mode, modeWaitPlug)
	}

	boosting := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"status":    2.0, // charging
		"boostMode": 1.0,
	})
	if boosting.Mode != modeBoostCharge {
		t.Errorf("mode = %q, want %q while boost is running", boosting.Mode, modeBoostCharge)
	}

	idle := projectMqttSnapshot("SN1", "A5191", map[string]any{"status": 0.0})
	if idle.Mode != modeStopCharge {
		t.Errorf("mode = %q, want %q in standby", idle.Mode, modeStopCharge)
	}
	if !slices.Contains(idle.ModeOptions, modeStartCharge) {
		t.Errorf("modeOptions = %v, want a standby charger to be startable", idle.ModeOptions)
	}
}

// Two of the charger's settings read 1 for on and 2 for off, which is the
// opposite of every other flag it sends: read as booleans they would both come
// back on.
func TestProjectSnapshotHandlesTheInvertedSwitches(t *testing.T) {
	off := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"plugLockSwitch": 2.0,
		"scheduleSwitch": 2.0,
	})
	if off.Settings == nil {
		t.Fatal("settings missing")
	}
	if off.Settings.PlugLock == nil || *off.Settings.PlugLock {
		t.Errorf("plugLock = %v, want off for the charger's value 2", off.Settings.PlugLock)
	}
	if off.Settings.ScheduleEnabled == nil || *off.Settings.ScheduleEnabled {
		t.Errorf("scheduleEnabled = %v, want off for the charger's value 2", off.Settings.ScheduleEnabled)
	}

	on := projectMqttSnapshot("SN1", "A5191", map[string]any{"plugLockSwitch": 1.0})
	if on.Settings == nil || on.Settings.PlugLock == nil || !*on.Settings.PlugLock {
		t.Errorf("plugLock = %v, want on for the charger's value 1", on.Settings)
	}
}

// The settings message carries the charger's own view of its LAN side, which is
// the address the Modbus mode otherwise has to be told by hand.
func TestProjectSnapshotReportsLocalAccess(t *testing.T) {
	snap := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"modbusSwitch":         1.0,
		"ipAddress":            "192.168.1.44",
		"modbusPort":           502.0,
		"modbusTimeoutSeconds": 60.0,
		"maxCurrentSetA":       32.0,
	})
	if snap.Local == nil {
		t.Fatal("local access missing")
	}
	if snap.Local.ModbusEnabled == nil || !*snap.Local.ModbusEnabled {
		t.Errorf("modbusEnabled = %v, want on", snap.Local.ModbusEnabled)
	}
	if snap.Local.Host != "192.168.1.44" {
		t.Errorf("host = %q", snap.Local.Host)
	}
	if snap.Local.Port == nil || *snap.Local.Port != 502 {
		t.Errorf("port = %v, want 502", snap.Local.Port)
	}
	if snap.Settings == nil || snap.Settings.MaxCurrentA == nil || *snap.Settings.MaxCurrentA != 32 {
		t.Errorf("settings.maxCurrentA = %+v, want 32", snap.Settings)
	}
}

func TestProjectSnapshotOfNothingIsEmpty(t *testing.T) {
	snap := projectMqttSnapshot("SN1", "A5191", map[string]any{})
	if snap.Status != nil || snap.Mode != "" || snap.Settings != nil || snap.Local != nil {
		t.Errorf("an empty message set produced state: %+v", snap)
	}
}

// A value the projection has a field for belongs in that field; extra is what is
// left, which — now that every name in the message maps is projected — is the
// fields no map names at all.
func TestProjectSnapshotKeepsWhatItHasNoFieldFor(t *testing.T) {
	snap := projectMqttSnapshot("SN1", "A5191", map[string]any{
		"powerTotal":                       3680.0,
		"maxCurrentSetA":                   16.0,
		"sessionWhL1":                      4100.0,
		rawFieldName(msgEVTelemetry, 0xc9): 300.0,
		rawFieldName("0400", 0xa2):         7.0,
	})

	for _, key := range []string{"powerTotal", "maxCurrentSetA", "sessionWhL1"} {
		if _, ok := snap.Extra[key]; ok {
			t.Errorf("extra[%q] is set; a value with a field of its own must not be repeated there", key)
		}
	}
	for key, want := range map[string]any{"0410.c9": 300.0, "0400.a2": 7.0} {
		if snap.Extra[key] != want {
			t.Errorf("extra[%q] = %v, want %v", key, snap.Extra[key], want)
		}
	}

	// A charger whose every value has a field of its own reports no extra at all,
	// so a view can hide the block rather than draw an empty one.
	bare := projectMqttSnapshot("SN1", "A5191", map[string]any{"powerTotal": 0.0})
	if bare.Extra != nil {
		t.Errorf("extra = %v, want none when nothing is left over", bare.Extra)
	}
}

// Every field the message maps name has somewhere to land. A name added to a map
// without a field to project it into would otherwise show up in the raw block
// under a name that reads as if it were understood.
func TestEveryNamedFieldIsProjected(t *testing.T) {
	values := map[string]any{}
	for _, fields := range []map[byte]mqttField{evTelemetry, evParams, evCharging} {
		for _, f := range fields {
			values[f.name] = 1.0
		}
	}
	snap := projectMqttSnapshot("SN1", "A5191", values)
	if len(snap.Extra) != 0 {
		t.Errorf("these named fields reach no snapshot field: %v", snap.Extra)
	}
}
