package identity

import (
	"context"
	"regexp"
	"testing"
	"time"

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

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

func TestJT808RegistrationProjectorProjectsRegistrationUsingReceiveTime(t *testing.T) {
	loc := time.FixedZone("Asia/Shanghai", 8*60*60)
	projector := NewJT808RegistrationProjector(loc, 10*time.Minute)
	receivedAt := time.Date(2026, 7, 13, 17, 20, 30, 987000000, loc)
	env := envelope.FrameEnvelope{
		Protocol:       envelope.ProtocolJT808,
		MessageID:      JT808RegisterMessageID,
		Phone:          "0013307795425",
		VIN:            "LTESTVIN000000001",
		DeviceID:       "DEV-1",
		Plate:          "粤A00001",
		SourceEndpoint: "115.231.168.135:43625",
		ReceivedAtMS:   receivedAt.UnixMilli(),
		EventTimeMS:    receivedAt.Add(24 * time.Hour).UnixMilli(),
		ParseStatus:    envelope.ParseOK,
		ParsedFields: map[string]any{
			"jt808.registration.province":     "44",
			"jt808.registration.city":         "1",
			"jt808.registration.manufacturer": "YUTNG",
			"jt808.registration.device_type":  "TBOX-1",
			"jt808.registration.plate_color":  "2",
		},
	}

	facts := projector.ProjectBatch([]envelope.FrameEnvelope{env})
	if len(facts) != 1 {
		t.Fatalf("facts = %d, want 1", len(facts))
	}
	fact := facts[0]
	if fact.Phone != "13307795425" || fact.VIN != env.VIN || fact.Plate != env.Plate {
		t.Fatalf("identity fact = %+v", fact)
	}
	wantTime := receivedAt.Truncate(time.Second)
	if !fact.SeenAt.Equal(wantTime) || fact.FirstRegisteredAt == nil || !fact.FirstRegisteredAt.Equal(wantTime) {
		t.Fatalf("fact times = seen %s first %#v, want %s", fact.SeenAt, fact.FirstRegisteredAt, wantTime)
	}
	if fact.SourceIP != "115.231.168.135" || fact.Manufacturer != "YUTNG" || fact.PlateColor != "2" {
		t.Fatalf("registration details = %+v", fact)
	}
}

func TestJT808RegistrationProjectorThrottlesLocationOnlyAfterPersist(t *testing.T) {
	loc := time.UTC
	projector := NewJT808RegistrationProjector(loc, 10*time.Minute)
	base := time.Date(2026, 7, 13, 8, 0, 0, 0, loc)
	location := func(at time.Time) envelope.FrameEnvelope {
		return envelope.FrameEnvelope{
			Protocol:     envelope.ProtocolJT808,
			MessageID:    JT808LocationMessageID,
			Phone:        "013307795425",
			VIN:          "LTESTVIN000000001",
			ReceivedAtMS: at.UnixMilli(),
			ParseStatus:  envelope.ParseOK,
		}
	}

	first := projector.ProjectBatch([]envelope.FrameEnvelope{location(base)})
	if len(first) != 1 {
		t.Fatalf("first facts = %d, want 1", len(first))
	}
	// A failed database attempt must remain immediately replayable.
	if replay := projector.ProjectBatch([]envelope.FrameEnvelope{location(base)}); len(replay) != 1 {
		t.Fatalf("uncommitted replay facts = %d, want 1", len(replay))
	}
	projector.MarkPersisted(first)
	if throttled := projector.ProjectBatch([]envelope.FrameEnvelope{location(base.Add(9 * time.Minute))}); len(throttled) != 0 {
		t.Fatalf("throttled facts = %d, want 0", len(throttled))
	}
	if due := projector.ProjectBatch([]envelope.FrameEnvelope{location(base.Add(10 * time.Minute))}); len(due) != 1 {
		t.Fatalf("due facts = %d, want 1", len(due))
	}
}

func TestJT808RegistrationProjectorMergesRegisterAndAuthForPhone(t *testing.T) {
	projector := NewJT808RegistrationProjector(time.UTC, 10*time.Minute)
	base := time.Date(2026, 7, 13, 8, 0, 0, 0, time.UTC)
	register := envelope.FrameEnvelope{
		Protocol:     envelope.ProtocolJT808,
		MessageID:    JT808RegisterMessageID,
		Phone:        "013307795425",
		VIN:          "LTESTVIN000000001",
		Plate:        "粤A00001",
		ReceivedAtMS: base.UnixMilli(),
		ParseStatus:  envelope.ParseOK,
	}
	auth := envelope.FrameEnvelope{
		Protocol:     envelope.ProtocolJT808,
		MessageID:    JT808AuthMessageID,
		Phone:        "13307795425",
		ReceivedAtMS: base.Add(time.Second).UnixMilli(),
		ParseStatus:  envelope.ParseOK,
		ParsedFields: map[string]any{
			"jt808.authentication.token": "g7gps",
			"jt808.authentication.imei":  "123456789012345",
		},
	}

	facts := projector.ProjectBatch([]envelope.FrameEnvelope{register, auth})
	if len(facts) != 1 {
		t.Fatalf("facts = %d, want 1", len(facts))
	}
	fact := facts[0]
	if fact.VIN != register.VIN || fact.Plate != register.Plate || fact.AuthToken != "g7gps" {
		t.Fatalf("merged fact = %+v", fact)
	}
	if fact.FirstRegisteredAt == nil || fact.LatestRegisteredAt == nil || fact.LatestAuthenticated == nil {
		t.Fatalf("merged timestamps missing: %+v", fact)
	}
}

func TestJT808RegistrationProjectorDoesNotTrustEnforcedRejectedAuth(t *testing.T) {
	projector := NewJT808RegistrationProjector(time.UTC, 10*time.Minute)
	seenAt := time.Date(2026, 7, 13, 8, 0, 0, 0, time.UTC)
	facts := projector.ProjectBatch([]envelope.FrameEnvelope{{
		Protocol:               envelope.ProtocolJT808,
		MessageID:              JT808AuthMessageID,
		Phone:                  "13307795425",
		ReceivedAtMS:           seenAt.UnixMilli(),
		ParseStatus:            envelope.ParseOK,
		AuthenticationEnforced: true,
		AuthenticationStatus:   "rejected",
		Parsed: map[string]any{
			"authentication": map[string]any{
				"token":            "untrusted-token",
				"imei":             "untrusted-imei",
				"software_version": "untrusted-version",
			},
		},
	}})
	if len(facts) != 1 {
		t.Fatalf("facts = %d, want 1 audit touch", len(facts))
	}
	fact := facts[0]
	if fact.AuthToken != "" || fact.AuthIMEI != "" || fact.AuthSoftwareVersion != "" || fact.LatestAuthenticated != nil {
		t.Fatalf("rejected credential leaked into registration fact: %+v", fact)
	}
}

func TestJT808RegistrationStoreUsesIdempotentEventTimeUpsert(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewJT808RegistrationStore(db)
	seenAt := time.Date(2026, 7, 13, 8, 0, 0, 0, time.UTC)
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO jt808_registration")).
		WithArgs(
			"13307795425", "DEV-1", "粤A00001", "LTESTVIN000000001", "", "", "YUTNG", "", "",
			"g7gps", "", "", "115.231.168.135:43625", "115.231.168.135",
			sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), seenAt,
		).
		WillReturnResult(sqlmock.NewResult(1, 1))
	err = store.UpsertBatch(context.Background(), []JT808RegistrationFact{{
		Phone:               "013307795425",
		DeviceID:            "DEV-1",
		Plate:               "粤A00001",
		VIN:                 "LTESTVIN000000001",
		Manufacturer:        "YUTNG",
		AuthToken:           "g7gps",
		SourceEndpoint:      "115.231.168.135:43625",
		SourceIP:            "115.231.168.135",
		FirstRegisteredAt:   timePointer(seenAt),
		LatestRegisteredAt:  timePointer(seenAt),
		LatestAuthenticated: timePointer(seenAt),
		SeenAt:              seenAt,
	}})
	if err != nil {
		t.Fatalf("UpsertBatch() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestRegistrationUpsertProtectsLatestValuesFromOldReplay(t *testing.T) {
	var query string
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(_ string, actual string) error {
		query = actual
		return nil
	})))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewJT808RegistrationStore(db)
	mock.ExpectExec("INSERT INTO jt808_registration").WillReturnResult(sqlmock.NewResult(1, 1))
	if err := store.UpsertBatch(context.Background(), []JT808RegistrationFact{{
		Phone:  "13307795425",
		VIN:    "unknown",
		SeenAt: time.Date(2026, 7, 13, 8, 0, 0, 0, time.UTC),
	}}); err != nil {
		t.Fatal(err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
	for _, required := range []string{
		"VALUES(latest_seen_at) >= latest_seen_at",
		"LEAST(first_registered_at, VALUES(first_registered_at))",
		"GREATEST(latest_authenticated_at, VALUES(latest_authenticated_at))",
		"latest_seen_at = GREATEST(latest_seen_at, VALUES(latest_seen_at))",
	} {
		if !regexp.MustCompile(regexp.QuoteMeta(required)).MatchString(query) {
			t.Fatalf("upsert SQL missing %q", required)
		}
	}
}
