package ankersolix

import (
	"strings"
	"testing"
)

// cmdFor finds the command that carries a setting, the way a write does.
func cmdFor(t *testing.T, key string) settingCmd {
	t.Helper()
	i, ok := settingIndex[key]
	if !ok {
		t.Fatalf("%q is not a writable setting", key)
	}
	return settingCmds[i]
}

// A single-field setting is one field between the opener and the timestamp, so
// the frame is checkable byte for byte up to the clock.
func TestEncodeSettingCmdWritesOneField(t *testing.T) {
	frame, err := encodeSettingCmd(cmdFor(t, "ledBrightness"), map[string]any{"ledBrightness": 50.0}, nil)
	if err != nil {
		t.Fatalf("encodeSettingCmd: %v", err)
	}
	got := encodeHex(frame)
	// header (type 0100), the a1 opener, aa as a one-byte 50, then fe and the sum.
	if want := "ff09180003000f0100a10122aa020132fe0503"; !strings.HasPrefix(got, want) {
		t.Fatalf("frame = %s\nwant prefix %s", got, want)
	}
	if sum := xorChecksum(frame); sum != 0 {
		t.Errorf("XOR over the whole frame = %02x, want 00", sum)
	}
}

// Plug lock and the schedule switch are the two fields where on is 1 and off is
// 2. Getting that backwards would silently invert them, so both are pinned.
func TestSwitchesUseTheirOwnOnOffValues(t *testing.T) {
	cases := []struct {
		key  string
		on   bool
		want byte
	}{
		{"plugLock", true, 1},
		{"plugLock", false, 2},
		{"autoStart", true, 1},
		{"autoStart", false, 0},
	}
	for _, tc := range cases {
		cmd := cmdFor(t, tc.key)
		var field settingField
		for _, f := range cmd.fields {
			if f.key == tc.key {
				field = f
			}
		}
		got, err := field.encode(field.wire, tc.on)
		if err != nil {
			t.Fatalf("%s(%v): %v", tc.key, tc.on, err)
		}
		if len(got.value) != 1 || got.value[0] != tc.want {
			t.Errorf("%s(%v) = %v, want %d", tc.key, tc.on, got.value, tc.want)
		}
	}
}

// The charger reads a grouped command as the whole truth, so a write that names
// one of its settings must carry the others as the charger last reported them.
func TestGroupedSettingResendsSiblingsFromState(t *testing.T) {
	state := map[string]any{
		"lightOffScheduleSwitch": 1.0,
		"lightOffStart":          "22:30",
		"lightOffEnd":            "06:15",
	}
	frame, err := encodeSettingCmd(cmdFor(t, "lightOffSchedule"),
		map[string]any{"lightOffSchedule": false}, state)
	if err != nil {
		t.Fatalf("encodeSettingCmd: %v", err)
	}
	fields := fieldsOf(t, frame)
	// b4 the switch, then the two times as hour<<8|minute, least significant byte
	// first — 22:30 and 06:15 exactly as the charger reported them.
	if got := encodeHex(fields[0xb4]); got != "00" {
		t.Errorf("switch = %s, want 00 — the value the write asked for", got)
	}
	if got := encodeHex(fields[0xb5]); got != "1e16" {
		t.Errorf("start = %s, want 1e16 (22:30) — the charger's own value, carried through", got)
	}
	if got := encodeHex(fields[0xb6]); got != "0f06" {
		t.Errorf("end = %s, want 0f06 (06:15) — the charger's own value, carried through", got)
	}
}

// A sibling the charger has never reported is the one case where the write
// stops: sending a made-up value would set it for real.
func TestGroupedSettingRefusesWithoutState(t *testing.T) {
	_, err := encodeSettingCmd(cmdFor(t, "lightOffSchedule"),
		map[string]any{"lightOffSchedule": true}, map[string]any{})
	if err == nil {
		t.Fatal("a grouped write with no state was accepted")
	}
	if !strings.Contains(err.Error(), "lightOffStart") {
		t.Errorf("error does not name the missing sibling: %v", err)
	}
}

