package platform

import (
	"context"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

type MockStore struct {
	vehicles           []VehicleRow
	locations          []RealtimeLocationRow
	accessMu           sync.RWMutex
	accessThresholds   AccessThresholdConfig
	sourcePolicyMu     sync.RWMutex
	sourcePolicies     map[string]VehicleSourcePolicyConfig
	profileMu          sync.RWMutex
	profiles           map[string]VehicleProfile
	alertMu            sync.RWMutex
	alertRules         []AlertRule
	alertEvents        []AlertEvent
	alertNotifications []AlertNotification
	nextAlertActionID  int64
	nextNotificationID int64
}

func NewMockStore() *MockStore {
	vehicles := []VehicleRow{
		{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Phone: "13307795425", OEM: "G7s", Protocol: "JT808", Online: true, LastSeen: "2026-07-03 20:12:10", LocationText: "广东省广州市", BindingScore: 96},
		{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Phone: "13307795425", OEM: "G7s", Protocol: "GB32960", Online: false, LastSeen: "2026-07-03 20:10:10", LocationText: "广东省广州市", BindingScore: 96},
		{VIN: "LNXNEGRR7SR318212", Plate: "川AHTWO1", Phone: "", OEM: "Hyundai", Protocol: "GB32960", Online: true, LastSeen: "2026-07-03 20:12:06", LocationText: "四川省成都市", BindingScore: 100},
		{VIN: "LMRKH9AC2R1004087", Plate: "豫A88888", Phone: "", OEM: "宇通", Protocol: "YUTONG_MQTT", Online: true, LastSeen: "2026-07-03 20:11:59", LocationText: "上海市临港", BindingScore: 92},
		{VIN: "LB9A32A24P0LS1230", Plate: "粤AFF7936", Phone: "13307795426", OEM: "广安车联", Protocol: "JT808", Online: false, LastSeen: "2026-07-03 19:58:00", LocationText: "广东省佛山市", BindingScore: 88},
	}
	store := &MockStore{
		vehicles:         vehicles,
		accessThresholds: defaultAccessThresholds(time.Now()),
		sourcePolicies:   map[string]VehicleSourcePolicyConfig{},
		profiles: map[string]VehicleProfile{
			"LB9A32A24R0LS1426": {VIN: "LB9A32A24R0LS1426", ModelName: "新能源运营车", VehicleType: "乘用车", CompanyName: "岭牛示范车队", OperationStatus: "active", AccessProvider: "G7", FirstAccessAt: "2026-03-01T08:00:00+08:00", RuntimeSeconds: int64Pointer(1263600), SourceSystem: "manual", Version: 1, UpdatedBy: "demo-admin", UpdatedAt: "2026-07-03T20:12:10+08:00"},
		},
		locations: []RealtimeLocationRow{
			{VIN: vehicles[0].VIN, Plate: vehicles[0].Plate, Protocol: vehicles[0].Protocol, Longitude: 113.2644, Latitude: 23.1291, SpeedKmh: 42.5, SOCPercent: 76.2, TotalMileageKm: 48798.9, LastSeen: vehicles[0].LastSeen},
			{VIN: vehicles[1].VIN, Plate: vehicles[1].Plate, Protocol: vehicles[1].Protocol, Longitude: 104.0668, Latitude: 30.5728, SpeedKmh: 18.3, SOCPercent: 64.8, TotalMileageKm: 119925, LastSeen: vehicles[1].LastSeen},
			{VIN: vehicles[2].VIN, Plate: vehicles[2].Plate, Protocol: vehicles[2].Protocol, Longitude: 121.075044, Latitude: 30.590921, SpeedKmh: 27, SOCPercent: 78.4, TotalMileageKm: 119925, LastSeen: vehicles[2].LastSeen},
		},
	}
	store.seedAlertCenter()
	return store
}

func int64Pointer(value int64) *int64 { return &value }

func (m *MockStore) VehicleLocationSourceHistory(_ context.Context, vin string) ([]vehicleLocationSourceHistory, error) {
	if vin != "LB9A32A24R0LS1426" {
		return []vehicleLocationSourceHistory{}, nil
	}
	return []vehicleLocationSourceHistory{
		{Protocol: "GB32960", sourceKey: "GB32960:canonical", FirstSeenAt: "2026-03-01 08:00:00", LastSeenAt: "2026-07-16 16:20:08", ReportSampleCount: 108000},
		{Protocol: "JT808", sourceKey: "jt808:g7", FirstSeenAt: "2026-04-01 09:00:00", LastSeenAt: "2026-07-16 16:20:06", ReportSampleCount: 432000},
		{Protocol: "JT808", sourceKey: "jt808:beidou", FirstSeenAt: "2026-05-01 09:00:00", LastSeenAt: "2026-07-16 16:19:58", ReportSampleCount: 216000},
	}, nil
}

func (m *MockStore) VehicleSourcePolicy(_ context.Context, vin string) (VehicleSourcePolicyConfig, error) {
	m.sourcePolicyMu.RLock()
	defer m.sourcePolicyMu.RUnlock()
	config, exists := m.sourcePolicies[vin]
	if !exists {
		return VehicleSourcePolicyConfig{VIN: vin, Version: 1, UpdatedBy: "system", Audit: []VehicleSourcePolicyAudit{}}, nil
	}
	config.Audit = append([]VehicleSourcePolicyAudit(nil), config.Audit...)
	return config, nil
}

func (m *MockStore) SaveVehicleSourcePolicy(_ context.Context, update vehicleSourcePolicyStoreUpdate) (VehicleSourcePolicyConfig, error) {
	m.sourcePolicyMu.Lock()
	defer m.sourcePolicyMu.Unlock()
	current, exists := m.sourcePolicies[update.VIN]
	if !exists {
		current = VehicleSourcePolicyConfig{VIN: update.VIN, Version: 1, UpdatedBy: "system", Audit: []VehicleSourcePolicyAudit{}}
	}
	if current.Version != update.Version {
		return VehicleSourcePolicyConfig{}, clientError{Code: "SOURCE_POLICY_VERSION_CONFLICT", Message: "来源策略已被其他用户更新，请刷新后重试"}
	}
	if update.CurrentEnabled == update.Enabled && update.CurrentPriority == update.Priority && update.CurrentRemark == update.Remark {
		return current, nil
	}
	now := time.Now().Format(time.RFC3339)
	current.Version++
	current.UpdatedBy = update.Actor
	current.UpdatedAt = now
	current.Audit = append([]VehicleSourcePolicyAudit{{
		Version: current.Version, Protocol: update.Protocol, SourceRef: update.SourceRef,
		SourceLabel: update.SourceLabel, Actor: update.Actor, ChangedAt: now,
		Summary: update.SourceLabel + "：" + enabledLabel(update.CurrentEnabled) + "→" + enabledLabel(update.Enabled),
	}}, current.Audit...)
	m.sourcePolicies[update.VIN] = current
	return current, nil
}

func (m *MockStore) VehicleProfile(_ context.Context, vin string) (VehicleProfile, bool, error) {
	m.profileMu.RLock()
	defer m.profileMu.RUnlock()
	profile, ok := m.profiles[vin]
	return profile, ok, nil
}

func (m *MockStore) VehicleSourceEvidence(_ context.Context, vin string, date string) (VehicleSourceEvidence, error) {
	if vin != "LB9A32A24R0LS1426" {
		return VehicleSourceEvidence{
			VIN: vin, MileageDate: date,
			LocationSources: []VehicleLocationSourceEvidence{},
			MileageSources:  []VehicleMileageSourceEvidence{},
		}, nil
	}
	lonGB, latGB, speedGB, mileageGB, socGB := 113.2644, 23.1291, 41.8, 48798.9, 76.2
	lonJT1, latJT1, speedJT1, mileageJT1 := 113.2646, 23.1293, 42.5, 48797.6
	lonJT2, latJT2, speedJT2, mileageJT2 := 113.2675, 23.1315, 40.2, 48794.1
	firstGB, latestGB, dailyGB := 48782.3, 48798.9, 16.6
	firstJT, latestJT, dailyJT := 48781.7, 48797.6, 15.9
	return VehicleSourceEvidence{
		VIN: vin, Plate: "粤AG18312", MileageDate: date,
		LocationSources: []VehicleLocationSourceEvidence{
			{Protocol: "GB32960", SourceLabel: "车厂 32960", SourceKind: "PLATFORM", SelectedWithinProtocol: true, Enabled: true, Priority: 10, Online: true, QualityStatus: "OK", Longitude: &lonGB, Latitude: &latGB, SpeedKmh: &speedGB, TotalMileageKm: &mileageGB, SOCPercent: &socGB, EventTime: "2026-07-16 16:20:08", ReceivedAt: "2026-07-16 16:20:09", sourceKey: "GB32960:canonical"},
			{Protocol: "JT808", SourceLabel: "G7", TerminalLabel: "终端 133****5425", SourceKind: "PLATFORM", SelectedWithinProtocol: true, Enabled: true, Priority: 20, Online: true, QualityStatus: "OK", Longitude: &lonJT1, Latitude: &latJT1, SpeedKmh: &speedJT1, TotalMileageKm: &mileageJT1, EventTime: "2026-07-16 16:20:06", ReceivedAt: "2026-07-16 16:20:08", sourceKey: "jt808:g7"},
			{Protocol: "JT808", SourceLabel: "北斗平台", TerminalLabel: "终端 139****1208", SourceKind: "PLATFORM", SelectedWithinProtocol: false, Enabled: true, Priority: 30, Online: true, QualityStatus: "OK", Longitude: &lonJT2, Latitude: &latJT2, SpeedKmh: &speedJT2, TotalMileageKm: &mileageJT2, EventTime: "2026-07-16 16:19:58", ReceivedAt: "2026-07-16 16:20:02", sourceKey: "jt808:beidou"},
		},
		MileageSources: []VehicleMileageSourceEvidence{
			{Protocol: "GB32960", SourceLabel: "车厂 32960", SourceKind: "PLATFORM", SelectedWithinProtocol: true, Enabled: true, Priority: 10, QualityStatus: "OK", FirstTotalMileageKm: &firstGB, LatestTotalMileageKm: &latestGB, DailyMileageKm: &dailyGB, SampleCount: 1080, FirstEventTime: "2026-07-16 00:00:10", LatestEventTime: "2026-07-16 16:20:08", sourceKey: "gb:factory"},
			{Protocol: "JT808", SourceLabel: "G7", TerminalLabel: "终端 133****5425", SourceKind: "PLATFORM", SelectedWithinProtocol: true, Enabled: true, Priority: 20, QualityStatus: "OK", FirstTotalMileageKm: &firstJT, LatestTotalMileageKm: &latestJT, DailyMileageKm: &dailyJT, SampleCount: 4320, FirstEventTime: "2026-07-16 00:00:03", LatestEventTime: "2026-07-16 16:20:06", sourceKey: "jt808:g7"},
		},
	}, nil
}

func (m *MockStore) SaveVehicleProfile(_ context.Context, vin string, input VehicleProfileInput) (VehicleProfile, error) {
	m.profileMu.Lock()
	defer m.profileMu.Unlock()
	current, exists := m.profiles[vin]
	if !exists && input.Version != 0 {
		return VehicleProfile{}, clientError{Code: "VEHICLE_PROFILE_NOT_FOUND", Message: "待更新车辆档案不存在"}
	}
	if exists && input.Version != current.Version {
		return VehicleProfile{}, clientError{Code: "VEHICLE_PROFILE_VERSION_CONFLICT", Message: "车辆档案已被其他用户更新，请刷新后重试"}
	}
	version := 1
	if exists {
		version = current.Version + 1
	}
	profile := VehicleProfile{VIN: vin, ModelName: input.ModelName, VehicleType: input.VehicleType, CompanyName: input.CompanyName, OperationStatus: input.OperationStatus, AccessProvider: input.AccessProvider, FirstAccessAt: input.FirstAccessAt, RuntimeSeconds: input.RuntimeSeconds, SourceSystem: "manual", Version: version, UpdatedBy: input.Actor, UpdatedAt: time.Now().Format(time.RFC3339)}
	m.profiles[vin] = profile
	return profile, nil
}

func (m *MockStore) SyncVehicleProfiles(_ context.Context, request VehicleProfileSyncRequest) (VehicleProfileSyncResult, error) {
	m.profileMu.Lock()
	defer m.profileMu.Unlock()
	result := VehicleProfileSyncResult{
		SourceSystem: request.SourceSystem, SourceVersion: request.SourceVersion,
		DryRun: request.DryRun, Received: len(request.Items),
		Items: make([]VehicleProfileSyncItemResult, 0, len(request.Items)),
	}
	known := make(map[string]bool, len(m.vehicles))
	for _, vehicle := range m.vehicles {
		known[vehicle.VIN] = true
	}
	now := time.Now().Format(time.RFC3339)
	for _, item := range request.Items {
		itemResult := VehicleProfileSyncItemResult{VIN: item.VIN}
		if !known[item.VIN] {
			itemResult.Status = profileSyncMissingVehicle
			countVehicleProfileSyncResult(&result, itemResult.Status)
			result.Items = append(result.Items, itemResult)
			continue
		}
		current, exists := m.profiles[item.VIN]
		if exists {
			itemResult.PreviousSource = current.SourceSystem
			itemResult.PreviousVersion = current.SourceVersion
		}
		itemResult.Status = vehicleProfileSyncDecision(current, exists, request, item)
		itemResult.ProfileVersion = current.Version
		if itemResult.Status == profileSyncCreated {
			itemResult.ProfileVersion = 1
		} else if itemResult.Status == profileSyncUpdated {
			itemResult.ProfileVersion = current.Version + 1
		}
		countVehicleProfileSyncResult(&result, itemResult.Status)
		result.Items = append(result.Items, itemResult)
		if request.DryRun || (itemResult.Status != profileSyncCreated && itemResult.Status != profileSyncUpdated) {
			continue
		}
		m.profiles[item.VIN] = VehicleProfile{
			VIN: item.VIN, ModelName: item.ModelName, VehicleType: item.VehicleType,
			CompanyName: item.CompanyName, OperationStatus: item.OperationStatus,
			AccessProvider: item.AccessProvider, FirstAccessAt: item.FirstAccessAt,
			RuntimeSeconds: item.RuntimeSeconds, SourceSystem: request.SourceSystem,
			SourceVersion: request.SourceVersion, SyncedAt: now,
			Version: itemResult.ProfileVersion, UpdatedBy: request.Actor, UpdatedAt: now,
		}
	}
	return result, nil
}

func (m *MockStore) AccessEvidence(context.Context) ([]AccessEvidenceRow, error) {
	now := time.Now()
	seconds := func(value int) string { return now.Add(-time.Duration(value) * time.Second).Format(time.RFC3339) }
	interval10, interval30, interval60 := 10, 30, 60
	return []AccessEvidenceRow{
		{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", OEM: "比亚迪", Model: "G7s", Company: "岭牛示范车队", Protocol: "JT808", Provider: "G7", Source: "vehicle_realtime_snapshot", FirstSeenAt: now.AddDate(0, -4, 0).Format(time.RFC3339), LatestEventAt: seconds(13), LatestReceivedAt: seconds(12), LatestUpdatedAt: seconds(12), ReportIntervalSec: &interval10, ReportSampleCount: 120, LatestMessageType: "位置信息汇报", LatestEventID: "evt-jt808-001", FirstSeenEvidence: "网关实时写入首次观测", FirstSeenSource: "live_writer", ReportIntervalProof: "网关连续接收时间差（持久样本 120 条）"},
		{VIN: "LNXNEGRR7SR318212", Plate: "川AHTWO1", OEM: "现代", Model: "氢燃料车型", Company: "示范物流", Protocol: "GB32960", Provider: "车厂平台", Source: "vehicle_realtime_snapshot", FirstSeenAt: now.AddDate(0, -2, 0).Format(time.RFC3339), LatestEventAt: seconds(57), LatestReceivedAt: seconds(12), LatestUpdatedAt: seconds(12), ReportIntervalSec: &interval30, ReportSampleCount: 48, LatestMessageType: "实时信息上报", LatestEventID: "evt-gb32960-002", LatestError: "上游事件时间延迟", FirstSeenEvidence: "网关实时写入首次观测", FirstSeenSource: "live_writer", ReportIntervalProof: "网关连续接收时间差（持久样本 48 条）"},
		{VIN: "LMRKH9AC2R1004087", Plate: "豫A88888", OEM: "宇通", Model: "纯电客车", Protocol: "YUTONG_MQTT", Provider: "宇通云", Source: "vehicle_realtime_snapshot", FirstSeenAt: now.AddDate(0, -8, 0).Format(time.RFC3339), LatestEventAt: seconds(2400), LatestReceivedAt: seconds(2398), LatestUpdatedAt: seconds(2398), ReportIntervalSec: &interval60, LatestMessageType: "实时遥测", LatestEventID: "evt-mqtt-003", FirstSeenEvidence: "测试接入台账", ReportIntervalProof: "最近两次接收时间差"},
		{VIN: "LB9A32A24P0LS1230", Plate: "粤AFF7936", OEM: "广安车联", Model: "运营车辆", Protocol: "JT808", Provider: "广安车联", Source: "vehicle_realtime_snapshot", FirstSeenAt: now.AddDate(-1, 0, 0).Format(time.RFC3339), LatestEventAt: seconds(9100), LatestReceivedAt: seconds(9098), LatestUpdatedAt: seconds(9098), ReportIntervalSec: &interval30, LatestMessageType: "位置信息汇报", LatestEventID: "evt-jt808-004", LatestError: "来源链路长时间无更新", FirstSeenEvidence: "测试接入台账", ReportIntervalProof: "最近两次接收时间差"},
		{VIN: "LUNKNOWNACCESS01", Plate: "粤A待核1", OEM: "待维护", Protocol: "UNKNOWN", Provider: "未知平台", Source: "vehicle_realtime_snapshot", LatestEventAt: "invalid-time", LatestMessageType: "协议未识别", LatestError: "协议或时间字段无法识别"},
		{VIN: "LNEVERREPORT0001", Plate: "粤A未报1", OEM: "档案车辆", Model: "待接入", Source: "vehicle_identity_binding", LatestError: "车辆已建档但从未形成实时快照"},
	}, nil
}

func (m *MockStore) AccessUnresolvedIdentities(_ context.Context, query AccessUnresolvedIdentityQuery) (Page[AccessUnresolvedIdentity], error) {
	items := []AccessUnresolvedIdentity{{
		ID: "mock-unresolved-jt808", Protocol: "JT808", IdentifierMasked: "138****0001", Plate: "待维护",
		Manufacturer: "示范终端", SourceEndpoint: "gateway-a", LatestSeenAt: time.Now().Add(-30 * time.Second).Format(time.RFC3339),
		FreshnessSec: 30, IssueCode: "missing_vin_jt808", RecommendedAction: "核对终端手机号、车牌和厂家后维护 phone→VIN 权威绑定；禁止猜测 VIN",
	}}
	if query.Offset >= len(items) {
		return Page[AccessUnresolvedIdentity]{Items: []AccessUnresolvedIdentity{}, Total: len(items), Limit: query.Limit, Offset: query.Offset}, nil
	}
	return Page[AccessUnresolvedIdentity]{Items: items, Total: len(items), Limit: query.Limit, Offset: query.Offset}, nil
}

func (m *MockStore) AccessThresholds(context.Context) (AccessThresholdConfig, error) {
	m.accessMu.RLock()
	defer m.accessMu.RUnlock()
	config := m.accessThresholds
	config.Protocols = append([]AccessProtocolThreshold(nil), config.Protocols...)
	config.Audit = append([]AccessThresholdAudit(nil), config.Audit...)
	return config, nil
}

func (m *MockStore) SaveAccessThresholds(_ context.Context, update AccessThresholdUpdate) (AccessThresholdConfig, error) {
	m.accessMu.Lock()
	defer m.accessMu.Unlock()
	if update.Version != m.accessThresholds.Version {
		return AccessThresholdConfig{}, clientError{Code: "ACCESS_THRESHOLD_VERSION_CONFLICT", Message: "阈值配置已被其他用户更新，请刷新后重试"}
	}
	now := time.Now().Format(time.RFC3339)
	next := AccessThresholdConfig{
		Version:             update.Version + 1,
		DefaultThresholdSec: update.DefaultThresholdSec,
		DelayThresholdSec:   update.DelayThresholdSec,
		LongOfflineSec:      update.LongOfflineSec,
		Protocols:           append([]AccessProtocolThreshold(nil), update.Protocols...),
		UpdatedBy:           update.Actor,
		UpdatedAt:           now,
		Audit:               append([]AccessThresholdAudit{{Version: update.Version + 1, Actor: update.Actor, ChangedAt: now, Summary: "更新在线、延迟和长离线阈值"}}, m.accessThresholds.Audit...),
	}
	if len(next.Audit) > 10 {
		next.Audit = next.Audit[:10]
	}
	m.accessThresholds = next
	return next, nil
}

func (m *MockStore) DashboardSummary(context.Context) (DashboardSummary, error) {
	kafkaLag := 0
	return DashboardSummary{
		OnlineVehicles: 3,
		ActiveToday:    4,
		FrameToday:     1286320,
		IssueVehicles:  7,
		KafkaLag:       &kafkaLag,
		Protocols: []ProtocolStat{
			{Protocol: "GB32960", Online: 18, Total: 64},
			{Protocol: "JT808", Online: 73, Total: 318},
			{Protocol: "YUTONG_MQTT", Online: 1, Total: 8},
		},
		ServiceStatuses: []ServiceStatusStat{
			{Status: "healthy", Title: "服务正常", Count: 2},
			{Status: "degraded", Title: "来源不完整", Count: 1},
			{Status: "offline", Title: "车辆离线", Count: 1},
			{Status: "identity_required", Title: "身份未绑定", Count: 0},
		},
		LinkHealth: []LinkHealth{
			{Name: "Redis realtime", Status: "ok", Detail: "在线状态 1 分钟 TTL 正常"},
			{Name: "TDengine raw_frames", Status: "ok", Detail: "最近 5 分钟有写入"},
			{Name: "Kafka vehicle.fields", Status: "ok", Detail: "consumer lag 0"},
			{Name: "MySQL identity", Status: "warning", Detail: "部分 808 phone 未绑定 VIN"},
		},
	}, nil
}

func (m *MockStore) Vehicles(_ context.Context, query url.Values) (Page[VehicleRow], error) {
	items := filterVehicles(m.vehicles, query)
	enriched := make([]VehicleRow, 0, len(items))
	for _, item := range items {
		item.ServiceStatus = m.vehicleRowServiceStatus(item)
		if keepServiceStatus(item.ServiceStatus, query.Get("serviceStatus")) {
			enriched = append(enriched, item)
		}
	}
	return page(enriched, query), nil
}

func (m *MockStore) vehicleRowServiceStatus(row VehicleRow) *VehicleServiceStatus {
	status := VehicleCoverageRow{
		VIN:           row.VIN,
		Plate:         row.Plate,
		Phone:         row.Phone,
		OEM:           row.OEM,
		LastSeen:      row.LastSeen,
		BindingStatus: "bound",
	}
	if strings.TrimSpace(row.VIN) == "" {
		status.BindingStatus = "unbound"
	}
	for _, vehicle := range m.vehicles {
		if vehicle.VIN != row.VIN {
			continue
		}
		if !containsString(status.Protocols, vehicle.Protocol) {
			status.Protocols = append(status.Protocols, vehicle.Protocol)
			status.SourceCount = len(status.Protocols)
		}
		if vehicle.Online {
			status.Online = true
			status.OnlineSourceCount++
		}
		if vehicle.LastSeen > status.LastSeen {
			status.LastSeen = vehicle.LastSeen
		}
	}
	return buildVehicleCoverageServiceStatus(status)
}

func (m *MockStore) VehicleCoverage(_ context.Context, query url.Values) (Page[VehicleCoverageRow], error) {
	vehicles := filterVehicles(m.vehicles, query)
	byVIN := map[string]*VehicleCoverageRow{}
	for _, vehicle := range vehicles {
		current := byVIN[vehicle.VIN]
		if current == nil {
			current = &VehicleCoverageRow{
				VIN:           vehicle.VIN,
				Plate:         vehicle.Plate,
				Phone:         vehicle.Phone,
				OEM:           vehicle.OEM,
				LastSeen:      vehicle.LastSeen,
				BindingStatus: "bound",
			}
			byVIN[vehicle.VIN] = current
		}
		if vehicle.LastSeen > current.LastSeen {
			current.LastSeen = vehicle.LastSeen
		}
		if vehicle.Online {
			current.Online = true
			current.OnlineSourceCount++
		}
		if !containsString(current.Protocols, vehicle.Protocol) {
			current.Protocols = append(current.Protocols, vehicle.Protocol)
			current.SourceCount = len(current.Protocols)
		}
	}
	items := make([]VehicleCoverageRow, 0, len(byVIN))
	for _, row := range byVIN {
		row.MissingProtocols = missingCanonicalProtocols(row.Protocols)
		onlineProtocols := make([]string, 0, row.OnlineSourceCount)
		for _, vehicle := range vehicles {
			if vehicle.VIN == row.VIN && vehicle.Online {
				onlineProtocols = append(onlineProtocols, vehicle.Protocol)
			}
		}
		row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, onlineProtocols, row.LastSeen)
		row.ServiceStatus = buildVehicleCoverageServiceStatus(*row)
		row.SourceConsistency = buildVehicleCoverageSourceConsistency(*row)
		if keepCoverageRow(*row, query) {
			items = append(items, *row)
		}
	}
	sortVehicleCoverage(items)
	return page(items, query), nil
}

func (m *MockStore) VehicleCoverageSummary(ctx context.Context, query url.Values) (VehicleCoverageSummary, error) {
	allQuery := copyValues(query)
	allQuery.Set("limit", "100000")
	allQuery.Set("offset", "0")
	coverage, err := m.VehicleCoverage(ctx, allQuery)
	if err != nil {
		return VehicleCoverageSummary{}, err
	}
	summary := VehicleCoverageSummary{TotalVehicles: coverage.Total}
	missingFieldCounts := map[string]int{}
	for _, row := range coverage.Items {
		if row.Online {
			summary.OnlineVehicles++
		}
		if row.SourceCount == 1 {
			summary.SingleSourceVehicles++
		}
		if row.SourceCount > 1 {
			summary.MultiSourceVehicles++
		}
		if row.SourceCount == 0 {
			summary.NoDataVehicles++
		}
		if row.BindingStatus != "bound" {
			summary.UnboundVehicles++
		}
		if vehicleArchiveIncomplete(row) {
			summary.ArchiveIncompleteVehicles++
		}
		for _, field := range vehicleArchiveMissingFields(row) {
			missingFieldCounts[field]++
		}
	}
	for _, protocol := range canonicalVehicleProtocols {
		missing := 0
		for _, row := range coverage.Items {
			if !containsString(row.Protocols, protocol) {
				missing++
			}
		}
		summary.MissingSources = append(summary.MissingSources, MissingSourceStat{Protocol: protocol, Count: missing})
	}
	summary.ArchiveMissingFields = buildArchiveMissingFieldStats(
		missingFieldCounts["plate"],
		missingFieldCounts["phone"],
		missingFieldCounts["oem"],
	)
	return summary, nil
}

func (m *MockStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) {
	coverage, err := m.VehicleCoverage(ctx, url.Values{"limit": {"10000"}})
	if err != nil {
		return VehicleServiceSummary{}, err
	}
	summary := VehicleServiceSummary{
		TotalVehicles:   coverage.Total,
		ServiceStatuses: []ServiceStatusStat{},
		Protocols:       []ProtocolStat{},
		MissingSources:  []MissingSourceStat{},
	}
	statusCounts := map[string]*ServiceStatusStat{
		"healthy":           {Status: "healthy", Title: "服务正常"},
		"degraded":          {Status: "degraded", Title: "来源不完整"},
		"offline":           {Status: "offline", Title: "车辆离线"},
		"no_data":           {Status: "no_data", Title: "暂无数据来源"},
		"identity_required": {Status: "identity_required", Title: "身份未绑定"},
	}
	protocolCounts := map[string]*ProtocolStat{}
	missingFieldCounts := map[string]int{}
	for _, row := range coverage.Items {
		if row.BindingStatus == "bound" {
			summary.BoundVehicles++
		}
		if vehicleArchiveIncomplete(row) {
			summary.ArchiveIncompleteVehicles++
		}
		for _, field := range vehicleArchiveMissingFields(row) {
			missingFieldCounts[field]++
		}
		if row.Online {
			summary.OnlineVehicles++
		}
		switch {
		case row.SourceCount <= 0:
			summary.NoDataVehicles++
		case row.SourceCount == 1:
			summary.SingleSourceVehicles++
		default:
			summary.MultiSourceVehicles++
		}
		status := ""
		title := ""
		if row.ServiceStatus != nil {
			status = row.ServiceStatus.Status
			title = row.ServiceStatus.Title
		}
		if status != "" {
			current := statusCounts[status]
			if current == nil {
				current = &ServiceStatusStat{Status: status, Title: title}
				statusCounts[status] = current
			}
			current.Count++
			if status == "identity_required" {
				summary.IdentityRequiredVehicles++
			}
		}
		onlineByProtocol := map[string]bool{}
		for _, status := range row.SourceStatus {
			onlineByProtocol[status.Protocol] = status.Online
		}
		for _, protocol := range row.Protocols {
			current := protocolCounts[protocol]
			if current == nil {
				current = &ProtocolStat{Protocol: protocol}
				protocolCounts[protocol] = current
			}
			current.Total++
			if onlineByProtocol[protocol] {
				current.Online++
			}
		}
	}
	for _, status := range statusCounts {
		summary.ServiceStatuses = append(summary.ServiceStatuses, *status)
	}
	for _, protocol := range protocolCounts {
		summary.Protocols = append(summary.Protocols, *protocol)
	}
	summary.Protocols = completeProtocolStats(summary.Protocols)
	for _, protocol := range canonicalVehicleProtocols {
		missing := 0
		for _, row := range coverage.Items {
			if !containsString(row.Protocols, protocol) {
				missing++
			}
		}
		summary.MissingSources = append(summary.MissingSources, MissingSourceStat{Protocol: protocol, Count: missing})
	}
	summary.ArchiveMissingFields = buildArchiveMissingFieldStats(
		missingFieldCounts["plate"],
		missingFieldCounts["phone"],
		missingFieldCounts["oem"],
	)
	sort.Slice(summary.ServiceStatuses, func(i, j int) bool { return summary.ServiceStatuses[i].Status < summary.ServiceStatuses[j].Status })
	return summary, nil
}

func vehicleArchiveIncomplete(row VehicleCoverageRow) bool {
	return strings.TrimSpace(row.VIN) == "" ||
		strings.TrimSpace(row.Plate) == "" ||
		strings.TrimSpace(row.Phone) == "" ||
		strings.TrimSpace(row.OEM) == ""
}

func vehicleArchiveMissingFields(row VehicleCoverageRow) []string {
	fields := []string{}
	if strings.TrimSpace(row.Plate) == "" {
		fields = append(fields, "plate")
	}
	if strings.TrimSpace(row.Phone) == "" {
		fields = append(fields, "phone")
	}
	if strings.TrimSpace(row.OEM) == "" {
		fields = append(fields, "oem")
	}
	return fields
}

func buildArchiveMissingFieldStats(missingPlate, missingPhone, missingOEM int) []ArchiveMissingFieldStat {
	return []ArchiveMissingFieldStat{
		{Field: "plate", Title: "缺车牌", Count: missingPlate},
		{Field: "phone", Title: "缺手机号", Count: missingPhone},
		{Field: "oem", Title: "缺OEM", Count: missingOEM},
	}
}

func (m *MockStore) VehicleRealtime(_ context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
	rows := m.locations
	keywords := vehicleSearchKeywords(query)
	matchesIdentity := func(vin, plate, phone, oem string) bool {
		if len(keywords) == 0 {
			return true
		}
		batch := strings.TrimSpace(query.Get("keywords")) != ""
		if batch {
			for _, keyword := range keywords {
				if strings.EqualFold(vin, keyword) || strings.EqualFold(plate, keyword) {
					return true
				}
			}
			return false
		}
		haystack := strings.ToLower(vin + plate + phone + oem)
		for _, keyword := range keywords {
			if strings.Contains(haystack, strings.ToLower(keyword)) {
				return true
			}
		}
		return false
	}
	if len(keywords) > 0 {
		rows = keep(rows, func(row RealtimeLocationRow) bool {
			vehicle := m.vehicleByVIN(row.VIN)
			return matchesIdentity(row.VIN, firstNonEmpty(row.Plate, vehicle.Plate), vehicle.Phone, vehicle.OEM)
		})
	}
	if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
		rows = keep(rows, func(row RealtimeLocationRow) bool { return row.Protocol == protocol })
	}
	scopeVINs := splitCSV(query.Get("scopeVins"))
	scope := make(map[string]struct{}, len(scopeVINs))
	for _, vin := range scopeVINs {
		scope[vin] = struct{}{}
	}
	if len(scope) > 0 {
		rows = keep(rows, func(row RealtimeLocationRow) bool {
			_, allowed := scope[row.VIN]
			return allowed
		})
	}
	byVIN := map[string]*VehicleRealtimeRow{}
	for _, location := range rows {
		current := byVIN[location.VIN]
		if current == nil {
			vehicle := m.vehicleByVIN(location.VIN)
			current = &VehicleRealtimeRow{
				VIN:           location.VIN,
				Plate:         firstNonEmpty(location.Plate, vehicle.Plate),
				Phone:         vehicle.Phone,
				OEM:           vehicle.OEM,
				BindingStatus: "unbound",
				LastSeen:      location.LastSeen,
			}
			if vehicle.VIN != "" {
				current.BindingStatus = "bound"
			}
			byVIN[location.VIN] = current
		}
		if !containsString(current.Protocols, location.Protocol) {
			current.Protocols = append(current.Protocols, location.Protocol)
			current.SourceCount = len(current.Protocols)
		}
		if location.LastSeen >= current.LastSeen {
			current.PrimaryProtocol = location.Protocol
			current.LocationAvailable = validRealtimeCoordinate(location.Longitude, location.Latitude)
			current.Longitude = location.Longitude
			current.Latitude = location.Latitude
			current.SpeedAvailable = true
			current.SpeedKmh = location.SpeedKmh
			current.SOCAvailable = location.Protocol == "GB32960" || location.Protocol == "YUTONG_MQTT"
			current.SOCPercent = location.SOCPercent
			current.MileageAvailable = true
			current.TotalMileageKm = location.TotalMileageKm
			current.LastSeen = location.LastSeen
		}
		if location.LastSeen >= "2026-07-03 20:11:00" {
			current.Online = true
			current.OnlineSourceCount++
		}
	}
	if strings.TrimSpace(query.Get("protocol")) == "" {
		for _, vehicle := range m.vehicles {
			if _, exists := byVIN[vehicle.VIN]; exists {
				continue
			}
			if len(scope) > 0 {
				if _, allowed := scope[vehicle.VIN]; !allowed {
					continue
				}
			}
			if !matchesIdentity(vehicle.VIN, vehicle.Plate, vehicle.Phone, vehicle.OEM) {
				continue
			}
			byVIN[vehicle.VIN] = &VehicleRealtimeRow{
				VIN:           vehicle.VIN,
				Plate:         vehicle.Plate,
				Phone:         vehicle.Phone,
				OEM:           vehicle.OEM,
				BindingStatus: "bound",
			}
		}
	}
	items := make([]VehicleRealtimeRow, 0, len(byVIN))
	for _, row := range byVIN {
		sort.Strings(row.Protocols)
		onlineProtocols := make([]string, 0, row.OnlineSourceCount)
		for _, location := range rows {
			if location.VIN == row.VIN && location.LastSeen >= "2026-07-03 20:11:00" {
				onlineProtocols = append(onlineProtocols, location.Protocol)
			}
		}
		row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, onlineProtocols, row.LastSeen)
		row.ServiceStatus = buildRealtimeServiceStatus(*row)
		switch strings.TrimSpace(query.Get("online")) {
		case "online":
			if !row.Online {
				continue
			}
		case "offline":
			if row.Online || row.SourceCount == 0 {
				continue
			}
		}
		if status := strings.TrimSpace(query.Get("status")); status != "" && !matchesMonitorStatus(*row, status) {
			if status != "no_location" || row.LocationAvailable {
				continue
			}
		}
		if !keepServiceStatus(row.ServiceStatus, query.Get("serviceStatus")) {
			continue
		}
		items = append(items, *row)
	}
	sort.Slice(items, func(i, j int) bool {
		if items[i].LastSeen == items[j].LastSeen {
			return items[i].VIN < items[j].VIN
		}
		return items[i].LastSeen > items[j].LastSeen
	})
	return page(items, query), nil
}

