package game

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"net/url"
	"strconv"
	"strings"
	"testing"
	"time"

	gamev1 "hyapp.local/api/proto/game/v1"
	gamedomain "hyapp/services/game-service/internal/domain/game"
)

func TestLaunchGameBuildsAMGURL(t *testing.T) {
	repo := &fakeRepository{
		launchable: gamedomain.LaunchableGame{
			CatalogItem: gamedomain.CatalogItem{
				AppCode:        "lalu",
				GameID:         "amg_1001",
				PlatformCode:   gamedomain.PlatformCodeAMG,
				ProviderGameID: "1001",
				GameName:       "AMG Test",
				Status:         gamedomain.StatusActive,
				Orientation:    "portrait",
			},
			PlatformStatus: gamedomain.StatusActive,
			AdapterType:    gamedomain.AdapterAMGV1,
			AdapterConfigJSON: `{
				"app_key":"kzphamrv01",
				"default_lang":"en",
				"token_ttl_seconds":86400,
				"mini":1,
				"app_audio":1,
				"use_lang":1,
				"game_urls":{"1001":"https://dev.playamg.com/greedy/index.html"}
			}`,
		},
	}
	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:        "amg_1001",
		RoomID:        "room_1",
		AccessToken:   strings.Repeat("full_app_access_token", 40),
		ProviderToken: "short_amg_provider_token",
	})
	if err != nil {
		t.Fatalf("LaunchGame failed: %v", err)
	}
	if result.ExpiresAtMS != 1700086400000 {
		t.Fatalf("AMG launch must use adapter token ttl, got %+v", result)
	}
	parsed, err := url.Parse(result.LaunchURL)
	if err != nil {
		t.Fatalf("parse AMG launch URL: %v", err)
	}
	if got := parsed.Scheme + "://" + parsed.Host + parsed.Path; got != "https://dev.playamg.com/greedy/index.html" {
		t.Fatalf("AMG launch base mismatch: %s", result.LaunchURL)
	}
	want := map[string]string{
		"user_token": "short_amg_provider_token",
		"app_key":    "kzphamrv01",
		"room_id":    "room_1",
		"mini":       "1",
		"lang":       "en",
		"app_audio":  "1",
		"use_lang":   "1",
	}
	for key, value := range want {
		if got := parsed.Query().Get(key); got != value {
			t.Fatalf("AMG query %s = %q, want %q URL=%s", key, got, value, result.LaunchURL)
		}
	}
}

func TestHandleAMGUserReturnsProfileAndWallet(t *testing.T) {
	token := "amg_user_token"
	repo := &fakeRepository{
		platform: gamedomain.Platform{
			PlatformCode:      gamedomain.PlatformCodeAMG,
			AdapterType:       gamedomain.AdapterAMGV1,
			AdapterConfigJSON: `{"uid_mode":"display_user_id","coin_scale":1}`,
		},
		session: gamedomain.LaunchSession{
			AppCode:         "lalu",
			UserID:          42,
			DisplayUserID:   "420001",
			RoomID:          "room_1",
			PlatformCode:    gamedomain.PlatformCodeAMG,
			GameID:          "amg_1001",
			ProviderGameID:  "1001",
			LaunchTokenHash: stableHash(token),
			Status:          gamedomain.SessionActive,
			ExpiresAtMS:     1700086400000,
		},
	}
	wallet := &fakeWallet{balanceAfter: 1200}
	svc := New(Config{}, repo, wallet, &fakeUser{})
	svc.now = func() time.Time { return time.UnixMilli(1700000000000) }

	raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
		Meta:         &gamev1.RequestMeta{AppCode: "lalu", RequestId: "req-amg-user"},
		PlatformCode: gamedomain.PlatformCodeAMG,
		Operation:    "user",
		Headers:      map[string]string{"Authorization": token},
	})
	if err != nil {
		t.Fatalf("HandleCallback failed: %v", err)
	}
	var response struct {
		Code int `json:"code"`
		Data struct {
			UserID         string      `json:"userId"`
			Nickname       string      `json:"nickname"`
			AvatarURL      string      `json:"avatarUrl"`
			AvailableCoins json.Number `json:"availableCoins"`
			RoomID         string      `json:"roomId"`
		} `json:"data"`
		Message string `json:"msg"`
	}
	if err := json.Unmarshal(raw, &response); err != nil {
		t.Fatalf("decode AMG user response: %v body=%s", err, raw)
	}
	if response.Code != 0 || response.Message != "success" || response.Data.UserID != "420001" || response.Data.Nickname != "tester" || response.Data.AvatarURL == "" || response.Data.AvailableCoins.String() != "1200" || response.Data.RoomID != "room_1" {
		t.Fatalf("AMG user response mismatch: %+v body=%s", response, raw)
	}
}

