package gb32960

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"strings"
	"testing"

	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)

const realFuelCellFrame = "232302FE4C423941333241323152304C53313730370102ED1A070116091C0102030100000008297D161F27104C020007D0000002010104564E204E205815CA271003000A000000B40002666902800200000100A101000400FFFF001205000733444601E2B5DB06013A0F6C018E0F4001014B01054A07000000000000000000080101161F271000900001900F520F530F520F640F660F650F650F650F650F660F650F650F640F640F550F550F540F560F580F570F540F570F560F550F5A0F550F580F570F5C0F5F0F5E0F610F5E0F5D0F5E0F5F0F5E0F5D0F610F5F0F610F600F600F610F610F610F600F630F630F600F610F610F610F620F620F610F6A0F6C0F6B0F6A0F6C0F6B0F6A0F6B0F6B0F6B0F6A0F610F630F630F630F640F660F640F630F630F630F580F5A0F580F5B0F550F580F5A0F5A0F590F580F5A0F620F650F650F650F660F640F640F640F640F640F660F650F640F650F660F650F670F670F670F670F630F620F680F670F650F650F670F650F610F5F0F600F620F5E0F5C0F590F4C0F490F480F440F470F480F470F420F430F450F420F450F440F440F430F530F540F520F400F410F4109010100084B4B4B4B4A4A4B4A3001026907B2FF00FF00380002002800000008006C00016C00080008000000080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008002800070007000700070007000700070007000700070007000700070007000700070007000700070007000700070007000700080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000800080008000831010E9C0005FFFFFFFF0E9CFFFFFFFF0083320E9C1B6115E212465733FFFFFFFFFF34FFFFFFFFFF002B800009690E9C2710000D000E83002402020500002700080001000000FD00000000FFFFFFFFFFFFFFFFFFFFFFFF1FFE1FF3001DBA"

func TestExtractFramesKeepsPartialRemainderAndParsesHeader(t *testing.T) {
	first := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", []byte{0x1a, 0x06, 0x1e, 0x16, 0x17, 0x39})
	second := buildFrame(0x05, 0xfe, "PLATFORM-LOGIN001", nil)
	stream := append([]byte{0x00, 0x01}, first...)
	stream = append(stream, second[:len(second)-3]...)

	frames, remainder, err := ExtractFrames(stream)
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 {
		t.Fatalf("expected one complete frame, got %d", len(frames))
	}
	if string(remainder) != string(second[:len(second)-3]) {
		t.Fatalf("expected partial second frame as remainder, got %s", hex.EncodeToString(remainder))
	}

	env, err := ParseFrame(frames[0], 1782745114999, "115.29.187.205:32960")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.Protocol != envelope.ProtocolGB32960 || env.MessageID != "0x02" {
		t.Fatalf("unexpected envelope header: %#v", env)
	}
	if env.VIN != "LNBSCB3D4R1234567" {
		t.Fatalf("unexpected vin: %q", env.VIN)
	}
}

func TestParseFrameRejectsBadBCC(t *testing.T) {
	frame := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", nil)
	frame[len(frame)-1] ^= 0xff

	if _, err := ParseFrame(frame, 1782745114999, ""); err == nil {
		t.Fatal("expected bad bcc error")
	}
}

