package ankersolix

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

// The realtime trigger frame is the one example the reference implementation
// documents byte for byte, so it is the anchor for the whole encoder: marker,
// little-endian length counting the checksum, send pattern, message type, then
// the fields in order.
func TestEncodeFrameMatchesTheDocumentedTrigger(t *testing.T) {
	at := time.Unix(1756813256, 0)
	got, err := encodeFrame(msgRealtimeTrigger, []cmdField{
		rawField(0xa1, 0x22),
		uintField(0xa2, 1),
		varField(0xa3, 300),
		timestampField(at),
	})
	if err != nil {
		t.Fatalf("encodeFrame: %v", err)
	}

	want := "ff091f0003000f0057" + // header: marker, length 31, send pattern, type 0057
		"a10122" + // a1: one byte, no value type
		"a2020101" + // a2: ui 1 — updates on
		"a305032c010000" + // a3: var 300 — the window in seconds
		"fe0503c8d7b668" // fe: var — the sender's clock
	wantWithSum := want + "21"
	if h := encodeHex(got); h != wantWithSum {
		// Recompute the checksum in the message so a mismatch says which half broke.
		body, _ := decodeHex(want)
		t.Fatalf("frame = %s\nwant   %s (checksum over the body is %02x)", h, wantWithSum, xorChecksum(body))
	}
	if len(got) != 31 {
		t.Errorf("frame is %d bytes, want 31", len(got))
	}
	if binary.LittleEndian.Uint16(got[2:4]) != uint16(len(got)) {
		t.Errorf("header length %d does not match the frame's %d bytes", binary.LittleEndian.Uint16(got[2:4]), len(got))
	}
}

// The status request is the one command whose timestamp travels without a value
// type — the app sends it that way, and the charger answers what the app sends.
func TestStatusRequestSendsItsClockWithoutAValueType(t *testing.T) {
	got, err := encodeFrame(msgEVStatusReq, []cmdField{bareTimestampField(time.Unix(1756813256, 0))})
	if err != nil {
		t.Fatalf("encodeFrame: %v", err)
	}
	want := "ff09100003000f0040" + // header: marker, length 16, send pattern, type 0040
		"fe04c8d7b668" // fe: four clock bytes, no value type between the length and them
	if h := encodeHex(got); h[:len(want)] != want {
		t.Fatalf("frame = %s\nwant   %s + checksum", h, want)
	}
	if len(got) != 16 {
		t.Errorf("frame is %d bytes, want 16", len(got))
	}
	// The field's length byte counts the value alone, since there is no type byte
	// to count — the whole point of the oddity.
	if got[10] != 4 {
		t.Errorf("fe length byte is %d, want 4", got[10])
	}
	var sum byte
	for _, b := range got {
		sum ^= b
	}
	if sum != 0 {
		t.Errorf("checksum does not close the frame: %02x", sum)
	}
}

// The restart carries the one value the map documents for the power-mode
// command, opened and closed like every other command.
func TestRestartFrameCarriesThePowerModeValue(t *testing.T) {
	got, err := encodeFrame(msgEVPowerMode, []cmdField{
		rawField(0xa1, 0x22),
		uintField(0xa2, powerModeRestart),
		timestampField(time.Unix(1756813256, 0)),
	})
	if err != nil {
		t.Fatalf("encodeFrame: %v", err)
	}
	want := "ff09180003000f0108" + // header: marker, length 24, send pattern, type 0108
		"a10122" + // a1: the opener, no value type
		"a2020105" + // a2: ui 5 — restart
		"fe0503c8d7b668" // fe: var — the sender's clock
	if h := encodeHex(got); h[:len(want)] != want {
		t.Fatalf("frame = %s / want %s + checksum", h, want)
	}
	var sum byte
	for _, b := range got {
		sum ^= b
	}
	if sum != 0 {
		t.Errorf("checksum does not close the frame: %02x", sum)
	}
}

// Both spellings reach the restart; nothing else does.
func TestIsRestartTakesEitherName(t *testing.T) {
	for _, name := range []string{"restart", "reset"} {
		if !isRestart(name) {
			t.Errorf("%q should ask for a restart", name)
		}
	}
	for _, name := range []string{"reboot", "start", "stop", "trigger", ""} {
		if isRestart(name) {
			t.Errorf("%q should not ask for a restart", name)
		}
	}
}

