package ankersolix

import (
	"context"
	"encoding/binary"
	"strings"
	"testing"
)

// liveBlock builds a 20041-20100 register block with every register zero, ready
// for a test to set the ones it cares about by address.
type liveBlock []uint16

func newLiveBlock() liveBlock { return make(liveBlock, liveCount) }

func (b liveBlock) set(addr int, v uint16) { b[addr-liveStart] = v }

func (b liveBlock) set32(addr int, v uint32) {
	b[addr-liveStart] = uint16(v >> 16)
	b[addr-liveStart+1] = uint16(v)
}

func TestDecodeLiveScalesMeasurements(t *testing.T) {
	b := newLiveBlock()
	b.set(regVoltageL1N, 2301)   // 230.1 V, gain 10
	b.set(regVoltageL1N+1, 2312) // 231.2 V
	b.set(regVoltageL1N+2, 2298) // 229.8 V
	b.set(regCurrentL1, 1600)    // 16.00 A, gain 100
	b.set(regCurrentL1+1, 1598)  // 15.98 A
	b.set(regCurrentL1+2, 0)
	b.set32(regPowerL1, 3680)
	b.set32(regPowerTotal, 11040)
	b.set32(regSessionSec, 3725)
	b.set32(regSessionWh, 12500)

	var snap ModbusSnapshot
	decodeLive(&snap, b)

	if snap.VoltageL1 == nil || *snap.VoltageL1 != 230.1 {
		t.Errorf("VoltageL1 = %v, want 230.1", snap.VoltageL1)
	}
	if snap.VoltageL3 == nil || *snap.VoltageL3 != 229.8 {
		t.Errorf("VoltageL3 = %v, want 229.8", snap.VoltageL3)
	}
	if snap.CurrentL1 == nil || *snap.CurrentL1 != 16.0 {
		t.Errorf("CurrentL1 = %v, want 16.0", snap.CurrentL1)
	}
	if snap.CurrentL2 == nil || *snap.CurrentL2 != 15.98 {
		t.Errorf("CurrentL2 = %v, want 15.98", snap.CurrentL2)
	}
	// The 32-bit values straddle two registers; a swapped pair would read as a
	// wildly different number rather than a near miss.
	if snap.PowerL1 == nil || *snap.PowerL1 != 3680 {
		t.Errorf("PowerL1 = %v, want 3680", snap.PowerL1)
	}
	if snap.PowerTotal == nil || *snap.PowerTotal != 11040 {
		t.Errorf("PowerTotal = %v, want 11040", snap.PowerTotal)
	}
	if snap.SessionSeconds == nil || *snap.SessionSeconds != 3725 {
		t.Errorf("SessionSeconds = %v, want 3725", snap.SessionSeconds)
	}
	if snap.SessionWh == nil || *snap.SessionWh != 12500 {
		t.Errorf("SessionWh = %v, want 12500", snap.SessionWh)
	}
}

func TestDecodeLiveNamesStates(t *testing.T) {
	b := newLiveBlock()
	b.set(regChargingStatus, 2) // charging
	b.set(regCPSignal, 5)       // C1, vehicle drawing
	b.set(regOCPPStatus, 2)     // connected
	b.set(regMQTTStatus, 1)     // connected
	b.set(regBoostMode, 1)
	b.set(regLoadBalancing, 0)
	b.set(regLEDBrightness, 70)

	var snap ModbusSnapshot
	decodeLive(&snap, b)

	// The Modbus status enum is the same one the cloud reports, so it must decode
	// to the same names the rest of the plugin already uses.
	if snap.Status == nil || *snap.Status != 2 || snap.StatusDesc != stateCharging {
		t.Errorf("status = %v/%q, want 2/%q", snap.Status, snap.StatusDesc, stateCharging)
	}
	if snap.CPSignalDesc != cpSignalNames[5] {
		t.Errorf("CPSignalDesc = %q, want %q", snap.CPSignalDesc, cpSignalNames[5])
	}
	if snap.OcppStatusDesc != "connected" {
		t.Errorf("OcppStatusDesc = %q, want connected", snap.OcppStatusDesc)
	}
	// Register value 1 means "connected" for MQTT but only "connecting" for OCPP;
	// the two registers must not be decoded through one table.
	if snap.MqttStatusDesc != "connected" {
		t.Errorf("MqttStatusDesc = %q, want connected", snap.MqttStatusDesc)
	}
	if snap.BoostMode == nil || !*snap.BoostMode {
		t.Errorf("BoostMode = %v, want true", snap.BoostMode)
	}
	if snap.LoadBalancing == nil || *snap.LoadBalancing {
		t.Errorf("LoadBalancing = %v, want false", snap.LoadBalancing)
	}
	if snap.LEDBrightness == nil || *snap.LEDBrightness != 70 {
		t.Errorf("LEDBrightness = %v, want 70", snap.LEDBrightness)
	}
}

