package game

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

	gamev1 "hyapp.local/api/proto/game/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/xerr"
	gamedomain "hyapp/services/game-service/internal/domain/game"
)

func TestLaunchGameCreatesSessionAndRejectsMaintenance(t *testing.T) {
	repo := &fakeRepository{
		launchable: gamedomain.LaunchableGame{
			CatalogItem: gamedomain.CatalogItem{
				AppCode:        "lalu",
				GameID:         "demo_rocket_001",
				PlatformCode:   "demo",
				ProviderGameID: "rocket_001",
				GameName:       "Rocket",
				Status:         gamedomain.StatusActive,
				Orientation:    "portrait",
			},
			PlatformStatus: gamedomain.StatusActive,
			APIBaseURL:     "https://provider.example/h5",
		},
	}
	svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
	svc.now = func() time.Time { return time.UnixMilli(1700000000000) }

	result, err := svc.LaunchGame(context.Background(), LaunchCommand{
		AppCode:       "lalu",
		UserID:        42,
		DisplayUserID: "420001",
		GameID:        "demo_rocket_001",
		Scene:         gamedomain.SceneVoiceRoom,
		RoomID:        "room_1",
	})
	if err != nil {
		t.Fatalf("LaunchGame failed: %v", err)
	}
	if result.SessionID == "" || result.ExpiresAtMS != 1700000900000 || !strings.Contains(result.LaunchURL, "session_token=") {
		t.Fatalf("launch result mismatch: %+v", result)
	}
	if repo.session.GameID != "demo_rocket_001" || repo.session.UserID != 42 || repo.session.LaunchTokenHash == "" {
		t.Fatalf("launch session was not persisted: %+v", repo.session)
	}

	repo.launchable.PlatformStatus = gamedomain.StatusMaintenance
	_, err = svc.LaunchGame(context.Background(), LaunchCommand{AppCode: "lalu", UserID: 42, GameID: "demo_rocket_001"})
	if !xerr.IsCode(err, xerr.Conflict) {
		t.Fatalf("maintenance game should reject launch, got %v", err)
	}
}

func TestHandleCallbackChangeCoinUsesWalletAndOrderIdempotency(t *testing.T) {
	repo := &fakeRepository{
		launchable: gamedomain.LaunchableGame{CatalogItem: gamedomain.CatalogItem{GameID: "demo_rocket_001", ProviderGameID: "rocket_001"}},
	}
	wallet := &fakeWallet{balanceAfter: 880}
	svc := New(Config{}, repo, wallet, &fakeUser{})
	svc.now = func() time.Time { return time.UnixMilli(1700000000000) }

	req := &gamev1.CallbackRequest{
		Meta:         &gamev1.RequestMeta{RequestId: "req-callback", AppCode: "lalu"},
		PlatformCode: "demo",
		Operation:    "change_coin",
		RawBody:      []byte(`{"user_id":42,"game_id":"demo_rocket_001","provider_order_id":"porder_1","op_type":"debit","coin_amount":120,"room_id":"room_1"}`),
	}
	raw, contentType, err := svc.HandleCallback(context.Background(), req)
	if err != nil {
		t.Fatalf("HandleCallback failed: %v", err)
	}
	if contentType != "application/json" || !strings.Contains(string(raw), `"balance_after":880`) {
		t.Fatalf("callback response mismatch: contentType=%s raw=%s", contentType, raw)
	}
	if wallet.lastApply.GetCommandId() != "game:demo:porder_1" || wallet.lastApply.GetCoinAmount() != 120 {
		t.Fatalf("wallet command mismatch: %+v", wallet.lastApply)
	}
	if repo.order.Status != gamedomain.OrderStatusSucceeded || repo.order.WalletTransactionID != "wtx-game-1" {
		t.Fatalf("order state mismatch: %+v", repo.order)
	}

	raw, _, err = svc.HandleCallback(context.Background(), req)
	if err != nil {
		t.Fatalf("idempotent callback failed: %v", err)
	}
	if !strings.Contains(string(raw), `"idempotent_replay":true`) {
		t.Fatalf("idempotent response mismatch: %s", raw)
	}
}

