package stats

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

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

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

func TestSourceKeyUsesProtocolDeviceAndSourceIP(t *testing.T) {
	got := SourceKey(envelope.ProtocolJT808, "13307765812", "", "115.231.168.135")
	if got != "JT808:13307765812@115.231.168.135" {
		t.Fatalf("source key = %q", got)
	}

	got = SourceKey(envelope.ProtocolGB32960, "", "iccid-1", "202.98.117.132")
	if got != "GB32960:iccid-1@202.98.117.132" {
		t.Fatalf("source key = %q", got)
	}
}

func TestSourceKeyForDirectUsesStableDeviceIdentity(t *testing.T) {
	first := SourceKeyForKind(envelope.ProtocolJT808, "13307765812", "", "115.231.168.135", "DIRECT")
	second := SourceKeyForKind(envelope.ProtocolJT808, "13307765812", "", "39.144.3.22", "DIRECT")
	if first != "JT808:13307765812@DIRECT" || second != first {
		t.Fatalf("direct source keys = %q/%q, want stable phone key", first, second)
	}

	fallback := SourceKeyForKind(envelope.ProtocolJT808, "", "", "39.144.3.22", "DIRECT")
	if fallback != "JT808:unknown@39.144.3.22" {
		t.Fatalf("direct fallback source key = %q", fallback)
	}
}

func TestSourceKeyForPlatformUsesStableSourceCode(t *testing.T) {
	first := SourceKeyForSource(envelope.ProtocolJT808, "41456413943", "", "117.132.194.167", "PLATFORM", "guangan_beidou")
	second := SourceKeyForSource(envelope.ProtocolJT808, "41456413943", "", "117.132.198.90", "PLATFORM", "guangan_beidou")
	if first != "JT808:41456413943@PLATFORM:guangan_beidou" || second != first {
		t.Fatalf("platform source keys = %q/%q, want stable source_code key", first, second)
	}

	withoutSourceCode := SourceKeyForSource(envelope.ProtocolJT808, "41456413943", "", "117.132.198.90", "PLATFORM", "")
	if withoutSourceCode != "JT808:41456413943@117.132.198.90" {
		t.Fatalf("platform fallback source key = %q", withoutSourceCode)
	}

	withoutDeviceIdentity := SourceKeyForSource(envelope.ProtocolJT808, "", "", "117.132.198.90", "PLATFORM", "guangan_beidou")
	if withoutDeviceIdentity != "JT808:unknown@117.132.198.90" {
		t.Fatalf("unknown platform source key = %q", withoutDeviceIdentity)
	}
}

