package cp

import (
	"context"
	"database/sql"
	"fmt"
	"testing"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	cpdomain "hyapp/services/user-service/internal/domain/cp"
	"hyapp/services/user-service/internal/testutil/mysqlschema"
)

func TestExpiredApplicationOperationsPersistExpiredStatus(t *testing.T) {
	tests := []struct {
		name          string
		applicationID string
		run           func(context.Context, *Repository, string, int64) error
	}{
		{
			name:          "accept expired",
			applicationID: "cp_expired_accept",
			run: func(ctx context.Context, repo *Repository, applicationID string, nowMS int64) error {
				_, _, err := repo.AcceptApplication(ctx, 1002, applicationID, nowMS)
				return err
			},
		},
		{
			name:          "reject expired",
			applicationID: "cp_expired_reject",
			run: func(ctx context.Context, repo *Repository, applicationID string, nowMS int64) error {
				_, err := repo.RejectApplication(ctx, 1002, applicationID, "too late", nowMS)
				return err
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			schema := mysqlschema.New(t)
			repo := New(schema.DB)
			ctx := appcode.WithContext(context.Background(), appcode.Default)
			nowMS := int64(1700000000000)

			seedCPTestUser(t, schema.DB, 1001, "CP Alice", nowMS)
			seedCPTestUser(t, schema.DB, 1002, "CP Bob", nowMS)
			seedExpiredCPApplication(t, schema.DB, test.applicationID, 1001, 1002, nowMS)

			err := test.run(ctx, repo, test.applicationID, nowMS)
			if xerr.CodeOf(err) != xerr.Conflict {
				t.Fatalf("expired operation must return conflict, got %v", err)
			}

			var status string
			if err := schema.DB.QueryRowContext(ctx, `
				SELECT status
				FROM user_cp_applications
				WHERE app_code = ? AND application_id = ?`,
				appcode.Default, test.applicationID,
			).Scan(&status); err != nil {
				t.Fatalf("read application status failed: %v", err)
			}
			if status != cpdomain.ApplicationStatusExpired {
				t.Fatalf("expired operation must persist status=%s, got %s", cpdomain.ApplicationStatusExpired, status)
			}
		})
	}
}

func TestAcceptApplicationUsesSiblingCapacityConfig(t *testing.T) {
	schema := mysqlschema.New(t)
	repo := New(schema.DB)
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	nowMS := int64(1700000000000)

	for _, userID := range []int64{2001, 2002, 2003, 2004} {
		seedCPTestUser(t, schema.DB, userID, fmt.Sprintf("Brother %d", userID), nowMS)
	}
	seedRelationConfig(t, schema.DB, cpdomain.RelationTypeBrother, 2, nowMS)
	seedPendingCPApplication(t, schema.DB, "brother_1", cpdomain.RelationTypeBrother, 2001, 2002, nowMS)
	seedPendingCPApplication(t, schema.DB, "brother_2", cpdomain.RelationTypeBrother, 2001, 2003, nowMS)
	seedPendingCPApplication(t, schema.DB, "brother_3", cpdomain.RelationTypeBrother, 2001, 2004, nowMS)

	if _, _, err := repo.AcceptApplication(ctx, 2002, "brother_1", nowMS); err != nil {
		t.Fatalf("accept first brother relationship failed: %v", err)
	}
	if _, _, err := repo.AcceptApplication(ctx, 2003, "brother_2", nowMS+1); err != nil {
		t.Fatalf("accept second brother relationship failed: %v", err)
	}
	if _, _, err := repo.AcceptApplication(ctx, 2004, "brother_3", nowMS+2); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("third brother relationship must hit configured limit, got %v", err)
	}
}

func TestAcceptApplicationKeepsCPHardLimit(t *testing.T) {
	schema := mysqlschema.New(t)
	repo := New(schema.DB)
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	nowMS := int64(1700000000000)

	for _, userID := range []int64{3001, 3002, 3003} {
		seedCPTestUser(t, schema.DB, userID, fmt.Sprintf("CP %d", userID), nowMS)
	}
	seedRelationConfig(t, schema.DB, cpdomain.RelationTypeCP, 9, nowMS)
	seedPendingCPApplication(t, schema.DB, "cp_1", cpdomain.RelationTypeCP, 3001, 3002, nowMS)
	seedPendingCPApplication(t, schema.DB, "cp_2", cpdomain.RelationTypeCP, 3001, 3003, nowMS)

	if _, _, err := repo.AcceptApplication(ctx, 3002, "cp_1", nowMS); err != nil {
		t.Fatalf("accept first cp relationship failed: %v", err)
	}
	if _, _, err := repo.AcceptApplication(ctx, 3003, "cp_2", nowMS+1); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("second cp relationship must ignore expanded config and stay limited, got %v", err)
	}
}

