package platform

import (
	"context"
	"testing"
	"time"

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

func TestAccessSummaryUsesDynamicFreshnessAndDelay(t *testing.T) {
	service := NewService(NewMockStore())
	summary, err := service.AccessSummary(context.Background(), AccessQuery{})
	if err != nil {
		t.Fatalf("AccessSummary returned error: %v", err)
	}
	if summary.TotalVehicles != 6 || summary.OnlineVehicles != 2 || summary.OfflineVehicles != 2 {
		t.Fatalf("unexpected access states: %+v", summary)
	}
	if summary.NeverReported != 1 || summary.UnknownVehicles != 1 || summary.LongOfflineVehicles != 2 {
		t.Fatalf("summary must distinguish never/offline/unknown: %+v", summary)
	}
	if summary.DelayAbnormal != 1 || summary.ThresholdVersion != 1 {
		t.Fatalf("summary must expose dynamic delay and threshold version: %+v", summary)
	}
	if summary.HealthyVehicles != 0 || summary.IncompleteVehicles != 3 || summary.DegradedVehicles != 0 {
		t.Fatalf("connection-state counters must be mutually exclusive: %+v", summary)
	}
}

func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) {
	service := NewService(NewMockStore())
	page, err := service.AccessVehicles(context.Background(), AccessQuery{OnlineState: "never_reported", Limit: 10})
	if err != nil {
		t.Fatalf("AccessVehicles returned error: %v", err)
	}
	if page.Total != 1 || len(page.Items) != 1 || page.Items[0].OnlineState != "never_reported" {
		t.Fatalf("unexpected never-reported page: %+v", page)
	}
	if page.Items[0].FirstSeenAt != "" || page.Items[0].FirstSeenEvidence == "" || page.Items[0].ReportIntervalProof == "" {
		t.Fatalf("missing source evidence must stay explicit: %+v", page.Items[0])
	}

	delayed, err := service.AccessVehicles(context.Background(), AccessQuery{DelayState: "abnormal", Limit: 10})
	if err != nil || delayed.Total != 1 || delayed.Items[0].DataDelaySec == nil || *delayed.Items[0].DataDelaySec != 45 {
		t.Fatalf("delay filter should use event/receive difference: page=%+v err=%v", delayed, err)
	}

	attention, err := service.AccessVehicles(context.Background(), AccessQuery{ConnectionState: "attention", Limit: 20})
	if err != nil || attention.Total != 6 {
		t.Fatalf("attention filter should return every vehicle with an access difference: page=%+v err=%v", attention, err)
	}
}

func TestAccessVehiclesSupportsModelProviderAndTimeFilters(t *testing.T) {
	service := NewService(NewMockStore())
	page, err := service.AccessVehicles(t.Context(), AccessQuery{Model: "氢燃料", Provider: "车厂", LatestSeenFrom: time.Now().Add(-time.Minute).Format("2006-01-02T15:04"), Limit: 10})
	if err != nil || page.Total != 1 || len(page.Items) != 1 || page.Items[0].VIN != "LNXNEGRR7SR318212" {
		t.Fatalf("advanced access filters failed: page=%+v err=%v", page, err)
	}
	_, err = service.AccessVehicles(t.Context(), AccessQuery{FirstSeenFrom: "not-a-time"})
	clientErr, ok := asClientError(err)
	if !ok || clientErr.Code != "ACCESS_TIME_INVALID" {
		t.Fatalf("invalid access time must be rejected, got %v", err)
	}
	_, err = service.AccessSummary(t.Context(), AccessQuery{LatestSeenFrom: "2026-07-14T09:00", LatestSeenTo: "2026-07-14T08:00"})
	clientErr, ok = asClientError(err)
	if !ok || clientErr.Code != "ACCESS_TIME_RANGE_INVALID" {
		t.Fatalf("reversed access time range must be rejected, got %v", err)
	}
}

