package mysql

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

	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/appcode"
	gamedomain "hyapp/services/game-service/internal/domain/game"
)

func TestMarkOrderSucceededCreatesClaimableLevelOutboxEvent(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
		DatabasePrefix: "hy_game_test",
	})

	ctx := appcode.WithContext(context.Background(), "lalu")
	repo, err := Open(ctx, schema.DSN)
	if err != nil {
		t.Fatalf("open repository failed: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	order := gamedomain.GameOrder{
		AppCode:         "lalu",
		OrderID:         "order_1",
		PlatformCode:    "demo",
		ProviderOrderID: "provider_order_1",
		GameID:          "demo_rocket_001",
		ProviderGameID:  "rocket_001",
		UserID:          42,
		RoomID:          "room_1",
		OpType:          "debit",
		CoinAmount:      120,
		Status:          gamedomain.OrderStatusWalletApplying,
		RequestHash:     "hash_1",
		CreatedAtMS:     1700000000000,
		UpdatedAtMS:     1700000000000,
	}
	if _, _, err := repo.CreateGameOrder(ctx, order); err != nil {
		t.Fatalf("create order failed: %v", err)
	}
	if err := repo.MarkOrderSucceeded(ctx, "lalu", "order_1", "wtx-game-1", 880, 1700000001000); err != nil {
		t.Fatalf("mark order succeeded failed: %v", err)
	}

	events, err := repo.ClaimPendingLevelEvents(ctx, "worker_1", 1700000002000, 30*time.Second, 10)
	if err != nil {
		t.Fatalf("claim level events failed: %v", err)
	}
	if len(events) != 1 {
		t.Fatalf("expected one level event, got %+v", events)
	}
	event := events[0]
	if event.EventID != "game_level:order_1" || event.UserID != 42 || event.CoinAmount != 120 {
		t.Fatalf("level event mismatch: %+v", event)
	}
	if event.PayloadJSON == "" || event.WalletTransactionID != "wtx-game-1" {
		t.Fatalf("level event payload mismatch: %+v", event)
	}
}