func TestHandleAMGSettlementDecryptsAndIsIdempotentAcrossRandomCiphertexts(t *testing.T) {
	privateKey, secret := amgTestPrivateKey(t)
	token := "amg_settlement_token"
	repo := &fakeRepository{
		platform: gamedomain.Platform{
			PlatformCode:             gamedomain.PlatformCodeAMG,
			AdapterType:              gamedomain.AdapterAMGV1,
			CallbackSecretCiphertext: secret,
			AdapterConfigJSON:        `{"coin_scale":1}`,
		},
		session: gamedomain.LaunchSession{
			AppCode:         "lalu",
			UserID:          42,
			DisplayUserID:   "420001",
			RoomID:          "room_1",
			RegionID:        100,
			CountryID:       971,
			PlatformCode:    gamedomain.PlatformCodeAMG,
			GameID:          "amg_1001",
			ProviderGameID:  "1001",
			LaunchTokenHash: stableHash(token),
			Status:          gamedomain.SessionActive,
			ExpiresAtMS:     1700086400000,
		},
	}
	wallet := &fakeWallet{balanceAfter: 900}
	svc := New(Config{}, repo, wallet, &fakeUser{})
	svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
	plain := []byte(`{"transactionId":"amg-order-1","coins":100,"type":1,"timestamp":1700000000000,"ext":"room_game","gameId":1001}`)

	for attempt := 0; attempt < 2; attempt++ {
		encrypted := amgTestEncrypt(t, &privateKey.PublicKey, plain)
		envelope, err := json.Marshal(map[string]string{"data": encrypted})
		if err != nil {
			t.Fatalf("marshal envelope: %v", err)
		}
		raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
			Meta:         &gamev1.RequestMeta{AppCode: "lalu", RequestId: "req-amg-settle-" + strconv.Itoa(attempt)},
			PlatformCode: gamedomain.PlatformCodeAMG,
			Operation:    "settlement",
			Headers:      map[string]string{"Authorization": token},
			RawBody:      envelope,
		})
		if err != nil {
			t.Fatalf("HandleCallback attempt %d failed: %v", attempt, err)
		}
		var response struct {
			Code int `json:"code"`
			Data struct {
				AvailableCoins json.Number `json:"availableCoins"`
			} `json:"data"`
		}
		if err := json.Unmarshal(raw, &response); err != nil || response.Code != 0 || response.Data.AvailableCoins.String() != "900" {
			t.Fatalf("AMG settlement response mismatch: err=%v response=%+v body=%s", err, response, raw)
		}
	}
	if wallet.applyCount != 1 || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 100 || wallet.lastApply.GetProviderOrderId() != "amg-order-1" {
		t.Fatalf("AMG wallet change mismatch: count=%d request=%+v", wallet.applyCount, wallet.lastApply)
	}
	wantHash := stableHash("amg-order-1|100|1|1700000000000|room_game|1001")
	if repo.order.RequestHash != wantHash || repo.order.Status != gamedomain.OrderStatusSucceeded {
		t.Fatalf("AMG order idempotency mismatch: %+v want_hash=%s", repo.order, wantHash)
	}
}

func amgTestPrivateKey(t *testing.T) (*rsa.PrivateKey, string) {
	t.Helper()
	privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		t.Fatalf("generate RSA key: %v", err)
	}
	der, err := x509.MarshalPKCS8PrivateKey(privateKey)
	if err != nil {
		t.Fatalf("marshal PKCS#8 key: %v", err)
	}
	return privateKey, base64.StdEncoding.EncodeToString(der)
}

func amgTestEncrypt(t *testing.T, publicKey *rsa.PublicKey, plain []byte) string {
	t.Helper()
	chunkSize := publicKey.Size() - 11
	ciphertext := make([]byte, 0, ((len(plain)+chunkSize-1)/chunkSize)*publicKey.Size())
	for start := 0; start < len(plain); start += chunkSize {
		end := start + chunkSize
		if end > len(plain) {
			end = len(plain)
		}
		chunk, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plain[start:end])
		if err != nil {
			t.Fatalf("encrypt AMG test chunk: %v", err)
		}
		ciphertext = append(ciphertext, chunk...)
	}
	return hex.EncodeToString(ciphertext)
}
