package identity

import (
	"context"
	"errors"
	"testing"
	"time"

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

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

func TestRefreshSnapshotResolvesKnownJT808WithoutPerFrameQueries(t *testing.T) {
	db, mock := newMockDB(t)
	defer db.Close()
	expectIdentitySnapshot(mock)

	resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{
		SnapshotOnlyLookups: true,
	})
	result, err := resolver.RefreshSnapshot(context.Background())
	if err != nil {
		t.Fatalf("RefreshSnapshot() error = %v", err)
	}
	if result.BindingEntries != 3 || result.IdentifierEntries != 2 || result.RegistrationEntries != 1 || result.SourceEntries != 1 {
		t.Fatalf("snapshot result = %+v", result)
	}

	resolved, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{
		Protocol:       envelope.ProtocolJT808,
		Phone:          "013307795425",
		SourceEndpoint: "115.231.168.135:43625",
		Parsed:         map[string]any{},
	})
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if resolved.VIN != "LNBVIN00000000001" {
		t.Fatalf("vin = %q", resolved.VIN)
	}
	if resolved.SourceCode != "g7s" || resolved.PlatformName != "G7s" || resolved.SourceKind != "PLATFORM" {
		t.Fatalf("source metadata = code:%q platform:%q kind:%q", resolved.SourceCode, resolved.PlatformName, resolved.SourceKind)
	}
	identityMetadata, _ := resolved.Parsed["identity"].(map[string]any)
	if identityMetadata["cache_status"] != "snapshot" {
		t.Fatalf("identity metadata = %#v, want snapshot cache status", identityMetadata)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestSnapshotOnlyResolverMissDoesNotQueryMySQL(t *testing.T) {
	db, mock := newMockDB(t)
	defer db.Close()

	resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{
		SnapshotOnlyLookups: true,
	})
	resolved, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{
		Protocol: envelope.ProtocolJT808,
		Phone:    "013307700000",
		Parsed:   map[string]any{},
	})
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if resolved.VIN != "" {
		t.Fatalf("vin = %q, want unresolved", resolved.VIN)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("snapshot-only miss should not query mysql: %v", err)
	}
}

func TestRefreshSnapshotFailureKeepsLastKnownGoodSnapshot(t *testing.T) {
	db, mock := newMockDB(t)
	defer db.Close()
	expectIdentitySnapshot(mock)

	resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{
		SnapshotOnlyLookups: true,
	})
	first, err := resolver.RefreshSnapshot(context.Background())
	if err != nil {
		t.Fatalf("first RefreshSnapshot() error = %v", err)
	}
	mock.ExpectQuery("SELECT vin, plate, phone").
		WillReturnError(errors.New("mysql unavailable"))
	if _, err := resolver.RefreshSnapshot(context.Background()); err == nil {
		t.Fatal("second RefreshSnapshot() error = nil, want failure")
	}

	resolved, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{
		Protocol: envelope.ProtocolJT808,
		Phone:    "013307795425",
		Parsed:   map[string]any{},
	})
	if err != nil {
		t.Fatalf("Resolve() after failed refresh error = %v", err)
	}
	if resolved.VIN != "LNBVIN00000000001" {
		t.Fatalf("vin after failed refresh = %q", resolved.VIN)
	}
	stats := resolver.CacheStats()
	if !stats.SnapshotReady || stats.SnapshotRefreshedAt.IsZero() || !stats.SnapshotRefreshedAt.Equal(first.RefreshedAt) {
		t.Fatalf("snapshot stats after failed refresh = %+v, first = %+v", stats, first)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func expectIdentitySnapshot(mock sqlmock.Sqlmock) {
	mock.ExpectQuery("SELECT vin, plate, phone").
		WillReturnRows(sqlmock.NewRows([]string{"vin", "plate", "phone"}).
			AddRow("LNBVIN00000000001", "粤A00001", "13307795425"))
	mock.ExpectQuery("SELECT protocol, source_code, identifier_type, identifier_value").
		WillReturnRows(sqlmock.NewRows([]string{"protocol", "source_code", "identifier_type", "identifier_value", "vin", "platform_name"}).
			AddRow("JT808", "g7s", "JT808_PHONE", "13307795425", "LNBVIN00000000001", "G7s"))
	mock.ExpectQuery("SELECT phone, vin, device_id, plate, auth_token").
		WillReturnRows(sqlmock.NewRows([]string{"phone", "vin", "device_id", "plate", "auth_token"}).
			AddRow("13307795425", "LNBVIN00000000001", "DEVICE-1", "粤A00001", "device-code"))
	mock.ExpectQuery("SELECT protocol, source_ip, source_code, platform_name, source_kind").
		WillReturnRows(sqlmock.NewRows([]string{"protocol", "source_ip", "source_code", "platform_name", "source_kind"}).
			AddRow("JT808", "115.231.168.135", "g7s", "G7s", "PLATFORM"))
}

func TestSnapshotServesJT808AuthenticationTokenByNormalizedPhone(t *testing.T) {
	db, mock := newMockDB(t)
	defer db.Close()
	expectIdentitySnapshot(mock)

	resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{
		SnapshotOnlyLookups: true,
	})
	if _, err := resolver.RefreshSnapshot(context.Background()); err != nil {
		t.Fatalf("RefreshSnapshot() error = %v", err)
	}
	token, ok := resolver.JT808AuthToken("0013307795425")
	if !ok || token != "device-code" {
		t.Fatalf("JT808AuthToken() = %q, %v", token, ok)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestSnapshotResultRefreshedAtUsesCurrentTime(t *testing.T) {
	db, mock := newMockDB(t)
	defer db.Close()
	expectIdentitySnapshot(mock)

	resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{
		SnapshotOnlyLookups: true,
	})
	before := time.Now()
	result, err := resolver.RefreshSnapshot(context.Background())
	if err != nil {
		t.Fatalf("RefreshSnapshot() error = %v", err)
	}
	if result.RefreshedAt.Before(before) || result.RefreshedAt.After(time.Now()) {
		t.Fatalf("refreshed_at = %v, want current time", result.RefreshedAt)
	}
}