func TestClaimPendingLevelEventsPreservesCreatedOrderAcrossStatuses(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
		DatabasePrefix: "hy_game_test",
	})

	ctx := appcode.WithContext(context.Background(), "lalu")
	repo, err := Open(ctx, schema.DSN)
	if err != nil {
		t.Fatalf("open repository failed: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	insertLevelEvent := func(eventID string, orderID string, status string, nextRetryAtMS int64, createdAtMS int64, payload string) {
		t.Helper()
		if _, err := repo.db.ExecContext(ctx, `
			INSERT INTO game_level_event_outbox (
				app_code, event_id, order_id, user_id, game_id, provider_order_id,
				provider_round_id, coin_amount, wallet_transaction_id, payload_json,
				status, attempt_count, next_retry_at_ms, locked_by, locked_until_ms,
				failure_reason, occurred_at_ms, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, 42, 'game_1', ?, '', 100, 'wallet_tx_1', ?,
			          ?, 0, ?, '', 0, '', ?, ?, ?)`,
			"lalu", eventID, orderID, "provider_"+orderID, payload, status, nextRetryAtMS, createdAtMS, createdAtMS, createdAtMS,
		); err != nil {
			t.Fatalf("insert level event %s failed: %v", eventID, err)
		}
	}
	insertLevelEvent("event_failed_old", "order_failed_old", "failed", 1000, 1000, `{"source":"failed"}`)
	insertLevelEvent("event_failed_future", "order_failed_future", "failed", 999999, 1500, `{"source":"future"}`)
	insertLevelEvent("event_pending_new", "order_pending_new", "pending", 0, 2000, `{"source":"pending"}`)

	events, err := repo.ClaimPendingLevelEvents(ctx, "worker_1", 3000, 30*time.Second, 2)
	if err != nil {
		t.Fatalf("claim level events failed: %v", err)
	}
	if len(events) != 2 {
		t.Fatalf("expected two claimed events, got %+v", events)
	}
	if events[0].EventID != "event_failed_old" || events[0].Status != "failed" || !strings.Contains(events[0].PayloadJSON, `"source": "failed"`) {
		t.Fatalf("first claimed event should be the older failed payload, got %+v", events[0])
	}
	if events[1].EventID != "event_pending_new" || events[1].Status != "pending" || !strings.Contains(events[1].PayloadJSON, `"source": "pending"`) {
		t.Fatalf("second claimed event should be the pending payload, got %+v", events[1])
	}

	var runningCount int
	if err := repo.db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM game_level_event_outbox
		WHERE app_code = 'lalu' AND status = 'running' AND locked_by = 'worker_1' AND locked_until_ms = 33000`,
	).Scan(&runningCount); err != nil {
		t.Fatalf("query running events failed: %v", err)
	}
	if runningCount != 2 {
		t.Fatalf("expected two running claimed events, got %d", runningCount)
	}
	var futureStatus string
	if err := repo.db.QueryRowContext(ctx, `
		SELECT status FROM game_level_event_outbox WHERE app_code = 'lalu' AND event_id = 'event_failed_future'`,
	).Scan(&futureStatus); err != nil {
		t.Fatalf("query future retry event failed: %v", err)
	}
	if futureStatus != "failed" {
		t.Fatalf("future retry event should stay failed, got %s", futureStatus)
	}
}

func TestLevelEventClaimKeyQueryUsesOrderIndexWithoutFilesort(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
		DatabasePrefix: "hy_game_test",
	})

	ctx := appcode.WithContext(context.Background(), "lalu")
	repo, err := Open(ctx, schema.DSN)
	if err != nil {
		t.Fatalf("open repository failed: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	var plan string
	if err := repo.db.QueryRowContext(ctx, `
		EXPLAIN FORMAT=JSON
		SELECT event_id, created_at_ms
		FROM game_level_event_outbox FORCE INDEX (idx_game_level_event_claim_order)
		WHERE app_code = ? AND status = ? AND locked_until_ms = 0 AND next_retry_at_ms <= ?
		ORDER BY created_at_ms ASC, event_id ASC
		LIMIT 100`,
		"lalu", "pending", int64(1780542672724),
	).Scan(&plan); err != nil {
		t.Fatalf("explain level event claim key query failed: %v", err)
	}
	if !strings.Contains(plan, `"key": "idx_game_level_event_claim_order"`) {
		t.Fatalf("claim key query did not use idx_game_level_event_claim_order:\n%s", plan)
	}
	if strings.Contains(strings.ToLower(plan), `"using_filesort": true`) {
		t.Fatalf("claim key query should avoid filesort:\n%s", plan)
	}
}

func TestUpsertCatalogCreatesDefaultVoiceRoomDisplayRule(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
		DatabasePrefix: "hy_game_test",
	})

	ctx := appcode.WithContext(context.Background(), "lalu")
	repo, err := Open(ctx, schema.DSN)
	if err != nil {
		t.Fatalf("open repository failed: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	item := gamedomain.CatalogItem{
		AppCode:        "lalu",
		GameID:         "sync_game_001",
		PlatformCode:   "demo",
		ProviderGameID: "provider_sync_001",
		GameName:       "Sync Game",
		Category:       "casino",
		LaunchMode:     gamedomain.LaunchModeH5Popup,
		Orientation:    "portrait",
		Status:         gamedomain.StatusActive,
		SortOrder:      70,
		Tags:           []string{"sync"},
	}
	if _, err := repo.UpsertCatalog(ctx, item); err != nil {
		t.Fatalf("upsert catalog failed: %v", err)
	}

	var scene string
	var visible, enabled bool
	var sortOrder int32
	if err := repo.db.QueryRowContext(ctx,
		`SELECT scene, visible, enabled, sort_order
		 FROM game_display_rules WHERE app_code = ? AND rule_id = ? AND game_id = ?`,
		"lalu", defaultVoiceRoomRuleID(item.GameID), item.GameID,
	).Scan(&scene, &visible, &enabled, &sortOrder); err != nil {
		t.Fatalf("query display rule failed: %v", err)
	}
	if scene != gamedomain.SceneVoiceRoom || !visible || !enabled || sortOrder != item.SortOrder {
		t.Fatalf("display rule mismatch: scene=%s visible=%v enabled=%v sort=%d", scene, visible, enabled, sortOrder)
	}
}

func TestDeleteCatalogRemovesGameAndDisplayRule(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
		DatabasePrefix: "hy_game_test",
	})

	ctx := appcode.WithContext(context.Background(), "lalu")
	repo, err := Open(ctx, schema.DSN)
	if err != nil {
		t.Fatalf("open repository failed: %v", err)
	}
	t.Cleanup(func() { _ = repo.Close() })

	item := gamedomain.CatalogItem{
		AppCode:        "lalu",
		GameID:         "delete_game_001",
		PlatformCode:   "demo",
		ProviderGameID: "provider_delete_001",
		GameName:       "Delete Game",
		Category:       "casino",
		LaunchMode:     gamedomain.LaunchModeH5Popup,
		Orientation:    "portrait",
		Status:         gamedomain.StatusActive,
		SortOrder:      80,
	}
	if _, err := repo.UpsertCatalog(ctx, item); err != nil {
		t.Fatalf("upsert catalog failed: %v", err)
	}
	if err := repo.DeleteCatalog(ctx, "lalu", item.GameID); err != nil {
		t.Fatalf("delete catalog failed: %v", err)
	}

	var gameCount int
	if err := repo.db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM game_catalog WHERE app_code = ? AND game_id = ?`,
		"lalu", item.GameID,
	).Scan(&gameCount); err != nil {
		t.Fatalf("query catalog count failed: %v", err)
	}
	if gameCount != 0 {
		t.Fatalf("game_catalog count = %d, want 0", gameCount)
	}

	var ruleCount int
	if err := repo.db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM game_display_rules WHERE app_code = ? AND game_id = ?`,
		"lalu", item.GameID,
	).Scan(&ruleCount); err != nil {
		t.Fatalf("query display rule count failed: %v", err)
	}
	if ruleCount != 0 {
		t.Fatalf("game_display_rules count = %d, want 0", ruleCount)
	}
}