func (m *MockStore) VehicleServiceOverviews(_ context.Context, query VehicleOverviewBatchQuery) (Page[VehicleServiceOverview], error) {
	items := make([]VehicleServiceOverview, 0, len(query.Keywords))
	for _, keyword := range normalizedKeywords(query.Keywords) {
		vehicles := m.vehiclesForKeyword(keyword, query.Protocol)
		resolution := buildVehicleIdentityResolution(keyword, vehicles)
		identity := resolveVehicleIdentity(keyword, vehicles)
		resolvedVIN := ""
		if identity != nil && strings.TrimSpace(identity.VIN) != "" {
			resolvedVIN = identity.VIN
		} else if resolution.Resolved {
			resolvedVIN = resolution.VIN
		}
		if resolvedVIN == "" {
			items = append(items, *buildVehicleServiceOverview("", keyword, &resolution, nil, nil, nil, Page[HistoryLocationRow]{}, Page[RawFrameRow]{}, Page[DailyMileageRow]{}, Page[QualityIssueRow]{}))
			continue
		}
		summary := m.realtimeSummaryForVIN(resolvedVIN, query.Protocol)
		sourceStatus := vehicleSourceStatus(vehicles, nil, nil, nil, nil)
		items = append(items, *buildVehicleServiceOverview(resolvedVIN, keyword, &resolution, identity, summary, sourceStatus, Page[HistoryLocationRow]{}, Page[RawFrameRow]{}, Page[DailyMileageRow]{}, Page[QualityIssueRow]{}))
	}
	return Page[VehicleServiceOverview]{Items: items, Total: len(items), Limit: query.Limit, Offset: query.Offset}, nil
}