func TestHandleCallbackGetUserInfoUsesUserAndWallet(t *testing.T) {
	wallet := &fakeWallet{balanceAfter: 1680}
	user := &fakeUser{}
	svc := New(Config{}, &fakeRepository{}, wallet, user)
	svc.now = func() time.Time { return time.UnixMilli(1700000000000) }

	raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
		Meta:         &gamev1.RequestMeta{RequestId: "req-user-info", AppCode: "lalu"},
		PlatformCode: "demo",
		Operation:    "get_user_info",
		RawBody:      []byte(`{"user_id":42}`),
	})
	if err != nil {
		t.Fatalf("HandleCallback failed: %v", err)
	}
	if contentType != "application/json" || !strings.Contains(string(raw), `"display_user_id":"420001"`) || !strings.Contains(string(raw), `"balance":1680`) {
		t.Fatalf("user info response mismatch: %s", raw)
	}
	if user.lastGet.GetUserId() != 42 {
		t.Fatalf("user request mismatch: %+v", user.lastGet)
	}
}

type fakeRepository struct {
	launchable gamedomain.LaunchableGame
	session    gamedomain.LaunchSession
	order      gamedomain.GameOrder
}

func (f *fakeRepository) Ping(context.Context) error { return nil }
func (f *fakeRepository) ListGames(context.Context, ListGamesQuery) ([]gamedomain.AppGame, error) {
	return []gamedomain.AppGame{{GameID: "demo_rocket_001"}}, nil
}
func (f *fakeRepository) GetLaunchableGame(context.Context, string, string) (gamedomain.LaunchableGame, error) {
	return f.launchable, nil
}
func (f *fakeRepository) CreateLaunchSession(_ context.Context, session gamedomain.LaunchSession) error {
	f.session = session
	return nil
}
func (f *fakeRepository) InsertCallbackLog(context.Context, gamedomain.CallbackLog) error { return nil }
func (f *fakeRepository) CreateGameOrder(_ context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
	if f.order.OrderID != "" {
		return f.order, true, nil
	}
	f.order = order
	return order, false, nil
}
func (f *fakeRepository) MarkOrderSucceeded(_ context.Context, _ string, orderID string, walletTransactionID string, balanceAfter int64, nowMs int64) error {
	f.order.OrderID = orderID
	f.order.Status = gamedomain.OrderStatusSucceeded
	f.order.WalletTransactionID = walletTransactionID
	f.order.WalletBalanceAfter = balanceAfter
	f.order.UpdatedAtMS = nowMs
	return nil
}
func (f *fakeRepository) MarkOrderFailed(_ context.Context, _ string, orderID string, status string, code string, message string, nowMs int64) error {
	f.order.OrderID = orderID
	f.order.Status = status
	f.order.FailureCode = code
	f.order.FailureMessage = message
	f.order.UpdatedAtMS = nowMs
	return nil
}
func (f *fakeRepository) ListPlatforms(context.Context, string, string) ([]gamedomain.Platform, error) {
	return nil, nil
}
func (f *fakeRepository) UpsertPlatform(context.Context, gamedomain.Platform) (gamedomain.Platform, error) {
	return gamedomain.Platform{}, nil
}
func (f *fakeRepository) ListCatalog(context.Context, ListCatalogQuery) ([]gamedomain.CatalogItem, string, error) {
	return nil, "", nil
}
func (f *fakeRepository) UpsertCatalog(context.Context, gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
	return gamedomain.CatalogItem{}, nil
}
func (f *fakeRepository) SetGameStatus(context.Context, string, string, string, int64) (gamedomain.CatalogItem, error) {
	return gamedomain.CatalogItem{}, nil
}

type fakeWallet struct {
	lastApply    *walletv1.ApplyGameCoinChangeRequest
	balanceAfter int64
}

func (f *fakeWallet) ApplyGameCoinChange(_ context.Context, req *walletv1.ApplyGameCoinChangeRequest) (*walletv1.ApplyGameCoinChangeResponse, error) {
	f.lastApply = req
	return &walletv1.ApplyGameCoinChangeResponse{WalletTransactionId: "wtx-game-1", BalanceAfter: f.balanceAfter}, nil
}

func (f *fakeWallet) GetBalances(context.Context, *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
	return &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{{AssetType: "COIN", AvailableAmount: f.balanceAfter}}}, nil
}

type fakeUser struct {
	lastGet *userv1.GetUserRequest
}

func (f *fakeUser) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
	f.lastGet = req
	return &userv1.GetUserResponse{User: &userv1.User{
		UserId:        req.GetUserId(),
		DisplayUserId: "420001",
		Username:      "tester",
		Avatar:        "https://cdn.example/avatar.png",
		Country:       "AE",
		RegionId:      100,
	}}, nil
}
