package history

import (
	"bytes"
	"context"
	"database/sql"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
)

func TestRawFrameRepositoryQueriesRawFramesWithFilters(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, .* AS parsed_fields, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_jt808_").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			time.Date(2026, 7, 1, 23, 25, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
			"go_frame", "event-1", 0x0200,
			time.Date(2026, 7, 1, 23, 25, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
			time.Date(2026, 7, 1, 23, 25, 37, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
			64, "7E0200", "", `{"header":{"message_id":"0x0200"}}`,
			"OK", "", "222.66.200.68:29646", "JT808", "013079963379", "LKLG7C4E3NA774736", "013079963379", "9963379",
		))

	repository := NewRawFrameRepository(db, "lingniu_vehicle_ts")
	rows, err := repository.Query(context.Background(), RawFrameQuery{
		Protocol:      "JT808",
		VIN:           "LKLG7C4E3NA774736",
		DateFrom:      "2026-07-01 00:00:00",
		DateTo:        "2026-07-01 23:59:59",
		IncludeFields: true,
		Limit:         20,
	})
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("row count = %d", len(rows))
	}
	if rows[0].MessageID != 512 || rows[0].MessageIDHex != "0x0200" || len(rows[0].ParsedFields) == 0 {
		t.Fatalf("unexpected row: %#v", rows[0])
	}
	if rows[0].TS != "2026-07-01 23:25:36" {
		t.Fatalf("timestamp = %q", rows[0].TS)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameRepositoryHydratesChunkedParsedFields(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()

	manifest := `{"chunked":true,"payload_kind":"parsed_fields","event_id":"event-oversized","chunk_count":2}`
	mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, .* AS parsed_fields, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_gb32960_").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-02 10:00:00", "go_frame", "event-oversized", 2,
			"2026-07-02 10:00:00", "2026-07-02 10:00:00", 4096,
			"2323", "", manifest, "OK", "", "8.134.95.166:53702",
			"GB32960", "LB9A32A21R0LS1707", "LB9A32A21R0LS1707", "", "",
		))
	mock.ExpectQuery("SELECT event_id, payload_kind, chunk_index, chunk_text FROM lingniu_vehicle_ts.raw_frame_payload_chunks").
		WillReturnRows(sqlmock.NewRows([]string{"event_id", "payload_kind", "chunk_index", "chunk_text"}).
			AddRow("event-oversized", "parsed_fields", 0, `{"data_units":[`).
			AddRow("event-oversized", "parsed_fields", 1, `{"field":"value"}]}`))

	repository := NewRawFrameRepository(db, "lingniu_vehicle_ts")
	rows, err := repository.Query(context.Background(), RawFrameQuery{
		Protocol:      "GB32960",
		VIN:           "LB9A32A21R0LS1707",
		IncludeFields: true,
		Limit:         1,
	})
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("row count = %d", len(rows))
	}
	if want := `{"data_units":[{"field":"value"}]}`; string(rows[0].ParsedFields) != want {
		t.Fatalf("hydrated parsed_fields = %q, want %q", string(rows[0].ParsedFields), want)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerReturnsRawFrames(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM lingniu_vehicle_ts.raw_gb32960_").
		WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(38))
	mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, .* AS parsed_fields, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_gb32960_").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-01 22:28:25", "go_frame", "event-2", 2, "2026-07-01 22:28:25", "2026-07-01 22:28:25",
			128, "2323", "", `{"command":"REALTIME"}`,
			"OK", "", "8.134.95.166:53702", "GB32960", "LB9A32A21R0LS1707", "LB9A32A21R0LS1707", "", "",
		))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=GB32960&vin=LB9A32A21R0LS1707&includeTotal=true&limit=5", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	for _, want := range []string{`"vin":"LB9A32A21R0LS1707"`, `"message_id_hex":"0x0002"`, `"total":38`} {
		if !strings.Contains(body, want) {
			t.Fatalf("response missing %s: %s", want, body)
		}
	}
	if strings.Contains(body, "fields_json") {
		t.Fatalf("response should not expose duplicate fields_json: %s", body)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerFiltersParsedFields(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, .* AS parsed_fields, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_gb32960_").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-01 22:28:25", "go_frame", "event-2", 2, "2026-07-01 22:28:25", "2026-07-01 22:28:25",
			128, "2323", "", `{"gb32960.vehicle.soc_percent":"85","gb32960.vehicle.speed_kmh":"30","gb32960.vehicle.total_mileage_km":"10000"}`,
			"OK", "", "8.134.95.166:53702", "GB32960", "LB9A32A21R0LS1707", "LB9A32A21R0LS1707", "", "",
		))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=GB32960&vin=LB9A32A21R0LS1707&limit=1&fields=gb32960.vehicle.soc_percent,gb32960.vehicle.speed_kmh", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	for _, want := range []string{`"gb32960.vehicle.soc_percent":"85"`, `"gb32960.vehicle.speed_kmh":"30"`} {
		if !strings.Contains(body, want) {
			t.Fatalf("response missing %s: %s", want, body)
		}
	}
	if strings.Contains(body, "gb32960.vehicle.total_mileage_km") {
		t.Fatalf("response should filter unrequested parsed field: %s", body)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerAcceptsPostQueryBody(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, .* AS parsed_fields, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_gb32960_").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-01 22:28:25", "go_frame", "event-2", 2, "2026-07-01 22:28:25", "2026-07-01 22:28:25",
			128, "2323", "", `{"gb32960.vehicle.soc_percent":"85","gb32960.vehicle.speed_kmh":"30","gb32960.vehicle.total_mileage_km":"10000"}`,
			"OK", "", "8.134.95.166:53702", "GB32960", "LB9A32A21R0LS1707", "LB9A32A21R0LS1707", "", "",
		))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	body := []byte(`{"protocol":"GB32960","vin":"LB9A32A21R0LS1707","limit":1,"fields":["gb32960.vehicle.soc_percent","gb32960.vehicle.speed_kmh"]}`)
	request := httptest.NewRequest(http.MethodPost, "/api/history/raw-frames/query", bytes.NewReader(body))
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	text := response.Body.String()
	for _, want := range []string{`"gb32960.vehicle.soc_percent":"85"`, `"gb32960.vehicle.speed_kmh":"30"`} {
		if !strings.Contains(text, want) {
			t.Fatalf("response missing %s: %s", want, text)
		}
	}
	if strings.Contains(text, "gb32960.vehicle.total_mileage_km") {
		t.Fatalf("response should filter unrequested parsed field: %s", text)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerFiltersByVehicleKey(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM lingniu_vehicle_ts.raw_jt808_").
		WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(11))
	mock.ExpectQuery("vehicle_key = 'JT808:013307811350'").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-02 00:18:22", "go_frame", "event-3", 0x0200, "2026-07-02 00:18:22", "2026-07-02 00:22:43",
			63, "7E0200", "", `{"header":{"message_id":"0x0200"}}`,
			"OK", "", "115.231.168.135:22170", "JT808", "JT808:013307811350", "", "013307811350", "",
		))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?vehicleKey=JT808:013307811350&protocol=JT808&includeTotal=true&limit=1", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	for _, want := range []string{`"vehicle_key":"JT808:013307811350"`, `"phone":"013307811350"`, `"total":11`} {
		if !strings.Contains(body, want) {
			t.Fatalf("response missing %s: %s", want, body)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerCanSkipTotalCountForFreshnessProbe(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("ORDER BY received_at DESC LIMIT 1 OFFSET 0").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-02 01:26:48", "go_frame", "event-4", 0x0200, "2026-07-02 01:20:46", "2026-07-02 01:26:48",
			63, "7E0200", "", `{"header":{"message_id":"0x0200"}}`,
			"OK", "", "115.231.168.135:22170", "JT808", "JT808:013307811254", "", "013307811254", "",
		))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=JT808&orderBy=receivedAt&includeTotal=false&limit=1", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	for _, want := range []string{`"total":1`, `"received_at":"2026-07-02 01:26:48"`} {
		if !strings.Contains(body, want) {
			t.Fatalf("response missing %s: %s", want, body)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerSkipsTotalCountByDefault(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("ORDER BY ts DESC LIMIT 1 OFFSET 0").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}).AddRow(
			"2026-07-02 01:26:48", "go_frame", "event-5", 0x0200, "2026-07-02 01:20:46", "2026-07-02 01:26:48",
			63, "7E0200", "", `{"header":{"message_id":"0x0200"}}`,
			"OK", "", "115.231.168.135:22170", "JT808", "JT808:13307811254", "", "13307811254", "",
		))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=JT808&limit=1", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	if !strings.Contains(body, `"total":1`) {
		t.Fatalf("response should use page size as total when total count is not requested: %s", body)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, .* AS parsed_fields, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_frames").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes",
			"raw_hex", "raw_text", "parsed_fields", "parse_status", "parse_error", "source_endpoint",
			"protocol", "vehicle_key", "vin", "phone", "device_id",
		}))

	handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=NOT_EXISTS&limit=1", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	if !strings.Contains(body, `"items":[]`) || strings.Contains(body, `"items":null`) {
		t.Fatalf("expected empty items array, got: %s", body)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestHistoryQueryPackageDoesNotKeepDuplicateMileagePointConcept(t *testing.T) {
	source, err := os.ReadFile("query.go")
	if err != nil {
		t.Fatalf("read query.go: %v", err)
	}
	if strings.Contains(string(source), "MileagePoint") || strings.Contains(string(source), "mileage-points") {
		t.Fatalf("history query should expose mileage through locations only")
	}
}