func (m *MockStore) vehiclesForKeyword(keyword string, protocol string) []VehicleRow {
	keyword = strings.ToLower(strings.TrimSpace(keyword))
	protocol = strings.TrimSpace(protocol)
	out := make([]VehicleRow, 0)
	for _, vehicle := range m.vehicles {
		if protocol != "" && vehicle.Protocol != protocol {
			continue
		}
		haystack := strings.ToLower(vehicle.VIN + vehicle.Plate + vehicle.Phone + vehicle.OEM)
		if keyword == "" || strings.Contains(haystack, keyword) {
			out = append(out, vehicle)
		}
	}
	return out
}

func (m *MockStore) realtimeSummaryForVIN(vin string, protocol string) *VehicleRealtimeRow {
	protocol = strings.TrimSpace(protocol)
	var summary *VehicleRealtimeRow
	for _, location := range m.locations {
		if location.VIN != vin {
			continue
		}
		if protocol != "" && location.Protocol != protocol {
			continue
		}
		if summary == nil {
			vehicle := m.vehicleByVIN(location.VIN)
			summary = &VehicleRealtimeRow{
				VIN:           location.VIN,
				Plate:         firstNonEmpty(location.Plate, vehicle.Plate),
				Phone:         vehicle.Phone,
				OEM:           vehicle.OEM,
				BindingStatus: "bound",
				LastSeen:      location.LastSeen,
			}
		}
		if !containsString(summary.Protocols, location.Protocol) {
			summary.Protocols = append(summary.Protocols, location.Protocol)
			summary.SourceCount = len(summary.Protocols)
		}
		if location.LastSeen >= summary.LastSeen {
			summary.PrimaryProtocol = location.Protocol
			summary.LocationAvailable = validRealtimeCoordinate(location.Longitude, location.Latitude)
			summary.Longitude = location.Longitude
			summary.Latitude = location.Latitude
			summary.SpeedAvailable = true
			summary.SpeedKmh = location.SpeedKmh
			summary.SOCAvailable = location.Protocol == "GB32960" || location.Protocol == "YUTONG_MQTT"
			summary.SOCPercent = location.SOCPercent
			summary.MileageAvailable = true
			summary.TotalMileageKm = location.TotalMileageKm
			summary.LastSeen = location.LastSeen
		}
		if location.LastSeen >= "2026-07-03 20:11:00" {
			summary.Online = true
			summary.OnlineSourceCount++
		}
	}
	if summary != nil {
		sort.Strings(summary.Protocols)
		summary.ServiceStatus = buildRealtimeServiceStatus(*summary)
	}
	return summary
}