// A frame is only self-consistent if XORing every byte, checksum included,
// comes to zero — which is exactly what the decoder checks.
func TestEncodeFrameChecksumClosesToZero(t *testing.T) {
	frame, err := encodeFrame(msgEVMode, []cmdField{
		rawField(0xa1, 0x22),
		uintField(0xa2, mqttModeValues[modeStartCharge]),
		timestampField(time.Unix(1756813256, 0)),
	})
	if err != nil {
		t.Fatalf("encodeFrame: %v", err)
	}
	if sum := xorChecksum(frame); sum != 0 {
		t.Errorf("XOR over the whole frame = %02x, want 00", sum)
	}
}

func TestEncodeFrameRejectsBadMessageType(t *testing.T) {
	for _, mt := range []string{"", "01", "zz01", "01020304"} {
		if _, err := encodeFrame(mt, []cmdField{rawField(0xa1, 0x22)}); err == nil {
			t.Errorf("encodeFrame(%q) accepted a message type it should not", mt)
		}
	}
}

// buildInbound assembles a device-style frame the way the charger sends one:
// the receive pattern, and no counter byte before the first field.
func buildInbound(t *testing.T, msgType string, body []byte) []byte {
	t.Helper()
	mt, err := decodeHex(msgType)
	if err != nil {
		t.Fatalf("bad message type %q: %v", msgType, err)
	}
	out := append([]byte{}, frameMarker...)
	out = binary.LittleEndian.AppendUint16(out, uint16(frameHeaderLen+len(body)+1))
	out = append(out, 0x03, 0x01, 0x0f)
	out = append(out, mt...)
	out = append(out, body...)
	return append(out, xorChecksum(out))
}

func field(name, typ byte, value ...byte) []byte {
	return append([]byte{name, byte(len(value) + 1), typ}, value...)
}

func TestDecodeFrameReadsTelemetry(t *testing.T) {
	var body []byte
	body = append(body, field(0xa2, typeInt16LE, 0xfd, 0x08)...) // 2301 -> 230.1 V
	body = append(body, field(0xa5, typeInt16LE, 0xa0, 0x00)...) // 160 -> 16.0 A
	body = append(body, field(0xa8, typeInt32LE, 0x60, 0x0e, 0x00, 0x00)...)
	body = append(body, field(0xa9, typeInt32LE, 0x8d, 0x0e, 0x00, 0x00)...)
	body = append(body, field(0xaa, typeInt32LE, 0xd4, 0x30, 0x00, 0x00)...)
	body = append(body, field(0xae, typeInt32LE, 0x2d, 0x00, 0x00, 0x00)...)
	body = append(body, []byte{0xbb, 0x01, 0x02}...) // single byte, no value type
	body = append(body, field(0xb8, typeUint8, 0x02)...)

	msgType, values, err := decodeFrame(buildInbound(t, msgEVTelemetry, body))
	if err != nil {
		t.Fatalf("decodeFrame: %v", err)
	}
	if msgType != msgEVTelemetry {
		t.Errorf("message type = %s, want %s", msgType, msgEVTelemetry)
	}
	want := map[string]float64{
		"voltageL1":             230.1,
		"currentL1":             16,
		"powerTotal":            3680,
		"sessionSeconds":        3725,
		"sessionWh":             12500,
		"startCountdownSeconds": 45,
		"status":                2,
		"ocppStatus":            2,
	}
	for k, v := range want {
		got, ok := values[k].(float64)
		if !ok {
			t.Errorf("%s missing from the decoded values (%v)", k, values[k])
			continue
		}
		if got != v {
			t.Errorf("%s = %v, want %v", k, got, v)
		}
	}
}

