package platform

import (
	"encoding/json"
	"errors"
	"reflect"
	"regexp"
	"testing"
	"time"

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

func TestDecodeAlertStreamRecordValidatesContractAndLateness(t *testing.T) {
	payload := []byte(`{"event_id":"evt:fields","event_kind":"FIELDS","source_event_id":"evt","field_mapping":"2026-07-03.v1","protocol":"JT808","vin":"VIN1","plate":"川A10001","event_time_ms":1783980000000,"received_at_ms":1783980181000,"fields":{"jt808.location.speed_kmh":88}}`)
	record := DecodeAlertStreamRecord("vehicle.fields.go.jt808.v1", 2, 10, 20, payload, 2*time.Minute)
	if !record.Valid || !record.Late || record.ErrorCode != "" || record.SourceEventID != "evt" || record.Protocol != "JT808" {
		t.Fatalf("unexpected valid record: %+v", record)
	}
	if delay := record.ReceivedAt.Sub(record.EventAt); delay != 181*time.Second {
		t.Fatalf("event/receipt delay=%s", delay)
	}
	if value, ok := alertStreamRawNumber(record.Fields["jt808.location.speed_kmh"]); !ok || value != 88 {
		t.Fatalf("decoded canonical fields were not retained: value=%v ok=%v", value, ok)
	}

	badTopic := DecodeAlertStreamRecord("vehicle.fields.go.gb32960.v1", 0, 1, 2, payload, time.Minute)
	if badTopic.Valid || badTopic.ErrorCode != "protocol_topic_mismatch" {
		t.Fatalf("topic/protocol mismatch accepted: %+v", badTopic)
	}
	badField := []byte(`{"event_id":"evt:fields","event_kind":"FIELDS","source_event_id":"evt","field_mapping":"v1","protocol":"JT808","vin":"VIN1","event_time_ms":1783980000000,"received_at_ms":1783980001000,"fields":{"speed_kmh":88}}`)
	record = DecodeAlertStreamRecord("vehicle.fields.go.jt808.v1", 0, 1, 2, badField, time.Minute)
	if record.Valid || record.ErrorCode != "invalid_field_name" {
		t.Fatalf("unscoped field accepted: %+v", record)
	}
}

func TestAlertStreamMetricValueUsesCatalogMappingAndStrictMissingSemantics(t *testing.T) {
	record := AlertStreamRecord{
		Protocol: "GB32960", EventAt: time.Unix(100, 0), ReceivedAt: time.Unix(130, 0),
		Fields: map[string]json.RawMessage{
			"gb32960.vehicle.speed_kmh":        json.RawMessage(`"82.5"`),
			"gb32960.alarm.general_alarm_flag": json.RawMessage(`"0x00000004"`),
		},
	}
	mappings := alertStreamMetricMapping{
		"speed_kmh":    {"GB32960": "gb32960.vehicle.speed_kmh"},
		"soc_percent":  {"GB32960": "gb32960.vehicle.soc_percent"},
		"alarm_active": {"GB32960": "gb32960.alarm.general_alarm_flag"},
	}
	if value, ok := alertStreamMetricValue("speed_kmh", record, mappings); !ok || value != 82.5 {
		t.Fatalf("mapped numeric field = %v,%v", value, ok)
	}
	if value, ok := alertStreamMetricValue("alarm_active", record, mappings); !ok || value != 1 {
		t.Fatalf("hex alarm field = %v,%v", value, ok)
	}
	if _, ok := alertStreamMetricValue("soc_percent", record, mappings); ok {
		t.Fatal("missing SOC must not be coerced to zero")
	}
	if value, ok := alertStreamMetricValue("data_delay_sec", record, mappings); !ok || value != 30 {
		t.Fatalf("derived delay = %v,%v", value, ok)
	}
}

func TestAlertStreamStateQueriesAreScopedToBatchRulesVehiclesAndProtocols(t *testing.T) {
	rules := []AlertRule{{ID: "r2"}, {ID: "r1"}, {ID: "r2"}}
	records := []AlertStreamRecord{{VIN: "VIN1", Protocol: "JT808"}, {VIN: "VIN2", Protocol: "GB32960"}, {VIN: "VIN1", Protocol: "JT808"}}
	where, args := alertStreamScopeWhere(rules, records)
	if where != "rule_id IN (?,?) AND vin IN (?,?) AND protocol IN (?,?)" {
		t.Fatalf("unexpected scoped predicate: %s", where)
	}
	want := []any{"r2", "r1", "VIN1", "VIN2", "JT808", "GB32960"}
	if !reflect.DeepEqual(args, want) {
		t.Fatalf("scope args=%#v want=%#v", args, want)
	}
}

func TestActiveAlertStreamCommitsRuleEffectsBeforeCheckpointInSameTransaction(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewProductionStore(db, nil, "").WithAlertStreamConfig("active", "alert-active")
	record := AlertStreamRecord{Topic: "vehicle.fields.go.jt808.v1", Partition: 0, Offset: 7, HighWatermark: 8, Protocol: "JT808", VIN: "VIN1", SourceEventID: "evt-7", EventAt: time.Now(), ReceivedAt: time.Now(), Fields: map[string]json.RawMessage{"jt808.location.speed_kmh": json.RawMessage(`10`)}, Valid: true}
	checkpointQuery := `SELECT next_offset FROM vehicle_alert_stream_checkpoint WHERE consumer_group=? AND topic=? AND partition_id=? FOR UPDATE`
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(checkpointQuery)).WithArgs("alert-active", record.Topic, 0).WillReturnRows(sqlmock.NewRows([]string{"next_offset"}))
	mock.ExpectQuery(regexp.QuoteMeta(alertRuleSelect + `WHERE enabled=1 AND metric<>'freshness_sec' ORDER BY id FOR UPDATE`)).WillReturnRows(sqlmock.NewRows([]string{"id"}))
	mock.ExpectExec(`INSERT INTO vehicle_alert_stream_checkpoint`).WithArgs("alert-active", record.Topic, 0, int64(8), int64(8), int64(1), int64(1), int64(0), int64(0), int64(0), record.EventAt, record.ReceivedAt, "evt-7", "", "").WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()
	result, err := store.RecordAlertStreamBatch(t.Context(), "alert-active", []AlertStreamRecord{record})
	if err != nil || result.Processed != 1 {
		t.Fatalf("active batch failed: result=%+v err=%v", result, err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestActiveAlertStreamRollsBackCheckpointWhenRuleEvaluationFails(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewProductionStore(db, nil, "").WithAlertStreamConfig("active", "alert-active")
	record := AlertStreamRecord{Topic: "vehicle.fields.go.jt808.v1", Partition: 0, Offset: 9, HighWatermark: 10, Protocol: "JT808", VIN: "VIN1", SourceEventID: "evt-9", EventAt: time.Now(), ReceivedAt: time.Now(), Fields: map[string]json.RawMessage{"jt808.location.speed_kmh": json.RawMessage(`10`)}, Valid: true}
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT next_offset FROM vehicle_alert_stream_checkpoint WHERE consumer_group=? AND topic=? AND partition_id=? FOR UPDATE`)).WithArgs("alert-active", record.Topic, 0).WillReturnRows(sqlmock.NewRows([]string{"next_offset"}))
	mock.ExpectQuery(regexp.QuoteMeta(alertRuleSelect + `WHERE enabled=1 AND metric<>'freshness_sec' ORDER BY id FOR UPDATE`)).WillReturnError(errors.New("rule read failed"))
	mock.ExpectRollback()
	if _, err := store.RecordAlertStreamBatch(t.Context(), "alert-active", []AlertStreamRecord{record}); err == nil {
		t.Fatal("rule evaluation failure must abort checkpoint transaction")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestDecodeAlertStreamRecordNormalizesMissingAndFutureEventTime(t *testing.T) {
	for _, eventMS := range []int64{0, 1783981000001} {
		payload := []byte(`{"event_id":"evt:fields","event_kind":"FIELDS","source_event_id":"evt","field_mapping":"v1","protocol":"JT808","vin":"VIN1","event_time_ms":` + itoa64(eventMS) + `,"received_at_ms":1783980000000,"fields":{"jt808.location.speed_kmh":1}}`)
		record := DecodeAlertStreamRecord("vehicle.fields.go.jt808.v1", 0, 1, 2, payload, time.Minute)
		if !record.Valid || !record.EventAt.Equal(record.ReceivedAt) || record.Late {
			t.Fatalf("event time was not normalized: %+v", record)
		}
	}
}

func TestAlertStreamCheckpointMakesDatabaseOffsetAuthoritative(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewProductionStore(db, nil, "")
	eventAt := time.Date(2026, 7, 14, 7, 0, 0, 123000000, time.Local)
	receivedAt := eventAt.Add(3 * time.Minute)
	records := []AlertStreamRecord{
		{Topic: "vehicle.fields.go.jt808.v1", Partition: 0, Offset: 10, HighWatermark: 20, Valid: true, Late: true, EventAt: eventAt, ReceivedAt: receivedAt, SourceEventID: "evt-10"},
		{Topic: "vehicle.fields.go.jt808.v1", Partition: 0, Offset: 11, HighWatermark: 20, Valid: false, ErrorCode: "invalid_json"},
	}
	checkpointQuery := `SELECT next_offset FROM vehicle_alert_stream_checkpoint WHERE consumer_group=? AND topic=? AND partition_id=? FOR UPDATE`
	checkpointInsert := `INSERT INTO vehicle_alert_stream_checkpoint`
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(checkpointQuery)).WithArgs("alert-shadow", "vehicle.fields.go.jt808.v1", 0).WillReturnRows(sqlmock.NewRows([]string{"next_offset"}))
	mock.ExpectExec(checkpointInsert).WithArgs("alert-shadow", "vehicle.fields.go.jt808.v1", 0, int64(12), int64(20), int64(2), int64(1), int64(1), int64(1), int64(0), eventAt, receivedAt, "evt-10", "invalid_json", "invalid_json").WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()
	result, err := store.RecordAlertStreamBatch(t.Context(), "alert-shadow", records)
	if err != nil {
		t.Fatal(err)
	}
	if result.Processed != 2 || result.Valid != 1 || result.Invalid != 1 || result.Late != 1 || result.ReplaySkipped != 0 || result.Partitions != 1 {
		t.Fatalf("unexpected first checkpoint result: %+v", result)
	}

	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta(checkpointQuery)).WithArgs("alert-shadow", "vehicle.fields.go.jt808.v1", 0).WillReturnRows(sqlmock.NewRows([]string{"next_offset"}).AddRow(12))
	mock.ExpectExec(checkpointInsert).WithArgs("alert-shadow", "vehicle.fields.go.jt808.v1", 0, int64(12), int64(20), int64(0), int64(0), int64(0), int64(0), int64(2), nil, nil, "", "", "").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()
	replayed, err := store.RecordAlertStreamBatch(t.Context(), "alert-shadow", records)
	if err != nil {
		t.Fatal(err)
	}
	if replayed.Processed != 0 || replayed.ReplaySkipped != 2 {
		t.Fatalf("database checkpoint did not suppress replay: %+v", replayed)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestAlertStreamCheckpointHealthExposesLagAndQuality(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewProductionStore(db, nil, "").WithAlertStreamConfig("shadow", "alert-shadow")
	query := `SELECT COUNT(*),COALESCE(SUM(GREATEST(high_watermark-next_offset,0)),0),COALESCE(SUM(processed_count),0),COALESCE(SUM(valid_count),0),COALESCE(SUM(invalid_count),0),COALESCE(SUM(late_count),0),COALESCE(SUM(replay_skipped_count),0),MAX(updated_at),COALESCE((SELECT c2.last_invalid_code FROM vehicle_alert_stream_checkpoint c2 WHERE c2.consumer_group=? AND c2.last_invalid_at IS NOT NULL ORDER BY c2.last_invalid_at DESC LIMIT 1),''),MAX(last_invalid_at) FROM vehicle_alert_stream_checkpoint WHERE consumer_group=?`
	now := time.Now()
	mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs("alert-shadow", "alert-shadow").WillReturnRows(sqlmock.NewRows([]string{"partitions", "lag", "processed", "valid", "invalid", "late", "replay", "updated_at", "last_invalid_code", "last_invalid_at"}).AddRow(9, 3, 1000, 999, 1, 4, 2, now, "missing_vin_jt808", now))
	health, status := store.alertStreamCheckpointHealth(t.Context())
	if health.Status != "warning" || status.Mode != "shadow" || status.Partitions != 9 || status.Lag != 3 || status.Processed != 1000 || status.Invalid != 1 || status.Late != 4 || status.ReplaySkipped != 2 || status.UpdatedAt == "" || status.LastInvalidCode != "missing_vin_jt808" || status.LastInvalidAt == "" {
		t.Fatalf("checkpoint health lost evidence: health=%+v status=%+v", health, status)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func itoa64(value int64) string {
	if value == 0 {
		return "0"
	}
	negative := value < 0
	if negative {
		value = -value
	}
	digits := make([]byte, 0, 20)
	for value > 0 {
		digits = append([]byte{byte('0' + value%10)}, digits...)
		value /= 10
	}
	if negative {
		digits = append([]byte{'-'}, digits...)
	}
	return string(digits)
}
