package auth

import (
	"context"
	"errors"
	"strconv"
	"testing"
	"time"

	jwt "github.com/golang-jwt/jwt/v5"
)

func TestVerifierParsesLargeUserIDStringWithoutPrecisionLoss(t *testing.T) {
	t.Parallel()

	const userID int64 = 399999999999990001
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id":   strconv.FormatInt(userID, 10),
		"app_code":  "lalu",
		"device_id": " device-auth-1 ",
	})
	signed, err := token.SignedString([]byte("secret"))
	if err != nil {
		t.Fatalf("sign token failed: %v", err)
	}

	claims, err := NewVerifier("secret").Verify("Bearer " + signed)
	if err != nil {
		t.Fatalf("Verify failed: %v", err)
	}
	if claims.UserID != userID {
		t.Fatalf("user_id lost precision: got %d want %d", claims.UserID, userID)
	}
	if claims.DeviceID != "device-auth-1" {
		t.Fatalf("device_id was not normalized from signed claim: got %q", claims.DeviceID)
	}
}

func TestVerifierKeepsLegacyTokenWithoutDeviceIDCompatible(t *testing.T) {
	t.Parallel()

	// 存量 access token 在滚动升级期不携带 device_id；gateway 仍应允许其进入 fixed_v2，
	// 但绝不得把 sid 自动填为设备，dynamic_v3 会在 owner 处对空设备 fail-close。
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": strconv.FormatInt(42, 10),
		"sid":     "sess-must-not-be-device",
	})
	signed, err := token.SignedString([]byte("secret"))
	if err != nil {
		t.Fatalf("sign legacy token failed: %v", err)
	}
	claims, err := NewVerifier("secret").Verify("Bearer " + signed)
	if err != nil {
		t.Fatalf("Verify legacy token failed: %v", err)
	}
	ctx := WithClaims(context.Background(), claims)
	if got := DeviceIDFromContext(ctx); got != "" {
		t.Fatalf("legacy token device_id=%q, want empty instead of sid fallback", got)
	}
}

func TestVerifierDistinguishesExpiredAccessTokenFromInvalidCredentials(t *testing.T) {
	t.Parallel()
	expired := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": "42",
		"sid":     "sess-expired",
		"exp":     time.Now().Add(-time.Minute).Unix(),
	})
	signed, err := expired.SignedString([]byte("secret"))
	if err != nil {
		t.Fatalf("sign expired token failed: %v", err)
	}
	if _, err := NewVerifier("secret").Verify("Bearer " + signed); !errors.Is(err, ErrAccessTokenExpired) {
		t.Fatalf("expired token must return ErrAccessTokenExpired, got %v", err)
	}
	if _, err := NewVerifier("different-secret").Verify("Bearer " + signed); errors.Is(err, ErrAccessTokenExpired) || err == nil {
		t.Fatalf("invalid signature must remain generic unauthorized, got %v", err)
	}
	contradictory := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": "42",
		"sid":     "sess-invalid-window",
		"exp":     time.Now().Add(-time.Minute).Unix(),
		"nbf":     time.Now().Add(time.Hour).Unix(),
	})
	contradictorySigned, err := contradictory.SignedString([]byte("secret"))
	if err != nil {
		t.Fatalf("sign contradictory token failed: %v", err)
	}
	if _, err := NewVerifier("secret").Verify("Bearer " + contradictorySigned); errors.Is(err, ErrAccessTokenExpired) || err == nil {
		t.Fatalf("expired token with another invalid time claim must remain generic unauthorized, got %v", err)
	}
}

func TestGameProviderTokenIsShortAndCallbackOnly(t *testing.T) {
	t.Parallel()
	verifier := NewVerifier("secret")
	token, err := verifier.IssueGameProviderToken("fami", 399999999999990001, 24*time.Hour)
	if err != nil {
		t.Fatalf("IssueGameProviderToken failed: %v", err)
	}
	if len(token) > 512 {
		t.Fatalf("game provider token length=%d, want <=512", len(token))
	}
	claims, err := verifier.VerifyGameCallbackToken("Bearer " + token)
	if err != nil || claims.AppCode != "fami" || claims.UserID != 399999999999990001 {
		t.Fatalf("callback token claims mismatch: claims=%+v err=%v", claims, err)
	}
	if _, err := verifier.Verify("Bearer " + token); err == nil {
		t.Fatal("game provider token must not authenticate ordinary App APIs")
	}
}