// The settings message carries the charger's own view of its LAN side and its
// schedule, which are two- and four-byte fields read differently from the
// telemetry's: a clock field is a minute and an hour, not a number.
func TestDecodeFrameReadsSettings(t *testing.T) {
	var body []byte
	body = append(body, field(0xa8, typeInt16LE, 0x40, 0x01)...) // 320 -> 32.0 A
	body = append(body, field(0xb7, typeUint8, 0x01)...)         // Modbus TCP on
	body = append(body, field(0xcf, typeInt16LE, 0xf6, 0x01)...) // port 502
	body = append(body, field(0xd0, typeString, []byte("192.168.1.44")...)...)
	body = append(body, field(0xe7, typeInt16LE, 0x00, 0x16)...) // 22:00
	body = append(body, field(0xdf, typeUint8, 0x01)...)         // boost running

	_, values, err := decodeFrame(buildInbound(t, msgEVParams, body))
	if err != nil {
		t.Fatalf("decodeFrame: %v", err)
	}
	if v, _ := values["maxCurrentSetA"].(float64); v != 32 {
		t.Errorf("maxCurrentSetA = %v, want 32", values["maxCurrentSetA"])
	}
	if v, _ := values["modbusPort"].(float64); v != 502 {
		t.Errorf("modbusPort = %v, want 502", values["modbusPort"])
	}
	if v, _ := values["ipAddress"].(string); v != "192.168.1.44" {
		t.Errorf("ipAddress = %q, want 192.168.1.44", values["ipAddress"])
	}
	// The two bytes are minute then hour, so reading them as a plain little-endian
	// number would give 5632 rather than a time of day.
	if v, _ := values["weekStart"].(string); v != "22:00" {
		t.Errorf("weekStart = %q, want 22:00", values["weekStart"])
	}
	if v, _ := values["boostMode"].(float64); v != 1 {
		t.Errorf("boostMode = %v, want 1", values["boostMode"])
	}
}

// A frame reaches us over a cloud connection we do not control, so a truncated
// or corrupted one must be refused rather than read as a charger reporting
// zeros — which would silently show a charging car as idle.
func TestDecodeFrameRejectsDamagedFrames(t *testing.T) {
	good := buildInbound(t, msgEVTelemetry, field(0xbb, typeUint8, 0x02))

	corrupt := append([]byte{}, good...)
	corrupt[len(corrupt)-2] ^= 0xff
	if _, _, err := decodeFrame(corrupt); err == nil {
		t.Error("a frame with a flipped value byte passed the checksum")
	}

	truncated := append([]byte{}, good[:len(good)-3]...)
	if _, _, err := decodeFrame(truncated); err == nil {
		t.Error("a truncated frame was accepted")
	}

	wrongMarker := append([]byte{}, good...)
	wrongMarker[0] = 0xfe
	if _, _, err := decodeFrame(wrongMarker); err == nil {
		t.Error("a frame without the Anker marker was accepted")
	}

	if _, _, err := decodeFrame([]byte{0xff, 0x09}); err == nil {
		t.Error("a frame too short to hold a header was accepted")
	}
}

// Some inbound frames carry a counter byte between the header and the first
// field; skipping it wrongly would shift every field name by one.
func TestDecodeFrameSkipsTheCounterByte(t *testing.T) {
	body := append([]byte{0x07}, field(0xbb, typeUint8, 0x05)...)
	_, values, err := decodeFrame(buildInbound(t, msgEVTelemetry, body))
	if err != nil {
		t.Fatalf("decodeFrame: %v", err)
	}
	if v, _ := values["status"].(float64); v != 5 {
		t.Errorf("status = %v, want 5 (the counter byte was not skipped)", values["status"])
	}
}

// A message type we have no map for still has to parse, and what it carried is
// kept under the message and name byte it arrived with rather than dropped: an
// unnamed field is the only place some of what the charger knows appears.
func TestDecodeFrameOfAnUnmappedTypeKeepsRawFields(t *testing.T) {
	_, values, err := decodeFrame(buildInbound(t, "0400", field(0xa2, typeUint8, 0x01)))
	if err != nil {
		t.Fatalf("decodeFrame: %v", err)
	}
	if v, _ := values["0400.a2"].(float64); v != 1 {
		t.Errorf("values = %v, want 0400.a2 = 1", values)
	}
}