func TestListRelationshipsIncludesServerLevelProgress(t *testing.T) {
	schema := mysqlschema.New(t)
	repo := New(schema.DB)
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	nowMS := int64(1700000000000)

	for _, userID := range []int64{4001, 4002, 4003} {
		seedCPTestUser(t, schema.DB, userID, fmt.Sprintf("Progress %d", userID), nowMS)
	}
	seedCPLevelRules(t, schema.DB, cpdomain.RelationTypeBrother, []int64{0, 100, 300, 600, 1000}, nowMS)
	seedActiveCPRelationship(t, schema.DB, "progress_mid", cpdomain.RelationTypeBrother, 4001, 4002, 180, 2, nowMS)
	seedActiveCPRelationship(t, schema.DB, "progress_max", cpdomain.RelationTypeBrother, 4001, 4003, 1200, 5, nowMS+1)

	relationships, total, err := repo.ListRelationships(ctx, 4001, cpdomain.RelationTypeBrother, 1, 20)
	if err != nil {
		t.Fatalf("list relationships failed: %v", err)
	}
	if total != 2 || len(relationships) != 2 {
		t.Fatalf("expected two relationships, total=%d len=%d", total, len(relationships))
	}

	byID := map[string]cpdomain.Relationship{}
	for _, relationship := range relationships {
		byID[relationship.RelationshipID] = relationship
	}
	mid := byID["progress_mid"]
	if mid.CurrentLevelThreshold != 100 || mid.NextLevelThreshold != 300 || mid.NeededForNextLevel != 120 || mid.LevelProgressPercent != 40 || mid.MaxLevel {
		t.Fatalf("mid-level progress mismatch: %+v", mid)
	}
	max := byID["progress_max"]
	if max.CurrentLevelThreshold != 1000 || max.NextLevelThreshold != 0 || max.NeededForNextLevel != 0 || max.LevelProgressPercent != 100 || !max.MaxLevel {
		t.Fatalf("max-level progress mismatch: %+v", max)
	}
}

func TestBreakRelationshipPrepareConfirmIsIdempotent(t *testing.T) {
	schema := mysqlschema.New(t)
	repo := New(schema.DB)
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	nowMS := int64(1700000000000)

	seedCPTestUser(t, schema.DB, 5001, "Break Alice", nowMS)
	seedCPTestUser(t, schema.DB, 5002, "Break Bob", nowMS)
	seedRelationConfigWithBreakupCost(t, schema.DB, cpdomain.RelationTypeCP, 1, 88, nowMS)
	seedActiveCPRelationship(t, schema.DB, "break_confirm", cpdomain.RelationTypeCP, 5001, 5002, 20, 1, nowMS)

	breakup, relationship, err := repo.PrepareBreakRelationship(ctx, 5001, "break_confirm", "break_cmd_1", nowMS+1)
	if err != nil {
		t.Fatalf("prepare breakup failed: %v", err)
	}
	if breakup.Status != cpdomain.BreakupStatusPending || breakup.BreakupCostCoins != 88 {
		t.Fatalf("prepare breakup mismatch: %+v", breakup)
	}
	if relationship.BreakupCostCoins != 88 || relationship.Status != cpdomain.RelationshipStatusActive {
		t.Fatalf("relationship cost/status mismatch: %+v", relationship)
	}

	replayed, _, err := repo.PrepareBreakRelationship(ctx, 5001, "break_confirm", "break_cmd_1", nowMS+2)
	if err != nil {
		t.Fatalf("prepare replay failed: %v", err)
	}
	if replayed.CommandID != breakup.CommandID || replayed.Status != cpdomain.BreakupStatusPending {
		t.Fatalf("prepare replay mismatch: %+v", replayed)
	}
	if _, _, err := repo.PrepareBreakRelationship(ctx, 5002, "break_confirm", "break_cmd_2", nowMS+3); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("concurrent breakup must be rejected, got %v", err)
	}
	if _, _, err := repo.ConfirmBreakRelationship(ctx, 5001, "break_confirm", "break_cmd_1", "wallet_tx_bad", 87, 913, nowMS+4); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("paid amount mismatch must be rejected, got %v", err)
	}

	confirmed, ended, err := repo.ConfirmBreakRelationship(ctx, 5001, "break_confirm", "break_cmd_1", "wallet_tx_1", 88, 912, nowMS+5)
	if err != nil {
		t.Fatalf("confirm breakup failed: %v", err)
	}
	if confirmed.Status != cpdomain.BreakupStatusConfirmed || confirmed.WalletTransactionID != "wallet_tx_1" {
		t.Fatalf("confirmed breakup mismatch: %+v", confirmed)
	}
	if ended.Status != cpdomain.RelationshipStatusEnded || ended.EndedAtMS != nowMS+5 || ended.BreakupCostCoins != 88 {
		t.Fatalf("ended relationship mismatch: %+v", ended)
	}
	replayedConfirm, replayedEnded, err := repo.ConfirmBreakRelationship(ctx, 5001, "break_confirm", "break_cmd_1", "wallet_tx_1", 88, 912, nowMS+6)
	if err != nil {
		t.Fatalf("confirm replay failed: %v", err)
	}
	if replayedConfirm.Status != cpdomain.BreakupStatusConfirmed || replayedEnded.Status != cpdomain.RelationshipStatusEnded {
		t.Fatalf("confirm replay mismatch: breakup=%+v relationship=%+v", replayedConfirm, replayedEnded)
	}
}

