package user_test

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"hyapp/pkg/appcode"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	userdomain "hyapp/services/user-service/internal/domain/user"
	userservice "hyapp/services/user-service/internal/service/user"
	"hyapp/services/user-service/internal/testutil/mysqltest"
)

func TestAdminChangeUserCountryOverridesProtectedRoleAndWritesOutbox(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	repository := mysqltest.NewRepository(t)
	repository.PutCountry(userdomain.Country{CountryCode: "US", Enabled: true})
	repository.PutCountry(userdomain.Country{CountryCode: "TH", Enabled: true})
	repository.PutRegion(userdomain.Region{RegionID: 101, RegionCode: "US-REGION", Name: "US Region", Countries: []string{"US"}})
	repository.PutRegion(userdomain.Region{RegionID: 202, RegionCode: "TH-REGION", Name: "TH Region", Countries: []string{"TH"}})
	repository.PutUser(userdomain.User{
		UserID:               91001,
		DefaultDisplayUserID: "91001",
		CurrentDisplayUserID: "91001",
		Country:              "US",
		RegionID:             101,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	repository.PutHostProfile(hostdomain.HostProfile{UserID: 91001, RegionID: 101, Status: hostdomain.HostStatusActive})
	if _, err := repository.RawDB().ExecContext(ctx, `
		INSERT INTO auth_sessions (
			app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms,
			revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
		)
		VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
	`, appcode.Default, "sess_country_admin", int64(91001), "refresh_hash_country_admin", "device_country_admin", int64(1_700_100_000_000), int64(1), int64(1)); err != nil {
		t.Fatalf("seed auth session failed: %v", err)
	}
	svc := userservice.New(repository,
		userservice.WithCountryRegionRepository(repository),
		userservice.WithClock(func() time.Time { return time.UnixMilli(1_700_000_123_000).UTC() }),
	)

	updated, nextAllowedAt, err := svc.AdminChangeUserCountry(ctx, 91001, "TH", "req-admin-country")
	if err != nil {
		t.Fatalf("AdminChangeUserCountry failed: %v", err)
	}
	if nextAllowedAt != 0 || updated.Country != "TH" || updated.RegionID != 202 {
		t.Fatalf("admin country result mismatch: user=%+v next=%d", updated, nextAllowedAt)
	}

	var payloadJSON string
	if err := repository.RawDB().QueryRowContext(ctx, `
		SELECT CAST(payload_json AS CHAR)
		FROM user_outbox
		WHERE app_code = ? AND event_type = 'UserRegionChanged' AND aggregate_id = ?
	`, appcode.Default, int64(91001)).Scan(&payloadJSON); err != nil {
		t.Fatalf("query UserRegionChanged outbox failed: %v", err)
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
		t.Fatalf("decode outbox payload failed: %v", err)
	}
	if payload["source"] != "admin" || int64(payload["old_region_id"].(float64)) != 101 || int64(payload["new_region_id"].(float64)) != 202 {
		t.Fatalf("outbox payload mismatch: %v", payload)
	}
	var revokedAtMs int64
	var revokedReason string
	var revokedRequestID string
	var revokedBy string
	if err := repository.RawDB().QueryRowContext(ctx, `
		SELECT revoked_at_ms, revoked_reason, revoked_request_id, revoked_by
		FROM auth_sessions
		WHERE app_code = ? AND session_id = ?
	`, appcode.Default, "sess_country_admin").Scan(&revokedAtMs, &revokedReason, &revokedRequestID, &revokedBy); err != nil {
		t.Fatalf("query revoked auth session failed: %v", err)
	}
	if revokedAtMs != 1_700_000_123_000 || revokedReason != "USER_COUNTRY_CHANGED" || revokedRequestID != "req-admin-country" || revokedBy != "country_change" {
		t.Fatalf("country change session revoke mismatch: at=%d reason=%q request=%q by=%q", revokedAtMs, revokedReason, revokedRequestID, revokedBy)
	}
}

func TestAdminChangeUserCountryWritesOutboxWhenRegionUnchanged(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	repository := mysqltest.NewRepository(t)
	repository.PutCountry(userdomain.Country{CountryCode: "US", Enabled: true})
	repository.PutCountry(userdomain.Country{CountryCode: "CA", Enabled: true})
	repository.PutRegion(userdomain.Region{RegionID: 303, RegionCode: "NORTH-AMERICA", Name: "North America", Countries: []string{"US", "CA"}})
	repository.PutUser(userdomain.User{
		UserID:               91002,
		DefaultDisplayUserID: "91002",
		CurrentDisplayUserID: "91002",
		Country:              "US",
		RegionID:             303,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	svc := userservice.New(repository,
		userservice.WithCountryRegionRepository(repository),
		userservice.WithClock(func() time.Time { return time.UnixMilli(1_700_000_456_000).UTC() }),
	)

	updated, nextAllowedAt, err := svc.AdminChangeUserCountry(ctx, 91002, "CA", "req-admin-country-same-region")
	if err != nil {
		t.Fatalf("AdminChangeUserCountry failed: %v", err)
	}
	if nextAllowedAt != 0 || updated.Country != "CA" || updated.RegionID != 303 {
		t.Fatalf("admin same-region country result mismatch: user=%+v next=%d", updated, nextAllowedAt)
	}

	var payloadJSON string
	if err := repository.RawDB().QueryRowContext(ctx, `
		SELECT CAST(payload_json AS CHAR)
		FROM user_outbox
		WHERE app_code = ? AND event_type = 'UserRegionChanged' AND aggregate_id = ?
	`, appcode.Default, int64(91002)).Scan(&payloadJSON); err != nil {
		t.Fatalf("query same-region UserRegionChanged outbox failed: %v", err)
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
		t.Fatalf("decode same-region outbox payload failed: %v", err)
	}
	if payload["source"] != "admin" ||
		payload["old_country"] != "US" ||
		payload["new_country"] != "CA" ||
		int64(payload["old_region_id"].(float64)) != 303 ||
		int64(payload["new_region_id"].(float64)) != 303 {
		t.Fatalf("same-region outbox payload mismatch: %v", payload)
	}
}

func TestAppChangeUserCountryKeepsSameMillisecondSameRegionOutboxEventsDistinct(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	repository := mysqltest.NewRepository(t)
	for _, countryCode := range []string{"US", "CA", "MX"} {
		repository.PutCountry(userdomain.Country{CountryCode: countryCode, Enabled: true})
	}
	repository.PutRegion(userdomain.Region{
		RegionID:   404,
		RegionCode: "NORTH-AMERICA",
		Name:       "North America",
		Countries:  []string{"US", "CA", "MX"},
	})
	repository.PutUser(userdomain.User{
		UserID:               91003,
		DefaultDisplayUserID: "91003",
		CurrentDisplayUserID: "91003",
		Country:              "US",
		RegionID:             404,
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	// 固定同一毫秒复现高速连续修改；国家码未进入 event_id 时，第二条同区域事件会被 INSERT IGNORE 静默丢弃。
	svc := userservice.New(repository,
		userservice.WithCountryRegionRepository(repository),
		userservice.WithClock(func() time.Time { return time.UnixMilli(1_700_000_789_000).UTC() }),
	)

	if _, _, err := svc.ChangeUserCountry(ctx, 91003, "CA", "req-app-country-ca"); err != nil {
		t.Fatalf("first app country change failed: %v", err)
	}
	if _, _, err := svc.ChangeUserCountry(ctx, 91003, "MX", "req-app-country-mx"); err != nil {
		t.Fatalf("second app country change failed: %v", err)
	}

	rows, err := repository.RawDB().QueryContext(ctx, `
		SELECT event_id, CAST(payload_json AS CHAR)
		FROM user_outbox
		WHERE app_code = ? AND event_type = 'UserRegionChanged' AND aggregate_id = ?
	`, appcode.Default, int64(91003))
	if err != nil {
		t.Fatalf("query app UserRegionChanged outbox failed: %v", err)
	}
	defer rows.Close()
	found := make(map[string]string)
	for rows.Next() {
		var eventID string
		var payloadJSON string
		if err := rows.Scan(&eventID, &payloadJSON); err != nil {
			t.Fatalf("scan app UserRegionChanged outbox failed: %v", err)
		}
		var payload map[string]any
		if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
			t.Fatalf("decode app UserRegionChanged payload failed: %v", err)
		}
		if payload["source"] != "app" || int64(payload["old_region_id"].(float64)) != 404 || int64(payload["new_region_id"].(float64)) != 404 {
			t.Fatalf("app UserRegionChanged payload mismatch: %v", payload)
		}
		found[payload["new_country"].(string)] = eventID
	}
	if err := rows.Err(); err != nil {
		t.Fatalf("iterate app UserRegionChanged outbox failed: %v", err)
	}
	if len(found) != 2 || found["CA"] == "" || found["MX"] == "" || found["CA"] == found["MX"] {
		t.Fatalf("same-millisecond app country changes must keep two distinct events: %+v", found)
	}
}