func (m *MockStore) vehicleByVIN(vin string) VehicleRow {
	for _, vehicle := range m.vehicles {
		if vehicle.VIN == vin {
			return vehicle
		}
	}
	return VehicleRow{}
}

func (m *MockStore) RealtimeLocations(_ context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
	items := m.locations
	if vin := strings.TrimSpace(query.Get("vin")); vin != "" {
		items = keep(items, func(row RealtimeLocationRow) bool { return row.VIN == vin })
	}
	if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
		items = keep(items, func(row RealtimeLocationRow) bool { return row.Protocol == protocol })
	}
	return page(items, query), nil
}

func (m *MockStore) HistoryLocations(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
	return m.HistoryLocationsFromTDengine(ctx, query)
}

func (m *MockStore) HistoryLocationsFromTDengine(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
	realtime, _ := m.RealtimeLocations(ctx, query)
	rows := make([]HistoryLocationRow, 0, len(realtime.Items)*18)
	speeds := []float64{0, 8, 18, 32, 61, 44, 12, 0, 16, 43, 72, 38, 26, 54, 80, 41, 14, 0}
	for _, row := range realtime.Items {
		endTime, ok := parseVehicleServiceTime(row.LastSeen)
		if !ok {
			endTime = time.Date(2026, 7, 3, 20, 12, 0, 0, time.UTC)
		}
		for index, speed := range speeds {
			remaining := len(speeds) - 1 - index
			observedAt := endTime.Add(-time.Duration(remaining*5) * time.Minute).Format("2006-01-02 15:04:05")
			rows = append(rows, HistoryLocationRow{
				VIN: row.VIN, Plate: row.Plate, Protocol: row.Protocol,
				Longitude: row.Longitude - float64(remaining)*0.0062,
				Latitude:  row.Latitude - float64(remaining)*0.0021 + float64(index%3)*0.0008,
				SpeedKmh:  speed, TotalMileageKm: row.TotalMileageKm - float64(remaining)*1.35,
				DeviceTime: observedAt, ServerTime: observedAt,
			})
		}
	}
	return page(rows, query), nil
}