func TestBreakRelationshipCancelKeepsRelationshipActive(t *testing.T) {
	schema := mysqlschema.New(t)
	repo := New(schema.DB)
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	nowMS := int64(1700000000000)

	seedCPTestUser(t, schema.DB, 6001, "Cancel Alice", nowMS)
	seedCPTestUser(t, schema.DB, 6002, "Cancel Bob", nowMS)
	seedRelationConfigWithBreakupCost(t, schema.DB, cpdomain.RelationTypeSister, 5, 66, nowMS)
	seedActiveCPRelationship(t, schema.DB, "break_cancel", cpdomain.RelationTypeSister, 6001, 6002, 20, 1, nowMS)

	if _, _, err := repo.PrepareBreakRelationship(ctx, 6002, "break_cancel", "break_cmd_cancel", nowMS+1); err != nil {
		t.Fatalf("prepare cancel breakup failed: %v", err)
	}
	canceled, err := repo.CancelBreakRelationship(ctx, 6002, "break_cancel", "break_cmd_cancel", "wallet_debit_failed", nowMS+2)
	if err != nil {
		t.Fatalf("cancel breakup failed: %v", err)
	}
	if canceled.Status != cpdomain.BreakupStatusCanceled {
		t.Fatalf("cancel status mismatch: %+v", canceled)
	}
	relationships, total, err := repo.ListRelationships(ctx, 6002, cpdomain.RelationTypeSister, 1, 20)
	if err != nil {
		t.Fatalf("list relationships after cancel failed: %v", err)
	}
	if total != 1 || len(relationships) != 1 || relationships[0].Status != cpdomain.RelationshipStatusActive {
		t.Fatalf("relationship must stay active after cancel, total=%d relationships=%+v", total, relationships)
	}
	if _, _, err := repo.ConfirmBreakRelationship(ctx, 6002, "break_cancel", "break_cmd_cancel", "wallet_tx_after_cancel", 66, 934, nowMS+3); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("canceled breakup must not be confirmed, got %v", err)
	}
}

func seedCPTestUser(t testing.TB, db *sql.DB, userID int64, username string, nowMS int64) {
	t.Helper()

	displayID := fmt.Sprintf("%d", userID)
	// CP 申请查询会 join requester/target 用户快照；测试只准备这些不可空主数据字段。
	if _, err := db.ExecContext(context.Background(), `
		INSERT INTO users (
			app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
			username, gender, country, region_id, avatar, profile_completed, profile_completed_at_ms,
			onboarding_status, status, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'US', 1, ?, 1, ?, 'completed', 'active', ?, ?)`,
		appcode.Default, userID, displayID, displayID, username, "https://cdn.example.com/cp-test.png", nowMS, nowMS, nowMS,
	); err != nil {
		t.Fatalf("seed cp test user %d failed: %v", userID, err)
	}
}

func seedRelationConfig(t testing.TB, db *sql.DB, relationType string, maxCount int32, nowMS int64) {
	seedRelationConfigWithBreakupCost(t, db, relationType, maxCount, 0, nowMS)
}

func seedRelationConfigWithBreakupCost(t testing.TB, db *sql.DB, relationType string, maxCount int32, breakupCost int64, nowMS int64) {
	t.Helper()

	// 容量测试直接写配置表，AcceptApplication 中的默认配置 upsert 不会覆盖已有业务配置。
	if _, err := db.ExecContext(context.Background(), `
		INSERT INTO user_cp_relation_configs (
			app_code, relation_type, max_count_per_user, application_expire_hours, breakup_cost_coins,
			status, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 24, ?, 'active', ?, ?)
		ON DUPLICATE KEY UPDATE
			max_count_per_user = VALUES(max_count_per_user),
			breakup_cost_coins = VALUES(breakup_cost_coins),
			updated_at_ms = VALUES(updated_at_ms)`,
		appcode.Default, relationType, maxCount, breakupCost, nowMS, nowMS,
	); err != nil {
		t.Fatalf("seed relation config %s failed: %v", relationType, err)
	}
}