// The load-balancing group carries a meter serial nothing here can invent, so it
// travels back exactly as the charger reported it.
func TestLoadBalancingCarriesTheMeterBack(t *testing.T) {
	state := map[string]any{
		"loadBalancing":          0.0,
		"loadBalanceMonitorMode": 1.0,
		"loadBalanceMeterFlag":   1.0,
		"loadBalanceMonitorSN":   "SM1234567890",
	}
	frame, err := encodeSettingCmd(cmdFor(t, "loadBalancing"),
		map[string]any{"loadBalancing": true}, state)
	if err != nil {
		t.Fatalf("encodeSettingCmd: %v", err)
	}
	if got := encodeHex(frame[7:9]); got != msgEVBalancing {
		t.Errorf("message type = %s, want %s", got, msgEVBalancing)
	}
	if !strings.Contains(string(frame), "SM1234567890") {
		t.Errorf("the meter serial is not in the frame: %s", encodeHex(frame))
	}
}

// Values are checked against the ranges the charger takes, before anything is
// published — a step, a bound, a clock and a name are all caught here.
func TestSettingValuesAreValidated(t *testing.T) {
	cases := []struct {
		name string
		key  string
		val  any
	}{
		{"brightness off its 10% step", "ledBrightness", 55.0},
		{"brightness above 100", "ledBrightness", 120.0},
		{"breaker limit below 10 A", "mainBreakerLimitA", 5.0},
		{"breaker limit above 500 A", "mainBreakerLimitA", 600.0},
		{"a current that pauses instead of charging", "maxCurrentA", 3.0},
		{"a schedule mode the charger has no name for", "scheduleMode", 7.0},
		{"a time that is not a time", "weekStart", "half past nine"},
		{"a switch that is not a switch", "autoStart", "maybe"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cmd := cmdFor(t, tc.key)
			for _, f := range cmd.fields {
				if f.key != tc.key {
					continue
				}
				if _, err := f.encode(f.wire, tc.val); err == nil {
					t.Errorf("%s = %v was accepted", tc.key, tc.val)
				}
			}
		})
	}
}

// Amps go on the wire in deciamps, the same as the Modbus register carries them.
func TestMaxCurrentIsWrittenInDeciamps(t *testing.T) {
	frame, err := encodeSettingCmd(cmdFor(t, "maxCurrentA"), map[string]any{"maxCurrentA": 16.0}, nil)
	if err != nil {
		t.Fatalf("encodeSettingCmd: %v", err)
	}
	// 16 A is 160 on the wire, two bytes, least significant first.
	if got := encodeHex(fieldsOf(t, frame)[0xa8]); got != "a000" {
		t.Errorf("a8 = %s, want a000 (160 deciamps)", got)
	}
}

// Two settings on the same command belong in one frame; two on different
// commands are two frames, in the table's order.
func TestSettingsGroupByCommand(t *testing.T) {
	byCmd := map[int]bool{}
	for _, k := range []string{"weekStart", "weekEnd", "ledBrightness"} {
		byCmd[settingIndex[k]] = true
	}
	if len(byCmd) != 2 {
		t.Errorf("three settings landed on %d commands, want 2", len(byCmd))
	}
	if settingIndex["weekStart"] != settingIndex["weekEnd"] {
		t.Error("the two week times are not on the same command")
	}
}

// Every settable name has a decode entry behind it, so a caller can read a
// snapshot and send the same names back.
func TestEverySettingReadsBack(t *testing.T) {
	decoded := map[string]bool{}
	for _, f := range evParams {
		decoded[f.name] = true
	}
	for _, cmd := range settingCmds {
		for _, f := range cmd.fields {
			if !decoded[f.state] {
				t.Errorf("setting %q reads its current value from %q, which nothing decodes", f.key, f.state)
			}
		}
	}
}

// fieldsOf splits a command frame into its data fields, keyed by wire name and
// stripped of the value type. A command message type has no decode map of its
// own — the charger reports these values back on a different message — so a
// command frame is checked as the bytes it is.
func fieldsOf(t *testing.T, frame []byte) map[byte][]byte {
	t.Helper()
	raw, ok := splitFields(frame, frameHeaderLen)
	if !ok {
		t.Fatalf("frame does not divide into whole data fields: %s", encodeHex(frame))
	}
	out := map[byte][]byte{}
	for _, f := range raw {
		out[f.name] = f.value
	}
	return out
}