func TestExtractFramesSupportsExtendedPlatformLogin(t *testing.T) {
	body := []byte{0x1a, 0x07, 0x02, 0x00, 0x26, 0x1f, 0x00, 0x01}
	body = append(body, fixedASCII("Hyundai", 12)...)
	body = append(body, fixedASCII("f2e3445d7cda409fb4f278f6fb890734", 32)...)
	body = append(body, 0x01)
	frame := buildFrameWithDeclaredLength(0x05, 0xfe, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", body, 41)

	frames, remainder, err := ExtractFrames(append(frame, []byte{0x23, 0x23, 0x05}...))
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 || len(remainder) != 3 {
		t.Fatalf("unexpected split frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
	}
	if len(frames[0]) != headerLen+53+1 {
		t.Fatalf("extended frame len = %d", len(frames[0]))
	}

	env, err := ParseFrame(frames[0], 1782745114999, "8.134.95.166:39376")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.MessageID != "0x05" || env.ParseStatus != envelope.ParseOK {
		t.Fatalf("unexpected env: %#v", env)
	}
	header := env.Parsed["header"].(map[string]any)
	if header["body_length"] != 41 || header["actual_body_length"] != 53 {
		t.Fatalf("unexpected length header: %#v", header)
	}
	login := env.Parsed["platform_login"].(map[string]any)
	if login["username"] != "Hyundai" || login["password"] != "f2e3445d7cda409fb4f278f6fb890734" {
		t.Fatalf("unexpected platform login: %#v", login)
	}
	if login["login_time"] != "2026-07-02T00:38:31+08:00" {
		t.Fatalf("unexpected login time: %#v", login)
	}
	if env.Fields["platform_account"] != "Hyundai" {
		t.Fatalf("platform account not exposed: %#v", env.Fields)
	}
}

func TestAutoResponseEchoesRawVINAndOriginalTime(t *testing.T) {
	body := []byte{0x1a, 0x07, 0x02, 0x00, 0x26, 0x0f, 0x00, 0x01}
	body = append(body, fixedASCII("Hyundai", 12)...)
	body = append(body, fixedASCII("ack-test-password", 32)...)
	body = append(body, 0x01)
	request := buildFrameWithDeclaredLength(0x05, 0xfe, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", body, 41)
	env, err := ParseFrame(request, 1782914969584, "8.134.95.166:39376")
	if err != nil {
		t.Fatal(err)
	}

	response, ok, err := AutoResponse(request, env)
	if err != nil {
		t.Fatalf("AutoResponse() error = %v", err)
	}
	if !ok {
		t.Fatal("expected response")
	}
	if response[2] != 0x05 || response[3] != 0x01 {
		t.Fatalf("unexpected response header: %x", response[:4])
	}
	if hex.EncodeToString(response[4:21]) != "0000000000000000000000000000000000" {
		t.Fatalf("raw vin not echoed: %x", response[4:21])
	}
	if hex.EncodeToString(response[24:30]) != "1a070200260f" {
		t.Fatalf("timestamp body not preserved: %x", response[24:30])
	}
	if got, want := bcc(response[2:len(response)-1]), response[len(response)-1]; got != want {
		t.Fatalf("response bcc got=0x%02x want=0x%02x", got, want)
	}
}

func TestAutoResponseRejectsEnforcedInvalidPlatformLogin(t *testing.T) {
	body := []byte{0x1a, 0x07, 0x02, 0x00, 0x26, 0x0f, 0x00, 0x01}
	body = append(body, fixedASCII("platform-a", 12)...)
	body = append(body, fixedASCII("wrong-password", 20)...)
	body = append(body, 0x01)
	request := buildFrame(0x05, 0xfe, "12345678901234567", body)
	env, err := ParseFrame(request, 1782914969584, "127.0.0.1:32960")
	if err != nil {
		t.Fatal(err)
	}
	env.AuthenticationEnforced = true
	env.AuthenticationStatus = "rejected"

	response, ok, err := AutoResponse(request, env)
	if err != nil || !ok {
		t.Fatalf("AutoResponse() ok=%v err=%v", ok, err)
	}
	if response[3] != responseError {
		t.Fatalf("response flag = 0x%02x, want 0x%02x", response[3], responseError)
	}
}

func TestParseFrameExtractsRealtimeVehicleMileageAndPosition(t *testing.T) {
	body := []byte{0x1a, 0x06, 0x1e, 0x16, 0x17, 0x39}
	body = append(body, 0x01)
	body = append(body,
		0x01,       // vehicle status
		0x03,       // charge status
		0x02,       // running mode
		0x01, 0x2c, // speed: 30.0km/h
		0x00, 0x01, 0x86, 0xa0, // total mileage: 10000.0km
		0x15, 0xe5, // total voltage: 560.5V
		0x27, 0x10, // total current: 0A after -1000 offset
		85, 0x01, 0x00,
		0x27, 0x10,
		0x00, 0x00)
	body = append(body, 0x05)
	body = append(body,
		0x00,
		0x07, 0x36, 0x50, 0x40, // longitude: 121.000000
		0x01, 0xd2, 0x4f, 0x00) // latitude: 30.560000
	frame := buildFrame(0x02, 0xfe, "LNBSCB3D4R1234567", body)

	env, err := ParseFrame(frame, 1782745114999, "115.29.187.205:32960")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	assertFloatField(t, env, envelope.FieldSpeedKMH, 30.0)
	assertFloatField(t, env, envelope.FieldTotalMileageKM, 10000.0)
	assertFloatField(t, env, envelope.FieldLongitude, 121.0)
	assertFloatField(t, env, envelope.FieldLatitude, 30.56)
	if env.Fields[envelope.FieldSOCPercent] != 85 {
		t.Fatalf("unexpected soc: %#v", env.Fields[envelope.FieldSOCPercent])
	}
	if env.EventTimeMS == 1782745114999 {
		t.Fatal("event time should come from GB32960 device timestamp")
	}
}

func TestParseFrameKeepsParsingRealFuelCellReportUntilUnknownExtension(t *testing.T) {
	frame, err := hex.DecodeString(realFuelCellFrame)
	if err != nil {
		t.Fatal(err)
	}

	env, err := ParseFrame(frame, 1782914969584, "115.29.187.205:32960")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.VIN != "LB9A32A21R0LS1707" {
		t.Fatalf("vin = %q", env.VIN)
	}
	assertFloatField(t, env, envelope.FieldTotalMileageKM, 53490.9)
	assertFloatField(t, env, "fuel_cell_hydrogen_consumption_kg_per_100km", 1.8)
	assertFloatField(t, env, envelope.FieldLongitude, 120.800326)
	assertFloatField(t, env, envelope.FieldLatitude, 31.634907)
	assertFloatField(t, env, "gd_fc_stack_hydrogen_inlet_pressure_kpa", 97.0)
	assertFloatField(t, env, "gd_fc_vehicle_hydrogen_mass_kg", 4.3)
	assertIntField(t, env, "gd_fc_demo_stack_temp_c", 65)

	units, ok := env.Parsed["data_units"].([]map[string]any)
	if !ok {
		t.Fatalf("data_units missing: %#v", env.Parsed["data_units"])
	}
	gotTypes := make([]string, 0, len(units))
	for _, unit := range units {
		gotTypes = append(gotTypes, fmt.Sprint(unit["type"]))
	}
	wantTypes := []string{"0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", "0x08", "0x09", "0x30", "0x31", "0x32", "0x33", "0x34", "0x80", "0x83"}
	if fmt.Sprint(gotTypes) != fmt.Sprint(wantTypes) {
		t.Fatalf("unit types = %v, want %v", gotTypes, wantTypes)
	}
	if units[1]["name"] != "drive_motor" {
		t.Fatalf("unexpected motor unit: %#v", units[1])
	}
	if units[2]["name"] != "fuel_cell" {
		t.Fatalf("unexpected fuel cell unit: %#v", units[2])
	}
	if units[9]["name"] != "gd_fc_stack" {
		t.Fatalf("unexpected gd stack unit: %#v", units[9])
	}
	if units[10]["name"] != "gd_fc_auxiliary" {
		t.Fatalf("unexpected gd auxiliary unit: %#v", units[10])
	}
	if units[15]["name"] != "gd_fc_vendor_tlv" {
		t.Fatalf("unexpected gd vendor tlv unit: %#v", units[15])
	}
	tlv := units[15]["value"].(map[string]any)
	if tlv["declared_length"] != 36 {
		t.Fatalf("unexpected vendor tlv: %#v", tlv)
	}
}

func TestScaledDecimalFieldsDoNotLeakBinaryFloatTails(t *testing.T) {
	unit := parseDriveMotorData([]byte{
		0x01,
		0x01,
		0x02,
		0x5d,
		0x6c, 0xc6,
		0x4e, 0x20,
		0x64,
		0x15, 0xb8,
		0x27, 0x6e, // 10094 -> 9.4A after -1000 offset.
	})
	data, err := json.Marshal(unit)
	if err != nil {
		t.Fatal(err)
	}
	text := string(data)
	if !strings.Contains(text, `"controller_current_a":9.4`) {
		t.Fatalf("controller_current_a should render as 9.4 without float tail: %s", text)
	}
	if strings.Contains(text, "999999") || strings.Contains(text, "000000000000") {
		t.Fatalf("scaled decimal field leaked binary float tail: %s", text)
	}

	aux := parseGdFCAuxiliaryData([]byte{
		0x01,
		0x01, 0x74,
		0x00, 0x05,
		0xff, 0xff,
		0x00, 0x05,
		0x01, 0x74,
		0xff, 0xff,
		0x00, 0x05,
		0x00, 0x84, // 132 -> 13.2V.
	})
	data, err = json.Marshal(aux)
	if err != nil {
		t.Fatal(err)
	}
	text = string(data)
	if !strings.Contains(text, `"low_voltage_battery_voltage_v":13.2`) {
		t.Fatalf("low voltage should render as 13.2 without float tail: %s", text)
	}
	if strings.Contains(text, "13.200000000000") {
		t.Fatalf("auxiliary scaled decimal leaked binary float tail: %s", text)
	}
}

func assertFloatField(t *testing.T, env envelope.FrameEnvelope, key string, want float64) {
	t.Helper()
	got, ok := env.Fields[key].(float64)
	if !ok {
		t.Fatalf("field %s missing or not float64: %#v", key, env.Fields[key])
	}
	if math.Abs(got-want) > 0.000001 {
		t.Fatalf("field %s = %v, want %v", key, got, want)
	}
}

func assertIntField(t *testing.T, env envelope.FrameEnvelope, key string, want int) {
	t.Helper()
	got, ok := env.Fields[key].(int)
	if !ok {
		t.Fatalf("field %s missing or not int: %#v", key, env.Fields[key])
	}
	if got != want {
		t.Fatalf("field %s = %v, want %v", key, got, want)
	}
}

func buildFrame(command byte, response byte, vin string, body []byte) []byte {
	return buildFrameWithDeclaredLength(command, response, vin, body, len(body))
}

func buildFrameWithDeclaredLength(command byte, response byte, vin string, body []byte, declaredBodyLen int) []byte {
	frame := []byte{'#', '#', command, response}
	vinBytes := []byte(vin)
	if len(vinBytes) < 17 {
		vinBytes = append(vinBytes, make([]byte, 17-len(vinBytes))...)
	}
	frame = append(frame, vinBytes[:17]...)
	frame = append(frame, 0x01, byte(declaredBodyLen>>8), byte(declaredBodyLen))
	frame = append(frame, body...)
	return append(frame, bcc(frame[2:]))
}

func fixedASCII(value string, size int) []byte {
	out := make([]byte, size)
	copy(out, []byte(value))
	return out
}