func TestDecodeLiveReadsNegativeTemperatures(t *testing.T) {
	b := newLiveBlock()
	below := int16(-72)
	b.set(regRelay1Temp, uint16(below))
	// 331 is what an idle charger actually reports: 33.1 °C, not 331 °C.
	b.set(regRelay2Temp, 331)

	var snap ModbusSnapshot
	decodeLive(&snap, b)

	if snap.Relay1TempC == nil || *snap.Relay1TempC != -7.2 {
		t.Errorf("Relay1TempC = %v, want -7.2", snap.Relay1TempC)
	}
	if snap.Relay2TempC == nil || *snap.Relay2TempC != 33.1 {
		t.Errorf("Relay2TempC = %v, want 33.1", snap.Relay2TempC)
	}
}

func TestDecodeLiveFlagsAlarms(t *testing.T) {
	quiet := newLiveBlock()
	var snap ModbusSnapshot
	decodeLive(&snap, quiet)
	if snap.Alarm {
		t.Error("Alarm set with every alarm word zero")
	}
	if len(snap.Alarms) != 12 {
		t.Errorf("got %d alarm words, want 12", len(snap.Alarms))
	}

	noisy := newLiveBlock()
	noisy.set(regAlarm1+7, 0x0040)
	var snap2 ModbusSnapshot
	decodeLive(&snap2, noisy)
	if !snap2.Alarm {
		t.Error("Alarm not set although an alarm bit is on")
	}
	if snap2.Alarms[7] != 0x0040 {
		t.Errorf("alarm word 8 = 0x%04x, want 0x0040", snap2.Alarms[7])
	}
}

// A short block must leave fields absent rather than decode whatever follows.
func TestDecodeLiveToleratesShortBlock(t *testing.T) {
	var snap ModbusSnapshot
	decodeLive(&snap, []uint16{1, 2, 3})
	if snap.Status != nil {
		t.Errorf("Status = %v, want nil from a truncated block", snap.Status)
	}
	if snap.PowerTotal != nil {
		t.Errorf("PowerTotal = %v, want nil from a truncated block", snap.PowerTotal)
	}
}

func TestDecodeIdentityReadsStrings(t *testing.T) {
	regs := make([]uint16, identityCount)
	put := func(addr int, s string, count int) {
		b := []byte(s)
		for len(b) < count*2 {
			b = append(b, 0)
		}
		for i := 0; i < count; i++ {
			regs[addr-identityStart+i] = binary.BigEndian.Uint16(b[i*2:])
		}
	}
	put(regModelName, "A5191", 10)
	put(regSerialNumber, "V1SN0123456789AB", 12)
	put(regSoftwareVersion, "1.2.3", 6)
	put(regHardwareVersion, "1.0", 6)

	var snap ModbusSnapshot
	decodeIdentity(&snap, regs)

	if snap.Model != "A5191" {
		t.Errorf("Model = %q, want A5191", snap.Model)
	}
	if snap.Serial != "V1SN0123456789AB" {
		t.Errorf("Serial = %q, want V1SN0123456789AB", snap.Serial)
	}
	if snap.Firmware != "1.2.3" {
		t.Errorf("Firmware = %q, want 1.2.3", snap.Firmware)
	}
	if snap.Hardware != "1.0" {
		t.Errorf("Hardware = %q, want 1.0", snap.Hardware)
	}
}

func TestDecodeIdentityReadsRatings(t *testing.T) {
	regs := make([]uint16, identityCount)
	put32 := func(addr int, v uint32) {
		regs[addr-identityStart] = uint16(v >> 16)
		regs[addr-identityStart+1] = uint16(v)
	}
	regs[regProductNumber-identityStart] = 7
	put32(regRatedPower, 7400)
	put32(regMinOutCurrent, 6)
	put32(regMaxOutCurrent, 32)

	var snap ModbusSnapshot
	decodeIdentity(&snap, regs)

	for _, c := range []struct {
		name string
		got  *int
		want int
	}{
		{"ProductNumber", snap.ProductNumber, 7},
		{"RatedPowerW", snap.RatedPowerW, 7400},
		{"MinCurrentA", snap.MinCurrentA, 6},
		{"MaxCurrentA", snap.MaxCurrentA, 32},
	} {
		if c.got == nil || *c.got != c.want {
			t.Errorf("%s = %v, want %d", c.name, c.got, c.want)
		}
	}
}

