package jt808

import (
	"encoding/binary"
	"encoding/hex"
	"testing"

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

const sampleLocationFrame = "7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F26063016235701040001900C2504000000000202000030011F31010F867E"
const real2019LocationFrame = "7E02004046010000000001489413506007CB00000000004C000301D276AB06FBFF4B000D014E00E826070122092214040000000017020000010400013361030201402504000000012A02000030011D310112EA0402008000C57E"
const fullAdditionalLocationFrame = "7e020000800123456789017fff000004000000080006eeb6ad02633df701380003006320070719235901040000000b02020016030200210402002c051e3737370000000000000000000000000000000000000000000000000000001105420000004212064d0000004d4d1307000000580058582504000000632a02000a2b040000001430011e31012806020001927e"

func TestExtractFramesUnescapesAndParsesLocationWithTotalMileage(t *testing.T) {
	stream, err := hex.DecodeString(sampleLocationFrame + "7E02000000")
	if err != nil {
		t.Fatal(err)
	}

	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 hex.EncodeToString(remainder) != "7e02000000" {
		t.Fatalf("expected partial remainder, got %s", hex.EncodeToString(remainder))
	}

	env, err := ParseFrame(frames[0], 1782745114999, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.Protocol != envelope.ProtocolJT808 {
		t.Fatalf("protocol = %q", env.Protocol)
	}
	if env.MessageID != "0x0200" {
		t.Fatalf("message id = %q", env.MessageID)
	}
	if env.Phone != "13307795425" {
		t.Fatalf("phone = %q", env.Phone)
	}
	if env.Sequence != 1 {
		t.Fatalf("sequence = %d", env.Sequence)
	}
	assertFloatField(t, env, envelope.FieldLatitude, 30.590151)
	assertFloatField(t, env, envelope.FieldLongitude, 121.069881)
	assertFloatField(t, env, envelope.FieldSpeedKMH, 23.0)
	assertFloatField(t, env, envelope.FieldTotalMileageKM, 10241.2)
	if env.Fields["direction_deg"] != uint16(79) {
		t.Fatalf("direction = %#v", env.Fields["direction_deg"])
	}

	location, ok := env.Parsed["location"].(map[string]any)
	if !ok {
		t.Fatalf("parsed location missing: %#v", env.Parsed)
	}
	if _, ok := location["additional"]; ok {
		t.Fatalf("additional wrapper should not be exposed: %#v", location["additional"])
	}
	if location[envelope.FieldTotalMileageKM] != 10241.2 {
		t.Fatalf("mileage should be flattened on location: %#v", location)
	}
	if _, ok := location["raw_value_tenth_km"]; ok {
		t.Fatalf("raw mileage helper should not be exposed: %#v", location)
	}
}

func TestParseFrameSupportsJT8082019VersionedHeader(t *testing.T) {
	stream, err := hex.DecodeString(real2019LocationFrame)
	if err != nil {
		t.Fatal(err)
	}

	frames, remainder, err := ExtractFrames(stream)
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 || len(remainder) != 0 {
		t.Fatalf("unexpected split result frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
	}

	env, err := ParseFrame(frames[0], 1782914967713, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.MessageID != "0x0200" {
		t.Fatalf("message id = %q", env.MessageID)
	}
	if env.Phone != "14894135060" {
		t.Fatalf("phone = %q", env.Phone)
	}
	if env.Sequence != 1995 {
		t.Fatalf("sequence = %d", env.Sequence)
	}
	assertFloatField(t, env, envelope.FieldLatitude, 30.570155)
	assertFloatField(t, env, envelope.FieldLongitude, 117.178187)
	assertFloatField(t, env, envelope.FieldSpeedKMH, 33.4)
	assertFloatField(t, env, envelope.FieldTotalMileageKM, 7868.9)
	if env.Fields["altitude_m"] != uint16(13) {
		t.Fatalf("altitude = %#v", env.Fields["altitude_m"])
	}
	if env.Fields["direction_deg"] != uint16(232) {
		t.Fatalf("direction = %#v", env.Fields["direction_deg"])
	}

	header, ok := env.Parsed["header"].(map[string]any)
	if !ok {
		t.Fatalf("parsed header missing: %#v", env.Parsed)
	}
	if header["versioned"] != true || header["protocol_version"] != byte(1) {
		t.Fatalf("unexpected versioned header: %#v", header)
	}
	if _, ok := header["phone_bcd_hex"]; ok {
		t.Fatalf("phone_bcd_hex should not be exposed: %#v", header)
	}
	if _, ok := header["phone_raw"]; ok {
		t.Fatalf("phone_raw should not be exposed: %#v", header)
	}
}

func TestParseFrameParsesCommonLocationAdditionalItems(t *testing.T) {
	stream, err := hex.DecodeString(fullAdditionalLocationFrame)
	if err != nil {
		t.Fatal(err)
	}
	frames, remainder, err := ExtractFrames(stream)
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 || len(remainder) != 0 {
		t.Fatalf("unexpected split result frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
	}

	env, err := ParseFrame(frames[0], 1782914967713, "127.0.0.1:808")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.Phone != "12345678901" {
		t.Fatalf("phone = %q", env.Phone)
	}
	assertFloatField(t, env, envelope.FieldTotalMileageKM, 1.1)

	location, ok := env.Parsed["location"].(map[string]any)
	if !ok {
		t.Fatalf("location missing: %#v", env.Parsed)
	}
	if _, ok := location["additional"]; ok {
		t.Fatalf("additional wrapper should not be exposed: %#v", location["additional"])
	}
	if location[envelope.FieldTotalMileageKM] != 1.1 {
		t.Fatalf("mileage parsed = %#v", location[envelope.FieldTotalMileageKM])
	}
	tirePressure, ok := location["tire_pressure"].(map[string]any)
	if !ok {
		t.Fatalf("tire pressure missing: %#v", location["tire_pressure"])
	}
	tireValues := tirePressure["values"].([]map[string]any)
	if tireValues[0]["value"] != byte(0x37) || tireValues[0]["valid"] != true {
		t.Fatalf("tire pressure first value = %#v", tireValues[0])
	}
	overspeed := location["overspeed_alarm"].(map[string]any)
	if overspeed["location_type"] != byte(0x42) || overspeed["area_id"] != uint32(0x42) {
		t.Fatalf("overspeed parsed = %#v", overspeed)
	}
	areaRoute := location["area_route_alarm"].(map[string]any)
	if areaRoute["location_type"] != byte(0x4d) || areaRoute["area_id"] != uint32(0x4d) || areaRoute["direction"] != byte(0x4d) {
		t.Fatalf("area route parsed = %#v", areaRoute)
	}
	roadSection := location["road_section_driving_time_alarm"].(map[string]any)
	if roadSection["road_section_id"] != uint32(0x58) || roadSection["driving_time_seconds"] != uint16(0x58) {
		t.Fatalf("road section parsed = %#v", roadSection)
	}
	ioStatus := location["io_status"].(map[string]any)
	ioBits := ioStatus["bits"].(map[string]bool)
	if ioBits["deep_sleep"] || !ioBits["sleep"] {
		t.Fatalf("io bits = %#v", ioBits)
	}
}

func TestParseFrameRejectsTruncatedSubpackageHeader(t *testing.T) {
	payload := []byte{0x02, 0x00, 0x20, 0x00}
	payload = append(payload, encodeBCD("013307795425", 6)...)
	payload = binary.BigEndian.AppendUint16(payload, 1)
	payload = append(payload, checksum(payload))

	_, err := ParseFrame(payload, 1782914967713, "115.231.168.135:43625")
	if err == nil {
		t.Fatal("expected truncated subpackage header to fail")
	}
}

func TestParseLocationAppliesSouthWestStatusBits(t *testing.T) {
	body := make([]byte, 28)
	binary.BigEndian.PutUint32(body[4:8], (1<<2)|(1<<3))
	binary.BigEndian.PutUint32(body[8:12], 12345678)
	binary.BigEndian.PutUint32(body[12:16], 98765432)

	location, err := parseLocation(body)
	if err != nil {
		t.Fatalf("parseLocation() error = %v", err)
	}
	if location.Latitude != -12.345678 {
		t.Fatalf("latitude = %v", location.Latitude)
	}
	if location.Longitude != -98.765432 {
		t.Fatalf("longitude = %v", location.Longitude)
	}
}

func TestParseFrameParsesRegistrationIdentity(t *testing.T) {
	body := make([]byte, 0, 46)
	body = binary.BigEndian.AppendUint16(body, 16)
	body = binary.BigEndian.AppendUint16(body, 32)
	body = appendFixedASCII(body, "YUTNG", 5)
	body = appendFixedASCII(body, "ZK6105CHEVNPG4", 20)
	body = appendFixedASCII(body, "DEV0001", 7)
	body = append(body, 2)
	body = append(body, []byte("TEST123")...)

	env, err := ParseFrame(buildFrame(0x0100, "013079963379", 7, body), 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.MessageID != "0x0100" {
		t.Fatalf("message id = %q", env.MessageID)
	}
	if env.Phone != "13079963379" {
		t.Fatalf("phone = %q", env.Phone)
	}
	if env.DeviceID != "DEV0001" {
		t.Fatalf("device id = %q", env.DeviceID)
	}
	if env.Plate != "TEST123" {
		t.Fatalf("plate = %q", env.Plate)
	}
	registration, ok := env.Parsed["registration"].(map[string]any)
	if !ok {
		t.Fatalf("registration missing: %#v", env.Parsed)
	}
	if registration["manufacturer"] != "YUTNG" || registration["device_type"] != "ZK6105CHEVNPG4" {
		t.Fatalf("registration fields = %#v", registration)
	}
	if registration["plate_color"] != uint8(2) {
		t.Fatalf("plate color = %#v", registration["plate_color"])
	}
}

func TestParseFrameParsesJT8082019RegistrationIdentity(t *testing.T) {
	body := make([]byte, 0, 84)
	body = binary.BigEndian.AppendUint16(body, 16)
	body = binary.BigEndian.AppendUint16(body, 32)
	body = appendFixedASCII(body, "YUTONG00001", 11)
	body = appendFixedASCII(body, "ZK6105CHEVNPG4-2019", 30)
	body = appendFixedASCII(body, "DEV201900000000000000000000001", 30)
	body = append(body, 2)
	body = append(body, []byte("TEST2019")...)

	env, err := ParseFrame(buildVersionedFrame(0x0100, "14894135060", 11, body), 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.Phone != "14894135060" {
		t.Fatalf("phone = %q", env.Phone)
	}
	if env.DeviceID != "DEV201900000000000000000000001" {
		t.Fatalf("device id = %q", env.DeviceID)
	}
	if env.Plate != "TEST2019" {
		t.Fatalf("plate = %q", env.Plate)
	}
	registration, ok := env.Parsed["registration"].(map[string]any)
	if !ok {
		t.Fatalf("registration missing: %#v", env.Parsed)
	}
	if registration["schema_version"] != "2019" || registration["manufacturer"] != "YUTONG00001" {
		t.Fatalf("registration fields = %#v", registration)
	}
}

func TestParseFrameDecodesGBKRegistrationPlate(t *testing.T) {
	body := make([]byte, 0, 46)
	body = binary.BigEndian.AppendUint16(body, 16)
	body = binary.BigEndian.AppendUint16(body, 32)
	body = appendFixedASCII(body, "YUTNG", 5)
	body = appendFixedASCII(body, "ZK6105CHEVNPG4", 20)
	body = appendFixedASCII(body, "DEV0002", 7)
	body = append(body, 2)
	body = append(body, []byte{0xBB, 0xA6, 'A', '5', '3', '3', '0', '1'}...)

	env, err := ParseFrame(buildFrame(0x0100, "013079963380", 9, body), 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.Plate != "沪A53301" {
		t.Fatalf("plate = %q", env.Plate)
	}
}

func TestParseFrameParsesJT8082019Authentication(t *testing.T) {
	body := []byte{5}
	body = append(body, []byte("TOKEN")...)
	body = appendFixedASCII(body, "123456789012345", 15)
	body = append(body, 4)
	body = append(body, []byte("v1.2")...)

	env, err := ParseFrame(buildVersionedFrame(0x0102, "14894135060", 12, body), 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	auth, ok := env.Parsed["authentication"].(map[string]any)
	if !ok {
		t.Fatalf("authentication missing: %#v", env.Parsed)
	}
	if auth["token"] != "TOKEN" || auth["imei"] != "123456789012345" || auth["software_version"] != "v1.2" {
		t.Fatalf("authentication = %#v", auth)
	}
}

func TestParseFrameParsesAuthenticationToken(t *testing.T) {
	env, err := ParseFrame(buildFrame(0x0102, "064646848757", 8, []byte("g7gps")), 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatalf("ParseFrame() error = %v", err)
	}
	if env.MessageID != "0x0102" {
		t.Fatalf("message id = %q", env.MessageID)
	}
	auth, ok := env.Parsed["authentication"].(map[string]any)
	if !ok {
		t.Fatalf("authentication missing: %#v", env.Parsed)
	}
	if auth["token"] != "g7gps" {
		t.Fatalf("token = %#v", auth["token"])
	}
}

func TestAutoResponderBuildsRegisterAck(t *testing.T) {
	body := make([]byte, 0, 46)
	body = binary.BigEndian.AppendUint16(body, 16)
	body = binary.BigEndian.AppendUint16(body, 32)
	body = appendFixedASCII(body, "YUTNG", 5)
	body = appendFixedASCII(body, "ZK6105CHEVNPG4", 20)
	body = appendFixedASCII(body, "DEV0001", 7)
	body = append(body, 2)
	body = append(body, []byte("TEST123")...)
	request := buildFrame(0x0100, "013079963379", 9, body)
	env, err := ParseFrame(request, 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatal(err)
	}

	response, ok, err := NewAutoResponder("g7gps").Respond(request, env)
	if err != nil {
		t.Fatalf("Respond() error = %v", err)
	}
	if !ok {
		t.Fatal("expected response")
	}
	frames, remainder, err := ExtractFrames(response)
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 || len(remainder) != 0 {
		t.Fatalf("unexpected response split frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
	}
	payload := frames[0]
	if binary.BigEndian.Uint16(payload[0:2]) != 0x8100 {
		t.Fatalf("message id = %04x", binary.BigEndian.Uint16(payload[0:2]))
	}
	if hex.EncodeToString(payload[4:10]) != "013079963379" {
		t.Fatalf("phone bcd = %x", payload[4:10])
	}
	if binary.BigEndian.Uint16(payload[12:14]) != 9 || payload[14] != 0 || string(payload[15:20]) != "g7gps" {
		t.Fatalf("unexpected register ack body: %x", payload[12:len(payload)-1])
	}
}

func TestAutoResponderBuildsVersionedGeneralAck(t *testing.T) {
	request := buildVersionedFrame(0x0200, "14894135060", 12, make([]byte, 28))
	env, err := ParseFrame(request, 1782918600000, "115.231.168.135:43625")
	if err != nil {
		t.Fatal(err)
	}

	response, ok, err := NewAutoResponder("").Respond(request, env)
	if err != nil {
		t.Fatalf("Respond() error = %v", err)
	}
	if !ok {
		t.Fatal("expected response")
	}
	frames, remainder, err := ExtractFrames(response)
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 || len(remainder) != 0 {
		t.Fatalf("unexpected response split frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
	}
	payload := frames[0]
	if binary.BigEndian.Uint16(payload[0:2]) != 0x8001 {
		t.Fatalf("message id = %04x", binary.BigEndian.Uint16(payload[0:2]))
	}
	if binary.BigEndian.Uint16(payload[2:4])&0x4000 == 0 || payload[4] != 1 {
		t.Fatalf("versioned header not preserved: %x", payload[:5])
	}
	if hex.EncodeToString(payload[5:15]) != "00000000014894135060" {
		t.Fatalf("phone bcd = %x", payload[5:15])
	}
	bodyStart := 17
	if binary.BigEndian.Uint16(payload[bodyStart:bodyStart+2]) != 12 ||
		binary.BigEndian.Uint16(payload[bodyStart+2:bodyStart+4]) != 0x0200 ||
		payload[bodyStart+4] != 0 {
		t.Fatalf("unexpected general ack body: %x", payload[bodyStart:len(payload)-1])
	}
}

func TestAutoResponderRejectsEnforcedInvalidAuthentication(t *testing.T) {
	request := buildFrame(0x0102, "064646848757", 13, []byte("wrong-code"))
	env, err := ParseFrame(request, 1782918600000, "127.0.0.1:808")
	if err != nil {
		t.Fatal(err)
	}
	env.AuthenticationEnforced = true
	env.AuthenticationStatus = "rejected"

	response, ok, err := NewAutoResponder("issued-code").Respond(request, env)
	if err != nil || !ok {
		t.Fatalf("Respond() ok=%v err=%v", ok, err)
	}
	frames, remainder, err := ExtractFrames(response)
	if err != nil || len(frames) != 1 || len(remainder) != 0 {
		t.Fatalf("ExtractFrames() frames=%d remainder=%x err=%v", len(frames), remainder, err)
	}
	payload := frames[0]
	if got := binary.BigEndian.Uint16(payload[0:2]); got != msgPlatformGeneralResponse {
		t.Fatalf("message id = 0x%04x", got)
	}
	if result := payload[len(payload)-2]; result != 1 {
		t.Fatalf("authentication response result = %d, want 1", result)
	}
}

func TestExtractFramesHandlesEscapedPayload(t *testing.T) {
	payload := []byte{0x02, 0x00, 0x00, 0x02, 0x01, 0x33, 0x07, 0x79, 0x54, 0x25, 0x00, 0x01, 0x7e, 0x7d}
	frame := append([]byte{0x7e}, escape(append(payload, checksum(payload)))...)
	frame = append(frame, 0x7e)

	frames, remainder, err := ExtractFrames(frame)
	if err != nil {
		t.Fatalf("ExtractFrames() error = %v", err)
	}
	if len(frames) != 1 || len(remainder) != 0 {
		t.Fatalf("unexpected split result frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
	}
	if hex.EncodeToString(frames[0][:len(payload)]) != hex.EncodeToString(payload) {
		t.Fatalf("frame was not unescaped: %s", hex.EncodeToString(frames[0]))
	}
}

func buildFrame(messageID uint16, phone string, sequence uint16, body []byte) []byte {
	payload := make([]byte, 0, 12+len(body)+1)
	payload = binary.BigEndian.AppendUint16(payload, messageID)
	payload = binary.BigEndian.AppendUint16(payload, uint16(len(body)))
	payload = append(payload, encodeBCD(phone, 6)...)
	payload = binary.BigEndian.AppendUint16(payload, sequence)
	payload = append(payload, body...)
	payload = append(payload, checksum(payload))
	return payload
}

func buildVersionedFrame(messageID uint16, phone string, sequence uint16, body []byte) []byte {
	payload := make([]byte, 0, 17+len(body)+1)
	payload = binary.BigEndian.AppendUint16(payload, messageID)
	payload = binary.BigEndian.AppendUint16(payload, 0x4000|uint16(len(body)))
	payload = append(payload, 1)
	payload = append(payload, encodeBCD(phone, 10)...)
	payload = binary.BigEndian.AppendUint16(payload, sequence)
	payload = append(payload, body...)
	payload = append(payload, checksum(payload))
	return payload
}

func appendFixedASCII(out []byte, value string, size int) []byte {
	start := len(out)
	out = append(out, make([]byte, size)...)
	copy(out[start:], []byte(value))
	return out
}

func encodeBCD(value string, size int) []byte {
	out := make([]byte, size)
	if len(value)%2 == 1 {
		value = "0" + value
	}
	if len(value) > size*2 {
		value = value[len(value)-size*2:]
	}
	for len(value) < size*2 {
		value = "0" + value
	}
	for i := 0; i < size; i++ {
		out[i] = (value[i*2]-'0')<<4 | (value[i*2+1] - '0')
	}
	return out
}

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 got != want {
		t.Fatalf("field %s = %v, want %v", key, got, want)
	}
}

func escape(payload []byte) []byte {
	var out []byte
	for _, value := range payload {
		switch value {
		case 0x7e:
			out = append(out, 0x7d, 0x02)
		case 0x7d:
			out = append(out, 0x7d, 0x01)
		default:
			out = append(out, value)
		}
	}
	return out
}