func (m *MockStore) HistoryLocationSeries(ctx context.Context, query HistoryLocationSeriesQuery) ([]HistoryLocationSeriesBucket, error) {
	page, err := m.HistoryLocationsFromTDengine(ctx, url.Values{"vin": {query.VIN}, "protocol": {query.Protocol}, "limit": {"1000"}})
	if err != nil {
		return nil, err
	}
	buckets := make([]HistoryLocationSeriesBucket, 0, len(page.Items))
	for _, row := range page.Items {
		speed, mileage := row.SpeedKmh, row.TotalMileageKm
		buckets = append(buckets, HistoryLocationSeriesBucket{Time: row.DeviceTime, Protocol: row.Protocol, Count: 1, SpeedAverage: &speed, SpeedMinimum: &speed, SpeedMaximum: &speed, SpeedLast: &speed, MileageAverage: &mileage, MileageMinimum: &mileage, MileageMaximum: &mileage, MileageLast: &mileage})
	}
	sort.SliceStable(buckets, func(i, j int) bool { return buckets[i].Time < buckets[j].Time })
	return buckets, nil
}

func (m *MockStore) HistoryExportCount(ctx context.Context, query HistoryExportStoreQuery) (int64, error) {
	rows, err := m.mockHistoryExportRows(ctx, query)
	return int64(len(rows)), err
}