func TestAccessThresholdUpdateUsesOptimisticVersion(t *testing.T) {
	store := NewMockStore()
	service := NewService(store)
	updated, err := service.UpdateAccessThresholds(context.Background(), AccessThresholdUpdate{
		Version: 1, DefaultThresholdSec: 600, DelayThresholdSec: 60, LongOfflineSec: 3600,
		Protocols: []AccessProtocolThreshold{{Protocol: "JT808", ThresholdSec: 300}}, Actor: "tester",
	})
	if err != nil {
		t.Fatalf("UpdateAccessThresholds returned error: %v", err)
	}
	if updated.Version != 2 || updated.UpdatedBy != "tester" || len(updated.Audit) != 1 {
		t.Fatalf("threshold update should version and audit: %+v", updated)
	}
	_, err = service.UpdateAccessThresholds(context.Background(), AccessThresholdUpdate{
		Version: 1, DefaultThresholdSec: 600, DelayThresholdSec: 60, LongOfflineSec: 3600,
	})
	clientErr, ok := asClientError(err)
	if !ok || clientErr.Code != "ACCESS_THRESHOLD_VERSION_CONFLICT" {
		t.Fatalf("stale update should conflict, got %v", err)
	}
}

func TestAccessVehicleCarriesDurableProjectionEvidenceAndMasterData(t *testing.T) {
	service := NewService(NewMockStore())
	page, err := service.AccessVehicles(context.Background(), AccessQuery{Keyword: "LB9A32A24R0LS1426", Limit: 10})
	if err != nil || len(page.Items) != 1 {
		t.Fatalf("access vehicle query failed: page=%+v err=%v", page, err)
	}
	row := page.Items[0]
	if row.Company != "岭牛示范车队" || row.FirstSeenSource != "live_writer" || row.ReportSampleCount != 120 || row.ReportIntervalSec == nil {
		t.Fatalf("durable access evidence/master data missing: %+v", row)
	}
}

func TestAccessVehiclesKeywordMatchesFleetAndModel(t *testing.T) {
	service := NewService(NewMockStore())
	fleet, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "岭牛示范车队", Limit: 20})
	if err != nil || fleet.Total != 1 || fleet.Items[0].VIN != "LB9A32A24R0LS1426" {
		t.Fatalf("fleet keyword should find its vehicles: page=%+v err=%v", fleet, err)
	}
	model, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "氢燃料车型", Limit: 20})
	if err != nil || model.Total != 1 || model.Items[0].VIN != "LNXNEGRR7SR318212" {
		t.Fatalf("model keyword should find its vehicles: page=%+v err=%v", model, err)
	}
}

func TestAccessVehicleGroupsCanonicalProtocolsByMasterVehicle(t *testing.T) {
	store := NewMockStore()
	service := NewService(store)
	page, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "LB9A32A24R0LS1426", Limit: 10})
	if err != nil || len(page.Items) != 1 {
		t.Fatalf("access vehicle query failed: page=%+v err=%v", page, err)
	}
	row := page.Items[0]
	if len(row.ExpectedProtocols) != 3 || len(row.ProtocolStatuses) != 3 {
		t.Fatalf("vehicle must expose three canonical protocol slots: %+v", row)
	}
	if len(row.ActualProtocols) != 1 || row.ActualProtocols[0] != "JT808" || len(row.MissingProtocols) != 2 {
		t.Fatalf("actual and missing protocols are incorrect: %+v", row)
	}
	if row.ConnectionState != "incomplete" || row.OnlineState != "online" {
		t.Fatalf("single online source should be an online but incomplete vehicle: %+v", row)
	}
	for _, status := range row.ProtocolStatuses {
		if status.Protocol == "JT808" && (!status.Connected || status.LatestReceivedAt == "") {
			t.Fatalf("connected protocol lost evidence: %+v", status)
		}
	}
}