// The control block says what the charger is set to, which is a different
// question from what it is doing: a boost that was asked for reads here even
// while the live block reports no boost running.
func TestDecodeSettingsReadsTheControlBlock(t *testing.T) {
	regs := make([]uint16, settingsCount)
	regs[regChargingCommand-settingsStart] = 1
	regs[regMaxCurrentSet-settingsStart] = 160 // 16.0 A, gain 10
	regs[regBoostSet-settingsStart] = 1
	regs[regTimeoutSet-settingsStart] = 120
	regs[regPhaseCountSet-settingsStart] = 2

	var snap ModbusSnapshot
	decodeSettings(&snap, regs)

	if snap.Settings == nil {
		t.Fatal("Settings is nil")
	}
	got := snap.Settings
	if got.LastCommand == nil || *got.LastCommand != 1 {
		t.Errorf("LastCommand = %v, want 1", got.LastCommand)
	}
	if got.MaxCurrentA == nil || *got.MaxCurrentA != 16 {
		t.Errorf("MaxCurrentA = %v, want 16", got.MaxCurrentA)
	}
	if got.Boost == nil || !*got.Boost {
		t.Errorf("Boost = %v, want true", got.Boost)
	}
	if got.TimeoutSeconds == nil || *got.TimeoutSeconds != 120 {
		t.Errorf("TimeoutSeconds = %v, want 120", got.TimeoutSeconds)
	}
	if got.PhaseSetting == nil || *got.PhaseSetting != 2 || got.PhaseDesc != "fixed three-phase" {
		t.Errorf("PhaseSetting = %v (%q), want 2 (fixed three-phase)", got.PhaseSetting, got.PhaseDesc)
	}
}

// The registers below were read into the live block all along but never
// decoded, so the block's own alignment is what these guard.
func TestDecodeLiveReadsLineAndReactivePower(t *testing.T) {
	b := newLiveBlock()
	b.set(regVoltageL1L2, 3809) // 380.9 V, gain 10
	b.set32(regReactiveL1, 120)
	b.set32(regApparentL1+4, 340)
	b.set(regPWMEnabled, 1)
	b.set(regCPVoltage, 11873) // 11.873 V, millivolts

	var snap ModbusSnapshot
	decodeLive(&snap, b)

	if snap.VoltageL1L2 == nil || *snap.VoltageL1L2 != 380.9 {
		t.Errorf("VoltageL1L2 = %v, want 380.9", snap.VoltageL1L2)
	}
	if snap.ReactiveL1 == nil || *snap.ReactiveL1 != 120 {
		t.Errorf("ReactiveL1 = %v, want 120", snap.ReactiveL1)
	}
	if snap.ApparentL3 == nil || *snap.ApparentL3 != 340 {
		t.Errorf("ApparentL3 = %v, want 340", snap.ApparentL3)
	}
	if snap.PWMEnabled == nil || !*snap.PWMEnabled {
		t.Errorf("PWMEnabled = %v, want true", snap.PWMEnabled)
	}
	if snap.CPVoltage == nil || *snap.CPVoltage != 11.873 {
		t.Errorf("CPVoltage = %v, want 11.873", snap.CPVoltage)
	}
}

// Below 6 A the charger stops instead of charging slowly, so a limit in that
// range has to be refused rather than quietly turned into a pause.
func TestModbusSetMaxCurrentRefusesBelowFloor(t *testing.T) {
	err := ModbusSetMaxCurrent(context.Background(), nil, 4)
	if err == nil {
		t.Fatal("4 A was accepted")
	}
	if !strings.Contains(err.Error(), "pauses charging") {
		t.Errorf("error = %q, want it to explain the pause", err)
	}
}

func TestModbusSetMaxCurrentRefusesOutOfRange(t *testing.T) {
	for _, amps := range []float64{-1, 40} {
		if err := ModbusSetMaxCurrent(context.Background(), nil, amps); err == nil {
			t.Errorf("%.0f A was accepted", amps)
		}
	}
}

func TestModbusSetPhaseModeRejectsUnknownMode(t *testing.T) {
	if err := ModbusSetPhaseMode(context.Background(), nil, 3); err == nil {
		t.Fatal("phase mode 3 was accepted")
	}
}

func TestModbusSetTimeoutEnforcesFloor(t *testing.T) {
	if err := ModbusSetTimeout(context.Background(), nil, 5); err == nil {
		t.Fatal("a 5 second timeout was accepted, but the spec requires more than 5")
	}
}

func TestModbusConfigDefaultsToPort502(t *testing.T) {
	if got := (ModbusConfig{Host: "10.2.1.55"}).Address(); got != "10.2.1.55:502" {
		t.Errorf("address = %q, want 10.2.1.55:502", got)
	}
	if got := (ModbusConfig{Host: "10.2.1.55", Port: 1502}).Address(); got != "10.2.1.55:1502" {
		t.Errorf("address = %q, want 10.2.1.55:1502", got)
	}
}

func TestModbusDialRequiresHost(t *testing.T) {
	if _, err := ModbusDial(context.Background(), ModbusConfig{}); err == nil {
		t.Fatal("an empty host was accepted")
	}
}