func (m *MockStore) HistoryExportScopeActive(context.Context, HistoryExportJob) (bool, error) {
	return true, nil
}

func (m *MockStore) HistoryExportBatch(ctx context.Context, query HistoryExportStoreQuery, cursor HistoryExportCursor, limit int) ([]HistoryDataRow, HistoryExportCursor, error) {
	rows, err := m.mockHistoryExportRows(ctx, query)
	if err != nil {
		return nil, cursor, err
	}
	if limit <= 0 {
		limit = 5000
	}
	start := cursor.Offset
	if start > len(rows) {
		start = len(rows)
	}
	end := start + limit
	if end > len(rows) {
		end = len(rows)
	}
	result := append([]HistoryDataRow(nil), rows[start:end]...)
	cursor.Offset = end
	return result, cursor, nil
}

func (m *MockStore) mockHistoryExportRows(ctx context.Context, query HistoryExportStoreQuery) ([]HistoryDataRow, error) {
	switch query.Category {
	case "raw":
		page, err := m.RawFrames(ctx, RawFrameQuery{VIN: query.VIN, Protocol: query.Protocol, IncludeFields: true, Fields: query.Metrics, Limit: 1000})
		if err != nil {
			return nil, err
		}
		rows := make([]HistoryDataRow, 0, len(page.Items))
		for _, item := range page.Items {
			values := map[string]any{"frameType": item.FrameType, "rawSizeBytes": item.RawSizeBytes}
			for key, value := range item.ParsedFields {
				values[key] = value
			}
			quality, reason := historyRawQuality(item)
			rows = append(rows, HistoryDataRow{ID: item.ID, VIN: item.VIN, Plate: item.Plate, Protocol: item.Protocol, DeviceTime: item.DeviceTime, ServerTime: item.ServerTime, Quality: quality, QualityReason: reason, EvidenceID: item.ID, Values: values})
		}
		return rows, nil
	case "mileage":
		page, err := m.DailyMileage(ctx, url.Values{"vin": {query.VIN}, "protocol": {query.Protocol}, "limit": {"1000"}})
		if err != nil {
			return nil, err
		}
		rows := make([]HistoryDataRow, 0, len(page.Items))
		for index, item := range page.Items {
			quality, reason := historyMileageQuality(item)
			rows = append(rows, HistoryDataRow{ID: "mileage-" + item.VIN + "-" + strconv.Itoa(index), VIN: item.VIN, Plate: item.Plate, Protocol: item.Source, DeviceTime: item.Date, ServerTime: item.Date, Quality: quality, QualityReason: reason, Values: map[string]any{"startMileageKm": item.StartMileageKm, "endMileageKm": item.EndMileageKm, "dailyMileageKm": item.DailyMileageKm}})
		}
		return rows, nil
	default:
		page, err := m.HistoryLocationsFromTDengine(ctx, url.Values{"vin": {query.VIN}, "protocol": {query.Protocol}, "limit": {"1000"}})
		if err != nil {
			return nil, err
		}
		rows := make([]HistoryDataRow, 0, len(page.Items))
		for index, item := range page.Items {
			quality, reason := historyLocationQuality(item)
			rows = append(rows, HistoryDataRow{ID: "location-" + item.VIN + "-" + strconv.Itoa(index), VIN: item.VIN, Plate: item.Plate, Protocol: item.Protocol, DeviceTime: item.DeviceTime, ServerTime: item.ServerTime, Quality: quality, QualityReason: reason, Values: map[string]any{"speedKmh": item.SpeedKmh, "totalMileageKm": item.TotalMileageKm, "longitude": item.Longitude, "latitude": item.Latitude}})
		}
		return rows, nil
	}
}