func TestProductionAccessEvidenceReadsDurableWriterProjection(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewProductionStore(db, nil, "")
	mock.ExpectQuery("(?s)SELECT.*access_first_seen_at.*access_latest_received_at.*FROM.*SELECT DISTINCT vin FROM vehicle_identity_binding.*vehicle_profile.*vehicle_realtime_snapshot").
		WithArgs(accessEvidenceLimit + 1).
		WillReturnRows(sqlmock.NewRows([]string{
			"vin", "plate", "oem", "model_name", "company_name", "protocol", "provider", "first_seen_at", "first_seen_source",
			"event_time", "received_at", "updated_at", "report_interval_ms", "report_sample_count", "event_id", "longitude", "latitude", "speed_kmh", "soc_percent", "total_mileage_km", "daily_mileage_km",
		}).AddRow("VIN001", "粤A1", "示范厂家", "纯电客车", "示范公交", "GB32960", "车厂平台", "2026-07-14 05:00:00.000", "snapshot_backfill", "2026-07-14 05:01:00.000", "2026-07-14 05:01:30.500", "2026-07-14 05:01:30.500", int64(30500), int64(3), "evt-3", 113.1, 23.1, 32.5, 76.0, 12000.5, 38.6))
	rows, err := store.AccessEvidence(context.Background())
	if err != nil || len(rows) != 1 {
		t.Fatalf("AccessEvidence rows=%+v err=%v", rows, err)
	}
	row := rows[0]
	if row.ReportIntervalSec == nil || *row.ReportIntervalSec != 31 || row.ReportSampleCount != 3 || row.Model != "纯电客车" || row.Company != "示范公交" {
		t.Fatalf("unexpected durable projection row: %+v", row)
	}
	if row.SpeedKmh != 32.5 || row.TotalMileageKm != 12000.5 || row.DailyMileageKm != 38.6 {
		t.Fatalf("realtime source metrics missing: %+v", row)
	}
	if row.FirstSeenEvidence != "上线基线：由实时快照回填（非历史首次接入）" || row.ReportIntervalProof == "" {
		t.Fatalf("projection evidence boundary missing: %+v", row)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestAccessUnresolvedIdentitiesRemainMaskedAndProtocolScoped(t *testing.T) {
	service := NewService(NewMockStore())
	page, err := service.AccessUnresolvedIdentities(t.Context(), AccessUnresolvedIdentityQuery{Protocol: "jt808", Limit: 20})
	if err != nil || page.Total != 1 || len(page.Items) != 1 {
		t.Fatalf("unresolved identity queue failed: page=%+v err=%v", page, err)
	}
	if page.Items[0].IdentifierMasked != "138****0001" || page.Items[0].IssueCode != "missing_vin_jt808" || page.Items[0].RecommendedAction == "" {
		t.Fatalf("unresolved evidence is not actionable and masked: %+v", page.Items[0])
	}
	empty, err := service.AccessUnresolvedIdentities(t.Context(), AccessUnresolvedIdentityQuery{Protocol: "GB32960"})
	if err != nil || empty.Total != 0 || len(empty.Items) != 0 {
		t.Fatalf("non-JT808 unresolved query must be empty: page=%+v err=%v", empty, err)
	}
}

func TestProductionUnresolvedIdentityQueueExcludesBoundPhonesAndNeverReturnsRawIdentifier(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	store := NewProductionStore(db, nil, "")
	mock.ExpectQuery(`(?s)SELECT COUNT\(\*\).*jt808_registration.*COALESCE.*b\.vin`).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
	mock.ExpectQuery(`(?s)SELECT.*SHA2\(CONCAT\('JT808:',r\.phone\),256\).*CHAR_LENGTH\(r\.phone\).*missing_vin_jt808.*ORDER BY r\.latest_seen_at`).WithArgs(20, 0).WillReturnRows(sqlmock.NewRows([]string{
		"id", "protocol", "identifier_masked", "plate", "manufacturer", "source_endpoint", "first_registered_at", "latest_registered_at", "latest_authenticated_at", "latest_seen_at", "freshness_sec", "issue_code", "recommended_action",
	}).AddRow("hash-id", "JT808", "410****3543", "", "示范终端", "gateway-a", "", "", "", "2026-07-14 07:52:09", 20, "missing_vin_jt808", "核对后绑定"))
	page, err := store.AccessUnresolvedIdentities(t.Context(), AccessUnresolvedIdentityQuery{Limit: 20})
	if err != nil || page.Total != 1 || len(page.Items) != 1 {
		t.Fatalf("production unresolved query failed: page=%+v err=%v", page, err)
	}
	if page.Items[0].ID != "hash-id" || page.Items[0].IdentifierMasked != "410****3543" || page.Items[0].FreshnessSec != 20 {
		t.Fatalf("production unresolved evidence lost: %+v", page.Items[0])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