func TestLocationHandlerReturnsLocationsByVIN(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM lingniu_vehicle_ts.vehicle_locations").
		WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(17))
	mock.ExpectQuery("vin = 'LKLG7C4E3NA774736'").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "event_id", "received_at", "longitude", "latitude", "altitude_m", "speed_kmh",
			"soc_percent", "direction_deg", "alarm_flag", "status_flag", "total_mileage_km", "protocol", "vin",
		}).AddRow(
			"2026-07-02 00:18:22", "event-3", "2026-07-02 00:22:43",
			121.07764, 30.585928, 11.0, 8.0, 82.5, 171, 0, 4718595, 8792.8,
			"JT808", "LKLG7C4E3NA774736",
		))

	handler := NewLocationHandler(NewLocationRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/locations?vin=LKLG7C4E3NA774736&protocol=JT808&includeTotal=true&limit=1", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	for _, want := range []string{`"vin":"LKLG7C4E3NA774736"`, `"longitude":121.07764`, `"soc_percent":82.5`, `"total_mileage_km":8792.8`, `"total":17`} {
		if !strings.Contains(body, want) {
			t.Fatalf("response missing %s: %s", want, body)
		}
	}
	for _, legacy := range []string{"frame_id", "vehicle_key", "phone", "device_id"} {
		if strings.Contains(body, legacy) {
			t.Fatalf("location response should not expose %s: %s", legacy, body)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestLocationHandlerSkipsTotalCountByDefault(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("vin = 'LKLG7C4E3NA774736'").
		WillReturnRows(sqlmock.NewRows([]string{
			"ts", "event_id", "received_at", "longitude", "latitude", "altitude_m", "speed_kmh",
			"soc_percent", "direction_deg", "alarm_flag", "status_flag", "total_mileage_km", "protocol", "vin",
		}).AddRow(
			"2026-07-02 00:18:22", "event-3", "2026-07-02 00:22:43",
			121.07764, 30.585928, 11.0, 8.0, 82.5, 171, 0, 4718595, 8792.8,
			"JT808", "LKLG7C4E3NA774736",
		))

	handler := NewLocationHandler(NewLocationRepository(db, "lingniu_vehicle_ts"))
	request := httptest.NewRequest(http.MethodGet, "/api/history/locations?vin=LKLG7C4E3NA774736&protocol=JT808&limit=1", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	if !strings.Contains(body, `"total":1`) {
		t.Fatalf("response should use page size as total when total count is not requested: %s", body)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestRawFrameHandlerRejectsInvalidLimit(t *testing.T) {
	handler := NewRawFrameHandler(NewRawFrameRepository(&sql.DB{}, ""))
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?limit=501", nil)
	response := httptest.NewRecorder()

	handler.ServeHTTP(response, request)

	if response.Code != http.StatusBadRequest {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
}

func TestParseRawFrameQueryAcceptsDatetimeLocalValues(t *testing.T) {
	request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?dateFrom=2026-07-01T00:00:00&dateTo=2026-07-02T00:00:00", nil)

	query, err := parseRawFrameQuery(request)
	if err != nil {
		t.Fatalf("parseRawFrameQuery() error = %v", err)
	}
	if query.DateFrom != "2026-07-01 00:00:00" || query.DateTo != "2026-07-02 00:00:00" {
		t.Fatalf("date range = %q -> %q", query.DateFrom, query.DateTo)
	}
}

func TestNormalizeDateTimeLiteralUsesAsiaShanghaiQueryTime(t *testing.T) {
	for raw, want := range map[string]string{
		"2026-07-01T00:00:00":       "2026-07-01 00:00:00",
		"2026-07-01 00:00:00":       "2026-07-01 00:00:00",
		"2026-07-01T00:00:00+08:00": "2026-07-01 00:00:00",
		"2026-06-30T16:00:00Z":      "2026-07-01 00:00:00",
	} {
		if got := normalizeDateTimeLiteral(raw); got != want {
			t.Fatalf("normalizeDateTimeLiteral(%q) = %q, want %q", raw, got, want)
		}
	}
}

func TestParseMessageIDSupportsDecimalAndHex(t *testing.T) {
	for raw, want := range map[string]int64{
		"512":    512,
		"0x0200": 512,
		"0X0100": 256,
	} {
		got, ok := parseMessageID(raw)
		if !ok || got != want {
			t.Fatalf("parseMessageID(%q) = %d,%v want %d,true", raw, got, ok, want)
		}
	}
}

func TestBuildLocationSQLUsesLiteralsForTDengine(t *testing.T) {
	sqlText, args := buildLocationSQL("lingniu_vehicle_ts.vehicle_locations", LocationQuery{
		Protocol: "JT808",
		VIN:      "LKLG7C4E3NA774736",
		DateFrom: "2026-07-02 00:00:00",
		DateTo:   "2026-07-02 23:59:59",
		Limit:    20,
		Offset:   5,
	})
	if len(args) != 0 {
		t.Fatalf("expected no query args for TDengine, got %#v", args)
	}
	for _, want := range []string{
		"protocol = 'JT808'",
		"vin = 'LKLG7C4E3NA774736'",
		"ts >= '2026-07-02 00:00:00'",
		"LIMIT 20 OFFSET 5",
	} {
		if !strings.Contains(sqlText, want) {
			t.Fatalf("sql missing %s: %s", want, sqlText)
		}
	}
	if strings.Contains(sqlText, "vehicle_key") || strings.Contains(sqlText, "phone") || strings.Contains(sqlText, "device_id") {
		t.Fatalf("location sql should use vin-only identity filters: %s", sqlText)
	}
	if strings.Contains(sqlText, "frame_id") {
		t.Fatalf("location sql should not select duplicate raw frame id: %s", sqlText)
	}
}

func TestBuildRawFrameSQLUsesLiteralsForTDengine(t *testing.T) {
	sqlText, args := buildRawFrameSQL("lingniu_vehicle_ts.raw_frames", RawFrameQuery{
		Protocol:   "JT808",
		VehicleKey: "JT808:013307811350",
		VIN:        "VIN'1",
		MessageID:  "0x0200",
		DateFrom:   "2026-07-01T00:00:00",
		DateTo:     "2026-07-01T23:59:59",
		Limit:      20,
		Offset:     5,
	})
	if len(args) != 0 {
		t.Fatalf("expected no query args for TDengine, got %#v", args)
	}
	for _, want := range []string{
		"protocol = 'JT808'",
		"vehicle_key = 'JT808:013307811350'",
		"vin = 'VIN''1'",
		"message_id = 512",
		"ts >= '2026-07-01 00:00:00'",
		"ts <= '2026-07-01 23:59:59'",
		"LIMIT 20 OFFSET 5",
	} {
		if !strings.Contains(sqlText, want) {
			t.Fatalf("sql missing %s: %s", want, sqlText)
		}
	}
}

func TestBuildRawFrameSQLCanOrderByReceivedAt(t *testing.T) {
	sqlText, args := buildRawFrameSQL("lingniu_vehicle_ts.raw_frames", RawFrameQuery{
		Protocol: "JT808",
		OrderBy:  "receivedAt",
		Limit:    1,
	})
	if len(args) != 0 {
		t.Fatalf("expected no query args for TDengine, got %#v", args)
	}
	if !strings.Contains(sqlText, "ORDER BY received_at DESC LIMIT 1 OFFSET 0") {
		t.Fatalf("sql should order by received_at: %s", sqlText)
	}
}