func (m *MockStore) RawFrames(_ context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
	fields := map[string]any{
		"gb32960.vehicle.speed_kmh":                  42.5,
		"gb32960.vehicle.total_mileage_km":           48798.9,
		"gb32960.fuel_cell.stack_1.avg_voltage_v":    0.79,
		"jt808.location.additional.total_mileage_km": 48798.9,
	}
	if len(query.Fields) > 0 {
		filtered := map[string]any{}
		for _, key := range query.Fields {
			if value, ok := fields[key]; ok {
				filtered[key] = value
			}
		}
		fields = filtered
	}
	if !query.IncludeFields && len(query.Fields) == 0 {
		fields = nil
	}
	rows := []RawFrameRow{
		{ID: "raw-20260703-001", VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Protocol: "GB32960", FrameType: "realtime", DeviceTime: "2026-07-03 20:12:06", ServerTime: "2026-07-03 20:12:06", RawSizeBytes: 430, ParseStatus: "ok", SourceEndpoint: "gb32960-gateway", ParsedFields: fields},
		{ID: "raw-20260703-002", VIN: "LNXNEGRR7SR318212", Plate: "川AHTWO1", Protocol: "GB32960", FrameType: "realtime", DeviceTime: "2026-07-03 20:12:06", ServerTime: "2026-07-03 20:12:06", RawSizeBytes: 430, ParseStatus: "ok", SourceEndpoint: "gb32960-gateway", ParsedFields: fields},
	}
	if vin := strings.TrimSpace(query.VIN); vin != "" {
		rows = keep(rows, func(row RawFrameRow) bool { return row.VIN == vin })
	}
	if protocol := strings.TrimSpace(query.Protocol); protocol != "" {
		rows = keep(rows, func(row RawFrameRow) bool { return row.Protocol == protocol })
	}
	limit := query.Limit
	if limit <= 0 {
		limit = 100
	}
	return page(rows, url.Values{"limit": {strconv.Itoa(limit)}, "offset": {strconv.Itoa(query.Offset)}}), nil
}

func (m *MockStore) DailyMileage(_ context.Context, query url.Values) (Page[DailyMileageRow], error) {
	rows := m.dailyMileageRows(query)
	return page(rows, query), nil
}

func (m *MockStore) MileageSummary(_ context.Context, query url.Values) (MileageSummary, error) {
	rows := m.dailyMileageRows(query)
	vehicles := map[string]struct{}{}
	sources := map[string]struct{}{}
	total := 0.0
	for _, row := range rows {
		vehicles[row.VIN] = struct{}{}
		sources[row.Source] = struct{}{}
		total += row.DailyMileageKm
	}
	summary := MileageSummary{
		VehicleCount:   len(vehicles),
		RecordCount:    len(rows),
		SourceCount:    len(sources),
		TotalMileageKm: total,
	}
	if summary.VehicleCount > 0 {
		summary.AverageMileagePerVIN = summary.TotalMileageKm / float64(summary.VehicleCount)
	}
	return summary, nil
}

func (m *MockStore) MileageStatistics(_ context.Context, query url.Values) (MileageStatistics, error) {
	rows := m.dailyMileageRows(query)
	byDate := map[string]*MileageTrendPoint{}
	result := MileageStatistics{DateFrom: query.Get("dateFrom"), DateTo: query.Get("dateTo"), Trend: []MileageTrendPoint{}, Ranking: []MileageVehicleRank{}, AsOf: "2026-07-03 20:12:06", Evidence: "mock mileage statistics"}
	vehicles, sources := map[string]*MileageVehicleRank{}, map[string]struct{}{}
	for _, row := range rows {
		result.PeriodMileageKm += row.DailyMileageKm
		result.RecordCount++
		sources[row.Source] = struct{}{}
		point := byDate[row.Date]
		if point == nil {
			point = &MileageTrendPoint{Date: row.Date}
			byDate[row.Date] = point
		}
		point.MileageKm += row.DailyMileageKm
		point.Vehicles++
		rank := vehicles[row.VIN]
		if rank == nil {
			rank = &MileageVehicleRank{VIN: row.VIN, Plate: row.Plate}
			vehicles[row.VIN] = rank
		}
		rank.MileageKm += row.DailyMileageKm
		rank.LatestMileageKm = row.EndMileageKm
		rank.ActiveDays++
	}
	result.VehicleCount, result.SourceCount = len(vehicles), len(sources)
	for _, point := range byDate {
		result.Trend = append(result.Trend, *point)
	}
	sort.Slice(result.Trend, func(i, j int) bool { return result.Trend[i].Date < result.Trend[j].Date })
	for _, rank := range vehicles {
		result.Ranking = append(result.Ranking, *rank)
		result.FleetLatestMileageKm += rank.LatestMileageKm
	}
	sort.Slice(result.Ranking, func(i, j int) bool { return result.Ranking[i].MileageKm > result.Ranking[j].MileageKm })
	if result.VehicleCount > 0 {
		result.AverageMileagePerVIN = result.PeriodMileageKm / float64(result.VehicleCount)
	}
	if result.RecordCount > 0 {
		result.AverageDailyMileageKm = result.PeriodMileageKm / float64(result.RecordCount)
	}
	return result, nil
}

func (m *MockStore) dailyMileageRows(query url.Values) []DailyMileageRow {
	rows := []DailyMileageRow{
		{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Date: "2026-07-03", StartMileageKm: 48710.2, EndMileageKm: 48798.9, DailyMileageKm: 88.7, Source: "JT808"},
		{VIN: "LNXNEGRR7SR318212", Plate: "川AHTWO1", Date: "2026-07-03", StartMileageKm: 119820.4, EndMileageKm: 119925, DailyMileageKm: 104.6, Source: "GB32960"},
	}
	if vin := strings.TrimSpace(query.Get("vin")); vin != "" {
		keyword := strings.ToLower(vin)
		rows = keep(rows, func(row DailyMileageRow) bool {
			return strings.Contains(strings.ToLower(row.VIN+row.Plate), keyword)
		})
	}
	if raw := strings.TrimSpace(query.Get("vins")); raw != "" {
		selected := map[string]bool{}
		for _, vin := range strings.Split(raw, ",") {
			selected[strings.TrimSpace(vin)] = true
		}
		rows = keep(rows, func(row DailyMileageRow) bool { return selected[row.VIN] })
	}
	if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
		rows = keep(rows, func(row DailyMileageRow) bool { return row.Source == protocol })
	}
	return rows
}