func seedCPLevelRules(t testing.TB, db *sql.DB, relationType string, thresholds []int64, nowMS int64) {
	t.Helper()

	for index, threshold := range thresholds {
		level := int32(index + 1)
		// 测试直接写等级阈值表，验证关系查询返回的是服务端配置结果，而不是客户端或测试本地推导结果。
		if _, err := db.ExecContext(context.Background(), `
			INSERT INTO user_cp_level_rules (
				app_code, relation_type, level_no, intimacy_threshold,
				reward_resource_group_id, level_icon_url, status, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, 0, '', 'active', ?, ?)
			ON DUPLICATE KEY UPDATE
				intimacy_threshold = VALUES(intimacy_threshold),
				status = VALUES(status),
				updated_at_ms = VALUES(updated_at_ms)`,
			appcode.Default, relationType, level, threshold, nowMS, nowMS,
		); err != nil {
			t.Fatalf("seed cp level rule %s/%d failed: %v", relationType, level, err)
		}
	}
}

func seedActiveCPRelationship(t testing.TB, db *sql.DB, relationshipID string, relationType string, userAID int64, userBID int64, intimacy int64, level int32, nowMS int64) {
	t.Helper()

	firstUserID, secondUserID := orderedPair(userAID, userBID)
	// active 关系事实只保存当前等级和亲密值；当前/下一阈值必须由查询时读取等级规则计算。
	if _, err := db.ExecContext(context.Background(), `
		INSERT INTO user_cp_relationships (
			app_code, relationship_id, user_a_id, user_b_id, relation_type, status,
			intimacy_value, level_no, source_application_id,
			formed_at_ms, ended_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', ?, 0, ?, ?)`,
		appcode.Default, relationshipID, firstUserID, secondUserID, relationType, cpdomain.RelationshipStatusActive,
		intimacy, level, nowMS, nowMS, nowMS,
	); err != nil {
		t.Fatalf("seed active cp relationship %s failed: %v", relationshipID, err)
	}
}

func seedPendingCPApplication(t testing.TB, db *sql.DB, applicationID string, relationType string, requesterUserID int64, targetUserID int64, nowMS int64) {
	t.Helper()

	if _, err := db.ExecContext(context.Background(), `
		INSERT INTO user_cp_applications (
			app_code, application_id, requester_user_id, target_user_id, relation_type, status,
			room_id, room_region_id, gift_id, gift_name, gift_icon_url, gift_animation_url,
			gift_count, gift_value, billing_receipt_id, source_room_event_id, source_command_id,
			created_at_ms, updated_at_ms, expires_at_ms, decided_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, 'cp_test_room', 1, 'cp_test_gift', 'CP Test Gift', '', '',
			1, 10, 'cp_test_receipt', ?, 'cp_test_command', ?, ?, ?, 0)`,
		appcode.Default, applicationID, requesterUserID, targetUserID, relationType,
		cpdomain.ApplicationStatusPending, "event_"+applicationID, nowMS, nowMS, nowMS+24*60*60*1000,
	); err != nil {
		t.Fatalf("seed pending cp application %s failed: %v", applicationID, err)
	}
}

func seedExpiredCPApplication(t testing.TB, db *sql.DB, applicationID string, requesterUserID int64, targetUserID int64, nowMS int64) {
	t.Helper()

	// expires_at_ms 放在 nowMS 前一毫秒，确保仓储操作走“不能操作但要落 expired”的惰性过期分支。
	if _, err := db.ExecContext(context.Background(), `
		INSERT INTO user_cp_applications (
			app_code, application_id, requester_user_id, target_user_id, relation_type, status,
			room_id, room_region_id, gift_id, gift_name, gift_icon_url, gift_animation_url,
			gift_count, gift_value, billing_receipt_id, source_room_event_id, source_command_id,
			created_at_ms, updated_at_ms, expires_at_ms, decided_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, 'cp_test_room', 1, 'cp_test_gift', 'CP Test Gift', '', '',
			1, 10, 'cp_test_receipt', ?, 'cp_test_command', ?, ?, ?, 0)`,
		appcode.Default, applicationID, requesterUserID, targetUserID, cpdomain.RelationTypeCP,
		cpdomain.ApplicationStatusPending, "event_"+applicationID, nowMS-1000, nowMS-1000, nowMS-1,
	); err != nil {
		t.Fatalf("seed expired cp application %s failed: %v", applicationID, err)
	}
}