// A field a mapped message does not name is kept the same way, beside the ones
// it does — and unscaled, since a factor is part of a meaning we do not have.
func TestDecodeFrameKeepsUnnamedFieldsBesideNamedOnes(t *testing.T) {
	body := append(field(0xbb, typeUint8, 0x05), field(0xc9, typeInt16LE, 0x2c, 0x01)...)
	_, values, err := decodeFrame(buildInbound(t, msgEVTelemetry, body))
	if err != nil {
		t.Fatalf("decodeFrame: %v", err)
	}
	if v, _ := values["status"].(float64); v != 5 {
		t.Errorf("status = %v, want 5", values["status"])
	}
	if v, _ := values[rawFieldName(msgEVTelemetry, 0xc9)].(float64); v != 300 {
		t.Errorf("0410.c9 = %v, want 300 unscaled", values[rawFieldName(msgEVTelemetry, 0xc9)])
	}
}

func TestDecodeValueSignsAndScales(t *testing.T) {
	// Two's-complement over two bytes: a relay reading below zero must stay below
	// zero rather than wrapping to 6553.5.
	v, ok := decodeValue(typeInt16LE, []byte{0xf6, 0xff}, mqttField{name: "x", factor: 0.1})
	if !ok || v.(float64) != -1 {
		t.Errorf("signed 2-byte value = %v (ok=%v), want -1", v, ok)
	}
	// The same bytes read unsigned are a large positive number, which is what the
	// fields marked unsigned actually mean.
	v, ok = decodeValue(typeInt16LE, []byte{0xf6, 0xff}, mqttField{name: "x", unsigned: true})
	if !ok || v.(float64) != 65526 {
		t.Errorf("unsigned 2-byte value = %v (ok=%v), want 65526", v, ok)
	}
	// A value shorter than its type is dropped rather than read as a smaller one.
	if _, ok := decodeValue(typeInt32LE, []byte{0x01, 0x02}, mqttField{name: "x"}); ok {
		t.Error("a 2-byte value was accepted for a 4-byte type")
	}
	// Scaling must not leave floating-point dust behind.
	v, _ = decodeValue(typeInt32LE, []byte{0xd4, 0x30, 0x00, 0x00}, mqttField{name: "x", factor: 0.001})
	if v.(float64) != 12.5 {
		t.Errorf("scaled value = %v, want 12.5", v)
	}
}

func TestPrintableKeepsOnlyReadableText(t *testing.T) {
	if got := printable([]byte("192.168.1.44\x00\x00")); got != "192.168.1.44" {
		t.Errorf("printable = %q, want %q", got, "192.168.1.44")
	}
}

func TestDecodeHexRejectsRubbish(t *testing.T) {
	for _, s := range []string{"abc", "zz", "00 11"} {
		if _, err := decodeHex(s); err == nil {
			t.Errorf("decodeHex(%q) accepted a non-hex string", s)
		}
	}
	b, err := decodeHex("FF09")
	if err != nil || len(b) != 2 || b[0] != 0xff || b[1] != 0x09 {
		t.Errorf("decodeHex(%q) = %v, %v", "FF09", b, err)
	}
	if s := encodeHex([]byte{0xff, 0x09}); s != "ff09" {
		t.Errorf("encodeHex = %q, want ff09", s)
	}
	if strings.ToUpper(encodeHex([]byte{0xab})) != "AB" {
		t.Errorf("encodeHex is not lowercase hex")
	}
}

// The three identity fields the reference leaves alone: four bytes that are the
// parts of a version, kept in the order they arrived rather than rearranged into
// one we cannot check.
func TestDecodeVersionFields(t *testing.T) {
	v, ok := decodeValue(typeInt32LE, []byte{1, 0, 6, 1}, mqttField{name: "softwareVersion", version: true})
	if !ok || v != "1.0.6.1" {
		t.Errorf("softwareVersion = %v (ok=%v), want 1.0.6.1", v, ok)
	}
	// A field that is not four bytes is text where it can be read as text, and
	// the bytes themselves where it cannot — never nothing.
	if v, ok := decodeValue(typeString, []byte("V1.2\x00"), mqttField{version: true}); !ok || v != "V1.2" {
		t.Errorf("text version = %v (ok=%v), want V1.2", v, ok)
	}
	if v, ok := decodeValue(typeInt32LE, []byte{0x01, 0x02}, mqttField{version: true}); !ok || v != "0102" {
		t.Errorf("unreadable version = %v (ok=%v), want its bytes", v, ok)
	}
}