func TestSourceMileageSampleFromMetricUsesDirectSourceKey(t *testing.T) {
	sample := MetricSample{
		VIN:            "LA9GG64L7PBAF4001",
		StatDate:       "2026-07-12",
		Protocol:       envelope.ProtocolJT808,
		Phone:          "13307765812",
		TotalMileageKM: 4123.9,
		EventTime:      time.Date(2026, 7, 12, 8, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
	}
	candidate := SourceMileageSampleFromMetric(sample, SourceIdentity{
		Protocol:       envelope.ProtocolJT808,
		SourceIP:       "115.231.168.135",
		SourceEndpoint: "115.231.168.135:43625",
		SourceKind:     "DIRECT",
	})
	if candidate.SourceKey != "JT808:13307765812@DIRECT" {
		t.Fatalf("source key = %q", candidate.SourceKey)
	}
	if candidate.SourceIP != "115.231.168.135" {
		t.Fatalf("source ip = %q", candidate.SourceIP)
	}
}

func TestSourceMileageSampleFromMetricUsesPlatformSourceCode(t *testing.T) {
	sample := MetricSample{
		VIN:            "LNXNEGRR0SR321372",
		StatDate:       "2026-07-12",
		Protocol:       envelope.ProtocolJT808,
		Phone:          "41456413943",
		TotalMileageKM: 46484.2,
		EventTime:      time.Date(2026, 7, 12, 8, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
	}
	candidate := SourceMileageSampleFromMetric(sample, SourceIdentity{
		Protocol:       envelope.ProtocolJT808,
		SourceIP:       "117.132.194.167",
		SourceEndpoint: "117.132.194.167:9806",
		SourceCode:     "guangan_beidou",
		PlatformName:   "广安北斗",
		SourceKind:     "PLATFORM",
	})
	if candidate.SourceKey != "JT808:41456413943@PLATFORM:guangan_beidou" {
		t.Fatalf("source key = %q", candidate.SourceKey)
	}
	if candidate.PlatformName != "广安北斗" {
		t.Fatalf("platform name = %q", candidate.PlatformName)
	}
}

func TestUpsertSourceMileageWritesCandidateRow(t *testing.T) {
	exec := &recordingExec{}
	sample := SourceMileageSample{
		VIN:             "LA9GG64L7PBAF4001",
		StatDate:        "2026-07-08",
		Protocol:        envelope.ProtocolJT808,
		SourceKey:       "JT808:13307765812@115.231.168.135",
		SourceIP:        "115.231.168.135",
		SourceEndpoint:  "115.231.168.135:20215",
		Phone:           "13307765812",
		FirstTotalKM:    4100.8,
		LatestTotalKM:   4123.9,
		DailyKM:         23.1,
		SampleCount:     2,
		FirstEventTime:  time.Date(2026, 7, 8, 0, 1, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
		LatestEventTime: time.Date(2026, 7, 8, 23, 59, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
		QualityStatus:   QualityOK,
		QualityReason:   "same_source_continuous",
	}
	if err := UpsertSourceMileage(context.Background(), exec, sample); err != nil {
		t.Fatalf("UpsertSourceMileage() error = %v", err)
	}
	if len(exec.calls) != 1 {
		t.Fatalf("exec calls = %d", len(exec.calls))
	}
	sql := exec.calls[0].query
	for _, want := range []string{
		"INSERT INTO vehicle_daily_mileage_source",
		"ON DUPLICATE KEY UPDATE",
		"daily_mileage_km = CASE",
		"VALUES(latest_event_time) >= latest_event_time",
		"VALUES(first_event_time) <= first_event_time",
		"quality_status = CASE",
		"THEN '" + QualityInvalidDelta + "'",
		"quality_reason = CASE",
		"THEN 'outside_daily_range'",
		"platform_name = COALESCE(NULLIF(TRIM(VALUES(platform_name)), ''), platform_name)",
	} {
		if !strings.Contains(sql, want) {
			t.Fatalf("candidate upsert missing %q: %s", want, sql)
		}
	}
	if got := exec.calls[0].args[0]; got != "LA9GG64L7PBAF4001" {
		t.Fatalf("first arg = %#v", got)
	}
}

func TestUpsertSourceMileageTruncatesSubsecondEventTimesAtDayBoundary(t *testing.T) {
	exec := &recordingExec{}
	loc := time.FixedZone("Asia/Shanghai", 8*3600)
	previous := time.Date(2026, 7, 12, 23, 59, 59, 999_000_000, loc)
	current := time.Date(2026, 7, 13, 0, 0, 1, 999_000_000, loc)
	sample := SourceMileageSample{
		VIN:             "LMRKH9AC7R1004098",
		StatDate:        "2026-07-13",
		Protocol:        envelope.ProtocolYutongMQTT,
		SourceKey:       "YUTONG_MQTT:LMRKH9AC7R1004098@PLATFORM:yutong",
		SourceIP:        "mqtt",
		FirstTotalKM:    41249,
		LatestTotalKM:   41250,
		DailyKM:         1,
		SampleCount:     1,
		FirstEventTime:  previous,
		LatestEventTime: current,
		QualityStatus:   QualityOK,
		QualityReason:   "historical_source_baseline",
	}
	if err := UpsertSourceMileage(context.Background(), exec, sample); err != nil {
		t.Fatalf("UpsertSourceMileage() error = %v", err)
	}
	if len(exec.calls) != 1 {
		t.Fatalf("exec calls = %d, want 1", len(exec.calls))
	}
	if got := exec.calls[0].args[13]; got != previous.Truncate(time.Second) {
		t.Fatalf("first event arg = %v, want %v", got, previous.Truncate(time.Second))
	}
	if got := exec.calls[0].args[14]; got != current.Truncate(time.Second) {
		t.Fatalf("latest event arg = %v, want %v", got, current.Truncate(time.Second))
	}
}

func TestUpsertSourceMileageUsesEventTimeBoundaries(t *testing.T) {
	for _, want := range []string{
		"first_total_mileage_km = CASE",
		"VALUES(first_event_time) <= first_event_time",
		"latest_total_mileage_km = CASE",
		"VALUES(latest_event_time) >= latest_event_time",
		"daily_mileage_km = CASE",
		"VALUES(latest_total_mileage_km)",
		"VALUES(first_total_mileage_km)",
	} {
		if !strings.Contains(upsertSourceMileageSQL, want) {
			t.Fatalf("event-time boundary upsert SQL missing %q:\n%s", want, upsertSourceMileageSQL)
		}
	}
	for _, forbidden := range []string{
		"GREATEST(latest_total_mileage_km, VALUES(latest_total_mileage_km))",
		"LEAST(first_total_mileage_km, VALUES(first_total_mileage_km))",
		"current_day_fallback_after_invalid_baseline",
	} {
		if strings.Contains(upsertSourceMileageSQL, forbidden) {
			t.Fatalf("event-time boundary upsert must not contain %q:\n%s", forbidden, upsertSourceMileageSQL)
		}
	}
}

func TestUpsertSourceMileageRechecksMergedDailyRange(t *testing.T) {
	if maxSelectedDailyMileageKMSQL != "2500" || maxNegativeMileageJitterKMSQL != "1" {
		t.Fatalf("SQL mileage limits drifted from Go quality constants: selected=%s jitter=%s", maxSelectedDailyMileageKMSQL, maxNegativeMileageJitterKMSQL)
	}
	for _, want := range []string{
		"quality_status = CASE",
		">= -" + maxNegativeMileageJitterKMSQL,
		"DATEDIFF(DATE(",
		"* " + maxSelectedDailyMileageKMSQL,
		"THEN '" + QualityInvalidDelta + "'",
		"quality_reason = CASE",
		"THEN 'outside_daily_range'",
		"THEN '" + QualityReasonCurrentDayFirst + "'",
		"ELSE '" + QualityReasonHistorical + "'",
	} {
		if !strings.Contains(upsertSourceMileageSQL, want) {
			t.Fatalf("merged range guard missing %q:\n%s", want, upsertSourceMileageSQL)
		}
	}
}

func TestPreviousSourceBaselineUsesLatestEarlierCalendarDay(t *testing.T) {
	if !strings.Contains(previousSourceBaselineSQL, "stat_date < ?") {
		t.Fatalf("previous baseline should search earlier calendar days:\n%s", previousSourceBaselineSQL)
	}
	if !strings.Contains(previousSourceBaselineSQL, "ORDER BY stat_date DESC, latest_event_time DESC") {
		t.Fatalf("previous baseline should prefer the nearest earlier sample:\n%s", previousSourceBaselineSQL)
	}
	if strings.Contains(previousSourceBaselineSQL, "quality_status =") {
		t.Fatalf("previous day's last odometer must remain usable even when that day's delta has no baseline:\n%s", previousSourceBaselineSQL)
	}
	for _, want := range []string{
		"latest_total_mileage_km > 0",
		"latest_event_time >= CAST(CONCAT(stat_date, ' 00:00:00') AS DATETIME)",
		"latest_event_time < DATE_ADD(CAST(CONCAT(stat_date, ' 00:00:00') AS DATETIME), INTERVAL 1 DAY)",
	} {
		if !strings.Contains(previousSourceBaselineSQL, want) {
			t.Fatalf("previous baseline SQL missing %q:\n%s", want, previousSourceBaselineSQL)
		}
	}
}

func TestDailyMileageFromDayBoundaryUsesCurrentMinusPrevious(t *testing.T) {
	got := DailyMileageFromDayBoundary(14989.0, 15369.0)
	if got != 380.0 {
		t.Fatalf("daily mileage = %v, want current latest minus previous last = 380", got)
	}
}

func TestUpsertSourceMileageSkipsBlankSourceIP(t *testing.T) {
	exec := &recordingExec{}
	sample := SourceMileageSample{
		VIN:           "LA9GG64L7PBAF4001",
		StatDate:      "2026-07-08",
		Protocol:      envelope.ProtocolJT808,
		SourceKey:     "JT808:13307765812@",
		SourceIP:      "   ",
		Phone:         "13307765812",
		QualityStatus: QualityOK,
	}
	if err := UpsertSourceMileage(context.Background(), exec, sample); err != nil {
		t.Fatalf("UpsertSourceMileage() error = %v", err)
	}
	if len(exec.calls) != 0 {
		t.Fatalf("exec calls = %d, want 0", len(exec.calls))
	}
}

func TestNormalizePlatformSourceMileageMergesLegacyIPKeys(t *testing.T) {
	exec := &recordingExec{}
	err := NormalizePlatformSourceMileage(context.Background(), exec, "LNXNEGRR0SR321372", "2026-07-12", envelope.ProtocolJT808)
	if err != nil {
		t.Fatalf("NormalizePlatformSourceMileage() error = %v", err)
	}
	if len(exec.calls) != 2 {
		t.Fatalf("exec calls = %d, want insert merge + delete legacy", len(exec.calls))
	}
	insertSQL := exec.calls[0].query
	for _, want := range []string{
		"INSERT INTO vehicle_daily_mileage_source",
		"CONCAT(s.protocol, ':', COALESCE(NULLIF(TRIM(s.phone), ''), NULLIF(TRIM(s.device_id), '')), '@PLATFORM:', COALESCE(NULLIF(TRIM(ds.source_code), ''), vi.source_code))",
		"LEFT JOIN vehicle_data_source ds",
		"LEFT JOIN (",
		"FROM vehicle_identifier",
		"identifier_type = 'JT808_PHONE'",
		"HAVING COUNT(DISTINCT TRIM(source_code)) = 1",
		"ds.source_kind = 'PLATFORM'",
		"MAX(TRIM(COALESCE(ds.platform_name, vi.platform_name)))",
		"s.source_key <> CONCAT",
		"GROUP BY s.vin, s.stat_date, s.protocol, stable_source_key",
		"ON DUPLICATE KEY UPDATE",
		"sample_count = sample_count + VALUES(sample_count)",
		"s.quality_status IN ('" + QualityOK + "', '" + QualityNoPreviousBaseline + "')",
		"WHEN MIN(s.first_event_time) < CAST(CONCAT(s.stat_date, ' 00:00:00') AS DATETIME)",
		"THEN '" + QualityInvalidDelta + "'",
		"ELSE '" + QualityOK + "'",
		"THEN 'negative_jitter_clamped'",
		"THEN '" + QualityReasonHistorical + "'",
		"ELSE '" + QualityReasonCurrentDayFirst + "'",
		upsertSourceMergedMissingPreviousBaselineSQL,
	} {
		if !strings.Contains(insertSQL, want) {
			t.Fatalf("normalize insert SQL missing %q:\n%s", want, insertSQL)
		}
	}
	deleteSQL := exec.calls[1].query
	for _, want := range []string{
		"DELETE s",
		"FROM vehicle_daily_mileage_source s",
		"LEFT JOIN vehicle_data_source ds",
		"FROM vehicle_identifier",
		"ds.source_kind = 'PLATFORM'",
		"s.source_key <> CONCAT",
	} {
		if !strings.Contains(deleteSQL, want) {
			t.Fatalf("normalize delete SQL missing %q:\n%s", want, deleteSQL)
		}
	}
	for i, call := range exec.calls {
		if len(call.args) != 3 || call.args[0] != "LNXNEGRR0SR321372" || call.args[1] != "2026-07-12" || call.args[2] != "JT808" {
			t.Fatalf("call %d args = %#v", i, call.args)
		}
	}
}

func TestNormalizePlatformSourceMileageForDateNormalizesAndProjectsLegacyVINs(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()

	statDate := "2026-07-12"
	protocol := envelope.ProtocolJT808
	rows := sqlmock.NewRows([]string{"vin"}).
		AddRow("LA9HE60A0PBAF4002").
		AddRow("LA9HE60A1PBAF4008")
	mock.ExpectQuery(selectPlatformSourceMileageLegacyVINSQL).
		WithArgs(statDate, string(protocol)).
		WillReturnRows(rows)
	for _, vin := range []string{"LA9HE60A0PBAF4002", "LA9HE60A1PBAF4008"} {
		mock.ExpectExec(normalizePlatformSourceMileageInsertSQL).
			WithArgs(vin, statDate, string(protocol)).
			WillReturnResult(sqlmock.NewResult(0, 1))
		mock.ExpectExec(normalizePlatformSourceMileageDeleteSQL).
			WithArgs(vin, statDate, string(protocol)).
			WillReturnResult(sqlmock.NewResult(0, 1))
		expectProjectDailyMileageSQL(mock, vin, statDate, protocol)
		mock.ExpectCommit()
	}

	normalized, err := NormalizePlatformSourceMileageForDate(context.Background(), db, statDate, protocol)
	if err != nil {
		t.Fatalf("NormalizePlatformSourceMileageForDate() error = %v", err)
	}
	if normalized != 2 {
		t.Fatalf("normalized = %d, want 2", normalized)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations were not met: %v", err)
	}
}

func TestShouldNormalizePlatformSourceMileage(t *testing.T) {
	if !ShouldNormalizePlatformSourceMileage(SourceMileageSample{SourceKey: "JT808:41456413943@PLATFORM:guangan_beidou"}) {
		t.Fatal("platform source key should trigger legacy key normalization")
	}
	for _, sourceKey := range []string{"JT808:41456413943@DIRECT", "JT808:41456413943@117.132.194.167", ""} {
		if ShouldNormalizePlatformSourceMileage(SourceMileageSample{SourceKey: sourceKey}) {
			t.Fatalf("source key %q should not trigger platform normalization", sourceKey)
		}
	}
}

func TestApplyMileageQualityRulesClampsSmallNegativeJitter(t *testing.T) {
	sample := SourceMileageSample{
		DailyKM:       -0.1,
		QualityStatus: QualityOK,
		QualityReason: "historical_source_baseline",
	}
	ApplyMileageQualityRules(&sample)
	if sample.DailyKM != 0 {
		t.Fatalf("daily km = %v, want 0", sample.DailyKM)
	}
	if sample.QualityStatus != QualityOK || sample.QualityReason != "negative_jitter_clamped" {
		t.Fatalf("quality = %s/%s", sample.QualityStatus, sample.QualityReason)
	}
}

func TestApplyMileageQualityRulesRejectsLargeNegativeDelta(t *testing.T) {
	sample := SourceMileageSample{
		DailyKM:       -55,
		QualityStatus: QualityOK,
		QualityReason: "historical_source_baseline",
	}
	ApplyMileageQualityRules(&sample)
	if sample.DailyKM != -55 {
		t.Fatalf("daily km = %v, want original invalid delta", sample.DailyKM)
	}
	if sample.QualityStatus != QualityInvalidDelta || sample.QualityReason != "outside_daily_range" {
		t.Fatalf("quality = %s/%s", sample.QualityStatus, sample.QualityReason)
	}
}

func TestApplyMileageQualityRulesAcceptsPlausibleMultiDayFallbackDelta(t *testing.T) {
	loc := time.FixedZone("Asia/Shanghai", 8*3600)
	sample := SourceMileageSample{
		DailyKM:         7833.5,
		FirstEventTime:  time.Date(2026, 7, 4, 11, 7, 58, 0, loc),
		LatestEventTime: time.Date(2026, 7, 12, 2, 52, 47, 0, loc),
		QualityStatus:   QualityOK,
		QualityReason:   "historical_source_baseline",
	}
	ApplyMileageQualityRules(&sample)
	if sample.QualityStatus != QualityOK || sample.QualityReason != "historical_source_baseline" {
		t.Fatalf("quality = %s/%s", sample.QualityStatus, sample.QualityReason)
	}
	if sample.DailyKM != 7833.5 {
		t.Fatalf("daily km = %v", sample.DailyKM)
	}
	if got := MileageQualityWindowDays(sample.FirstEventTime, sample.LatestEventTime); got != 8 {
		t.Fatalf("window days = %d, want 8", got)
	}
	if got := MileageQualityLimitKM(sample.FirstEventTime, sample.LatestEventTime); got != 8*maxSelectedDailyMileageKM {
		t.Fatalf("quality limit = %d, want 8-day limit %d", got, 8*maxSelectedDailyMileageKM)
	}
}

func TestApplyMileageQualityRulesAcceptsContinuousHighUtilizationDay(t *testing.T) {
	loc := time.FixedZone("Asia/Shanghai", 8*3600)
	sample := SourceMileageSample{
		DailyKM:         1895.7,
		FirstEventTime:  time.Date(2026, 7, 12, 23, 59, 37, 0, loc),
		LatestEventTime: time.Date(2026, 7, 13, 20, 32, 42, 0, loc),
		QualityStatus:   QualityOK,
		QualityReason:   QualityReasonHistorical,
	}
	ApplyMileageQualityRules(&sample)
	if sample.QualityStatus != QualityOK || sample.QualityReason != QualityReasonHistorical {
		t.Fatalf("quality = %s/%s", sample.QualityStatus, sample.QualityReason)
	}
	if got := MileageQualityLimitKM(sample.FirstEventTime, sample.LatestEventTime); got != 2500 {
		t.Fatalf("quality limit = %d, want 2500", got)
	}
}

func TestApplyMileageQualityRulesRejectsImplausibleSingleDayJump(t *testing.T) {
	loc := time.FixedZone("Asia/Shanghai", 8*3600)
	sample := SourceMileageSample{
		DailyKM:         4000,
		FirstEventTime:  time.Date(2026, 7, 12, 23, 59, 37, 0, loc),
		LatestEventTime: time.Date(2026, 7, 13, 20, 32, 42, 0, loc),
		QualityStatus:   QualityOK,
		QualityReason:   QualityReasonHistorical,
	}
	ApplyMileageQualityRules(&sample)
	if sample.QualityStatus != QualityInvalidDelta || sample.QualityReason != "outside_daily_range" {
		t.Fatalf("quality = %s/%s", sample.QualityStatus, sample.QualityReason)
	}
}

func TestProjectDailyMileageSelectsCandidateAndMarksSource(t *testing.T) {
	exec := &recordingExec{}
	err := ProjectDailyMileage(context.Background(), exec, "LA9GG64L7PBAF4001", "2026-07-08", envelope.ProtocolJT808)
	if err != nil {
		t.Fatalf("ProjectDailyMileage() error = %v", err)
	}
	if len(exec.calls) != 3 {
		t.Fatalf("exec calls = %d, want 3", len(exec.calls))
	}
	projectFinal := exec.calls[0].query
	markSelected := exec.calls[1].query
	cleanupFinal := exec.calls[2].query
	for _, want := range []string{
		"INSERT INTO vehicle_daily_mileage",
		"FROM vehicle_daily_mileage_source s",
		"LEFT JOIN vehicle_data_source ds",
		"ds.id",
		"CASE COALESCE(NULLIF(NULLIF(TRIM(ds.source_kind), ''), 'UNKNOWN'), CASE WHEN s.source_key LIKE '%@PLATFORM:%' THEN 'PLATFORM' WHEN s.source_key LIKE '%@DIRECT' THEN 'DIRECT' ELSE 'UNKNOWN' END)",
		"WHEN 'PLATFORM' THEN 0",
		"WHEN 'DIRECT' THEN 1",
		"WHEN 'UNKNOWN' THEN 2",
		"ds.trust_priority",
		"s.quality_status = '" + QualityOK + "'",
		"COALESCE(NULLIF(TRIM(ds.source_kind), ''), 'UNKNOWN') = 'UNKNOWN'",
		"AND (ds.source_code IS NULL OR TRIM(ds.source_code) = '')",
		"AND (ds.platform_name IS NULL OR TRIM(ds.platform_name) = '')",
		"s.daily_mileage_km BETWEEN 0 AND",
		"DATEDIFF(DATE(s.latest_event_time), DATE(s.first_event_time))",
	} {
		if !strings.Contains(projectFinal, want) {
			t.Fatalf("project query missing %q: %s", want, projectFinal)
		}
	}
	for _, forbidden := range []string{
		"trusted_source_key",
		"trusted_phone",
		"trusted_source_endpoint",
		"first_total_mileage_km",
		"sample_count)",
		"ds.latest_source_endpoint",
	} {
		if strings.Contains(projectFinal, forbidden) {
			t.Fatalf("project query should not use final-table field %q: %s", forbidden, projectFinal)
		}
	}
	if !strings.Contains(markSelected, "UPDATE vehicle_daily_mileage_source s") ||
		!strings.Contains(markSelected, "LEFT JOIN (") ||
		!strings.Contains(markSelected, "SET s.is_selected = CASE WHEN selected_source.source_key IS NULL THEN 0 ELSE 1 END") {
		t.Fatalf("mark query should reconcile the elected source: %s", markSelected)
	}
	if strings.Contains(markSelected, "JOIN vehicle_daily_mileage m") {
		t.Fatalf("mark query should not rejoin final mileage for candidate selection: %s", markSelected)
	}
	for _, want := range []string{
		"JOIN (",
		"SELECT",
		"s2.source_key",
		"s2.quality_status = '" + QualityOK + "'",
		"s2.daily_mileage_km BETWEEN 0 AND",
		"DATEDIFF(DATE(s2.latest_event_time), DATE(s2.first_event_time))",
		"COALESCE(NULLIF(TRIM(ds.source_kind), ''), 'UNKNOWN') = 'UNKNOWN'",
		"AND (ds.source_code IS NULL OR TRIM(ds.source_code) = '')",
		"AND (ds.platform_name IS NULL OR TRIM(ds.platform_name) = '')",
		"CASE COALESCE(NULLIF(NULLIF(TRIM(ds.source_kind), ''), 'UNKNOWN'), CASE WHEN s2.source_key LIKE '%@PLATFORM:%' THEN 'PLATFORM' WHEN s2.source_key LIKE '%@DIRECT' THEN 'DIRECT' ELSE 'UNKNOWN' END)",
		"WHEN 'PLATFORM' THEN 0",
		"WHEN 'DIRECT' THEN 1",
		"WHEN 'UNKNOWN' THEN 2",
		"ds.trust_priority",
		"LIMIT 1",
	} {
		if !strings.Contains(markSelected, want) {
			t.Fatalf("mark query missing %q: %s", want, markSelected)
		}
	}
	if len(exec.calls[1].args) != 7 {
		t.Fatalf("mark query args = %d, want 7", len(exec.calls[1].args))
	}
	for _, forbidden := range []string{
		"WHEN 'UNKNOWN' THEN 1",
		"WHEN 'DIRECT' THEN 2",
	} {
		if strings.Contains(projectFinal, forbidden) || strings.Contains(markSelected, forbidden) {
			t.Fatalf("source selection should prefer classified DIRECT before UNKNOWN, found %q", forbidden)
		}
	}
	if got := exec.calls[1].args[3]; got != maxSelectedDailyMileageKM {
		t.Fatalf("mark query max mileage arg = %#v, want %d", got, maxSelectedDailyMileageKM)
	}
	for _, want := range []string{
		"DELETE FROM vehicle_daily_mileage",
		"NOT EXISTS (",
		"FROM vehicle_daily_mileage_source",
		"is_selected = 1",
	} {
		if !strings.Contains(cleanupFinal, want) {
			t.Fatalf("cleanup query missing %q: %s", want, cleanupFinal)
		}
	}
	if len(exec.calls[2].args) != 6 {
		t.Fatalf("cleanup query args = %d, want 6", len(exec.calls[2].args))
	}
}

func TestProjectDailyMileageCommitsTransactionForSQLDB(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()

	vin := "LA9GG64L7PBAF4001"
	statDate := "2026-07-08"
	protocol := envelope.ProtocolJT808
	expectProjectDailyMileageSQL(mock, vin, statDate, protocol)
	mock.ExpectCommit()

	if err := ProjectDailyMileage(context.Background(), db, vin, statDate, protocol); err != nil {
		t.Fatalf("ProjectDailyMileage() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations were not met: %v", err)
	}
}

func TestProjectDailyMileageRollsBackTransactionOnFailure(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
	if err != nil {
		t.Fatalf("sqlmock.New() error = %v", err)
	}
	defer db.Close()

	vin := "LA9GG64L7PBAF4001"
	statDate := "2026-07-08"
	protocol := envelope.ProtocolJT808
	wantErr := errors.New("mark selected failed")
	mock.ExpectBegin()
	mock.ExpectExec(projectDailyMileageSQL).
		WithArgs(vin, statDate, string(protocol), maxSelectedDailyMileageKM).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(markSelectedSourceSQL).
		WithArgs(vin, statDate, string(protocol), maxSelectedDailyMileageKM, vin, statDate, string(protocol)).
		WillReturnError(wantErr)
	mock.ExpectRollback()

	err = ProjectDailyMileage(context.Background(), db, vin, statDate, protocol)
	if !errors.Is(err, wantErr) {
		t.Fatalf("ProjectDailyMileage() error = %v, want %v", err, wantErr)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations were not met: %v", err)
	}
}

func expectProjectDailyMileageSQL(mock sqlmock.Sqlmock, vin string, statDate string, protocol envelope.Protocol) {
	mock.ExpectBegin()
	mock.ExpectExec(projectDailyMileageSQL).
		WithArgs(vin, statDate, string(protocol), maxSelectedDailyMileageKM).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(markSelectedSourceSQL).
		WithArgs(vin, statDate, string(protocol), maxSelectedDailyMileageKM, vin, statDate, string(protocol)).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(cleanupProjectedDailyMileageSQL).
		WithArgs(vin, statDate, string(protocol), vin, statDate, string(protocol)).
		WillReturnResult(sqlmock.NewResult(0, 1))
}