func (m *MockStore) QualityIssues(_ context.Context, query url.Values) (Page[QualityIssueRow], error) {
	rows := mockQualityRows()
	if vin := strings.TrimSpace(query.Get("vin")); vin != "" {
		rows = keep(rows, func(row QualityIssueRow) bool { return row.VIN == vin })
	}
	if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
		rows = keep(rows, func(row QualityIssueRow) bool { return row.Protocol == protocol })
	}
	if issueType := strings.TrimSpace(query.Get("issueType")); issueType != "" {
		rows = keep(rows, func(row QualityIssueRow) bool { return row.IssueType == issueType })
	}
	return page(rows, query), nil
}

func (m *MockStore) QualitySummary(_ context.Context, query url.Values) (QualitySummary, error) {
	page, _ := m.QualityIssues(context.Background(), query)
	vehicles := map[string]struct{}{}
	protocols := map[string]int{}
	issueTypes := map[string]int{}
	summary := QualitySummary{IssueRecordCount: page.Total}
	for _, row := range page.Items {
		vehicles[firstNonEmpty(row.VIN, row.Plate, row.Detail)] = struct{}{}
		protocols[row.Protocol]++
		issueTypes[row.IssueType]++
		if row.Severity == "error" {
			summary.ErrorCount++
		} else {
			summary.WarningCount++
		}
	}
	summary.IssueVehicleCount = len(vehicles)
	summary.Protocols = bucketStats(protocols)
	summary.IssueTypes = bucketStats(issueTypes)
	return summary, nil
}

func mockQualityRows() []QualityIssueRow {
	return []QualityIssueRow{
		{VIN: "unknown", Plate: "", Phone: "13300000001", SourceEndpoint: "127.0.0.1:808", Protocol: "JT808", IssueType: "VIN_MISSING", Severity: "warning", LastSeen: "2026-07-03 19:58:00", Detail: "phone 13300000001 未命中 binding 表"},
		{VIN: "LNXNEGRR7SR318212", Plate: "川AHTWO1", Phone: "", SourceEndpoint: "Hyundai", Protocol: "GB32960", IssueType: "LINK_GAP", Severity: "error", LastSeen: "2026-07-03 18:20:00", Detail: "Hyundai 平台近 60 分钟无转发"},
		{VIN: "LB9NO_SOURCE001", Plate: "粤A无源1", Phone: "13300000002", SourceEndpoint: "vehicle_identity_binding", Protocol: "VEHICLE_SERVICE", IssueType: "NO_SOURCE", Severity: "warning", LastSeen: "", Detail: "车辆已绑定，但暂无 32960、808 或 MQTT 数据来源"},
	}
}

func bucketStats(values map[string]int) []QualityBucketStat {
	out := make([]QualityBucketStat, 0, len(values))
	for name, count := range values {
		out = append(out, QualityBucketStat{Name: name, Count: count})
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Count == out[j].Count {
			return out[i].Name < out[j].Name
		}
		return out[i].Count > out[j].Count
	})
	return out
}

func (m *MockStore) OpsHealth(context.Context) (OpsHealth, error) {
	summary, _ := m.DashboardSummary(context.Background())
	redisOnlineKeys := 92
	activeConnections := 120000
	capacityMetrics := CapacityMetrics{
		ActiveConnections:          activeConnections,
		KafkaLag:                   0,
		BridgeConsumerPending:      0,
		BridgeAckPending:           0,
		BridgeBatchPendingMessages: 0,
		FastWriterConsumerPending:  0,
		FastWriterAckPending:       0,
		FastWriterBatchPending:     0,
		HistoryBatchPending:        0,
		HistoryRowsPending:         0,
	}
	return OpsHealth{
		LinkHealth:        summary.LinkHealth,
		KafkaLag:          summary.KafkaLag,
		ActiveConnections: &activeConnections,
		CapacityMetrics:   capacityMetrics,
		RedisOnlineKeys:   &redisOnlineKeys,
		TDengineWritable:  true,
		MySQLWritable:     true,
	}, nil
}

func filterVehicles(rows []VehicleRow, query url.Values) []VehicleRow {
	keyword := strings.ToLower(strings.TrimSpace(query.Get("keyword")))
	protocol := strings.TrimSpace(query.Get("protocol"))
	if keyword == "" && protocol == "" {
		return rows
	}
	return keep(rows, func(row VehicleRow) bool {
		if protocol != "" && row.Protocol != protocol {
			return false
		}
		if keyword == "" {
			return true
		}
		value := strings.ToLower(row.VIN + row.Plate + row.Phone + row.OEM)
		return strings.Contains(value, keyword)
	})
}

func page[T any](rows []T, query url.Values) Page[T] {
	limit := parsePositive(query.Get("limit"), 20)
	offset := parsePositive(query.Get("offset"), 0)
	if offset > len(rows) {
		offset = len(rows)
	}
	end := offset + limit
	if end > len(rows) {
		end = len(rows)
	}
	return Page[T]{Items: rows[offset:end], Total: len(rows), Limit: limit, Offset: offset}
}

func keep[T any](rows []T, fn func(T) bool) []T {
	out := make([]T, 0, len(rows))
	for _, row := range rows {
		if fn(row) {
			out = append(out, row)
		}
	}
	return out
}

func containsString(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func sortVehicleCoverage(rows []VehicleCoverageRow) {
	sort.Slice(rows, func(i, j int) bool {
		if rows[i].LastSeen == rows[j].LastSeen {
			return rows[i].VIN < rows[j].VIN
		}
		return rows[i].LastSeen > rows[j].LastSeen
	})
}

func keepCoverageRow(row VehicleCoverageRow, query url.Values) bool {
	switch strings.TrimSpace(query.Get("coverage")) {
	case "single":
		if row.SourceCount != 1 {
			return false
		}
	case "multi":
		if row.SourceCount <= 1 {
			return false
		}
	}
	switch strings.TrimSpace(query.Get("online")) {
	case "online":
		if !row.Online {
			return false
		}
	case "offline":
		if row.Online {
			return false
		}
	}
	switch strings.TrimSpace(query.Get("bindingStatus")) {
	case "bound":
		if row.BindingStatus != "bound" {
			return false
		}
	case "unbound":
		if row.BindingStatus != "unbound" {
			return false
		}
	}
	if missingProtocol := strings.TrimSpace(query.Get("missingProtocol")); missingProtocol != "" {
		if containsString(row.Protocols, missingProtocol) {
			return false
		}
	}
	return keepServiceStatus(row.ServiceStatus, query.Get("serviceStatus"))
}

func keepServiceStatus(status *VehicleServiceStatus, raw string) bool {
	switch strings.TrimSpace(raw) {
	case "identity_required":
		return status != nil && status.Status == "identity_required"
	case "offline":
		return status != nil && status.Status == "offline"
	case "no_data":
		return status != nil && status.Status == "no_data"
	case "degraded":
		return status != nil && status.Status == "degraded"
	case "healthy":
		return status != nil && status.Status == "healthy"
	default:
		return true
	}
}

func copyValues(values url.Values) url.Values {
	next := url.Values{}
	for key, items := range values {
		next[key] = append([]string(nil), items...)
	}
	return next
}

func parsePositive(raw string, fallback int) int {
	value, err := strconv.Atoi(raw)
	if err != nil || value < 0 {
		return fallback
	}
	return value
}

func defaultString(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}
