package auth_test

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"maps"
	"math/big"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"

	jwt "github.com/golang-jwt/jwt/v5"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	authservice "hyapp/services/user-service/internal/service/auth"
)

func TestFirebaseThirdPartyVerifierAcceptsGoogleFirebaseIDToken(t *testing.T) {
	// Firebase verifier 必须从已签名 ID token 中抽取 sub/uid，而不是信任客户端直传 account id。
	now := time.Unix(2000, 0)
	privateKey, certPEM := firebaseTestKeyPair(t, now)
	certsServer := firebaseCertsServer(t, "kid-1", certPEM)
	verifier := authservice.NewFirebaseThirdPartyVerifier(authservice.FirebaseVerifierConfig{
		ProjectID:              "hyapp-test",
		AllowedSignInProviders: []string{"google.com"},
		CertsURL:               certsServer.URL,
		Now:                    func() time.Time { return now },
	})
	token := signFirebaseTestToken(t, privateKey, "kid-1", map[string]any{
		"iss":       "https://securetoken.google.com/hyapp-test",
		"aud":       "hyapp-test",
		"sub":       "firebase-uid-1",
		"iat":       now.Unix(),
		"exp":       now.Add(time.Hour).Unix(),
		"auth_time": now.Add(-time.Minute).Unix(),
		"firebase":  map[string]any{"sign_in_provider": "google.com"},
	})

	profile, err := verifier.Verify(context.Background(), "firebase", token)
	if err != nil {
		t.Fatalf("Verify failed: %v", err)
	}
	if profile.ProviderSubject != "firebase-uid-1" {
		t.Fatalf("provider_subject must come from Firebase sub: %+v", profile)
	}
}

func TestAppFirebaseThirdPartyVerifierRoutesByAppCode(t *testing.T) {
	// Firebase ID token 的 aud/iss 必须跟当前 app_code 对应；Huwaa 不能再被 Lalu project_id 误拒。
	now := time.Unix(2000, 0)
	privateKey, certPEM := firebaseTestKeyPair(t, now)
	certsServer := firebaseCertsServer(t, "kid-1", certPEM)
	verifier := authservice.NewAppFirebaseThirdPartyVerifier(authservice.AppFirebaseVerifierConfig{
		Default: authservice.FirebaseVerifierConfig{
			ProjectID:              "lalu-party",
			AllowedSignInProviders: []string{"google.com"},
			CertsURL:               certsServer.URL,
			Now:                    func() time.Time { return now },
		},
		Projects: map[string]authservice.FirebaseVerifierConfig{
			"lalu": {
				ProjectID: "lalu-party",
			},
			"huwaa": {
				ProjectID: "huwaa-6f3aa",
			},
		},
	})
	huwaaToken := signFirebaseProjectToken(t, privateKey, "kid-1", "huwaa-6f3aa", "huwaa-firebase-uid", now)
	laluToken := signFirebaseProjectToken(t, privateKey, "kid-1", "lalu-party", "lalu-firebase-uid", now)

	profile, err := verifier.Verify(appcode.WithContext(context.Background(), "huwaa"), "firebase", huwaaToken)
	if err != nil {
		t.Fatalf("huwaa token should verify against huwaa project: %v", err)
	}
	if profile.ProviderSubject != "huwaa-firebase-uid" {
		t.Fatalf("huwaa provider subject mismatch: %+v", profile)
	}
	if _, err := verifier.Verify(appcode.WithContext(context.Background(), "huwaa"), "firebase", laluToken); !xerr.IsCode(err, xerr.AuthFailed) {
		t.Fatalf("huwaa app must reject lalu Firebase token, got %v", err)
	}
	if _, err := verifier.Verify(appcode.WithContext(context.Background(), "lalu"), "firebase", huwaaToken); !xerr.IsCode(err, xerr.AuthFailed) {
		t.Fatalf("lalu app must reject huwaa Firebase token, got %v", err)
	}
}

func TestFirebaseThirdPartyVerifierRejectsInvalidClaims(t *testing.T) {
	// AUTH_FAILED 分支必须覆盖 provider、audience、登录方式和过期时间，避免身份枚举和配置泄漏。
	now := time.Unix(2000, 0)
	privateKey, certPEM := firebaseTestKeyPair(t, now)
	certsServer := firebaseCertsServer(t, "kid-1", certPEM)
	verifier := authservice.NewFirebaseThirdPartyVerifier(authservice.FirebaseVerifierConfig{
		ProjectID:              "hyapp-test",
		AllowedSignInProviders: []string{"google.com"},
		CertsURL:               certsServer.URL,
		Now:                    func() time.Time { return now },
	})
	validClaims := map[string]any{
		"iss":       "https://securetoken.google.com/hyapp-test",
		"aud":       "hyapp-test",
		"sub":       "firebase-uid-1",
		"iat":       now.Unix(),
		"exp":       now.Add(time.Hour).Unix(),
		"auth_time": now.Add(-time.Minute).Unix(),
		"firebase":  map[string]any{"sign_in_provider": "google.com"},
	}

	cases := []struct {
		name     string
		provider string
		mutate   func(map[string]any)
	}{
		{
			name:     "wrong provider",
			provider: "google",
		},
		{
			name:     "wrong audience",
			provider: "firebase",
			mutate: func(claims map[string]any) {
				claims["aud"] = "other-project"
			},
		},
		{
			name:     "wrong sign in provider",
			provider: "firebase",
			mutate: func(claims map[string]any) {
				claims["firebase"] = map[string]any{"sign_in_provider": "password"}
			},
		},
		{
			name:     "expired token",
			provider: "firebase",
			mutate: func(claims map[string]any) {
				claims["exp"] = now.Add(-time.Minute).Unix()
			},
		},
		{
			name:     "empty subject",
			provider: "firebase",
			mutate: func(claims map[string]any) {
				claims["sub"] = ""
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			claims := cloneFirebaseClaims(validClaims)
			if tc.mutate != nil {
				tc.mutate(claims)
			}
			token := signFirebaseTestToken(t, privateKey, "kid-1", claims)
			_, err := verifier.Verify(context.Background(), tc.provider, token)
			if !xerr.IsCode(err, xerr.AuthFailed) {
				t.Fatalf("expected AUTH_FAILED, got %v", err)
			}
		})
	}
}

func TestFirebaseThirdPartyVerifierRejectsUnknownKidWithoutRefreshingValidCache(t *testing.T) {
	// 缓存有效期内的未知 kid 只能本地拒绝，不能让随机 token 放大成 Firebase 证书端点请求。
	now := time.Unix(2000, 0)
	privateKey, certPEM := firebaseTestKeyPair(t, now)
	var fetches atomic.Int32
	certsServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		fetches.Add(1)
		writer.Header().Set("Cache-Control", "public, max-age=3600")
		_ = json.NewEncoder(writer).Encode(map[string]string{"kid-1": certPEM})
	}))
	t.Cleanup(certsServer.Close)
	verifier := authservice.NewFirebaseThirdPartyVerifier(authservice.FirebaseVerifierConfig{
		ProjectID:              "hyapp-test",
		AllowedSignInProviders: []string{"google.com"},
		CertsURL:               certsServer.URL,
		Now:                    func() time.Time { return now },
	})
	claims := map[string]any{
		"iss":       "https://securetoken.google.com/hyapp-test",
		"aud":       "hyapp-test",
		"sub":       "firebase-uid-1",
		"iat":       now.Unix(),
		"exp":       now.Add(time.Hour).Unix(),
		"auth_time": now.Add(-time.Minute).Unix(),
		"firebase":  map[string]any{"sign_in_provider": "google.com"},
	}
	if _, err := verifier.Verify(context.Background(), "firebase", signFirebaseTestToken(t, privateKey, "kid-1", claims)); err != nil {
		t.Fatalf("initial valid token should warm cache: %v", err)
	}
	if fetches.Load() != 1 {
		t.Fatalf("valid token should fetch certs once, got %d", fetches.Load())
	}

	_, err := verifier.Verify(context.Background(), "firebase", signFirebaseTestToken(t, privateKey, "random-kid", claims))
	if !xerr.IsCode(err, xerr.AuthFailed) {
		t.Fatalf("expected AUTH_FAILED for unknown kid, got %v", err)
	}
	if fetches.Load() != 1 {
		t.Fatalf("unknown kid must not refresh valid cert cache, got fetches=%d", fetches.Load())
	}
}

func TestRoutedThirdPartyVerifierNeverRoutesFirebaseToStaticVerifier(t *testing.T) {
	// 即使 static allowlist 误配了 firebase，provider=firebase 也必须走真实 Firebase verifier 并 fail-closed。
	verifier := authservice.NewRoutedThirdPartyVerifier(nil, authservice.NewStaticThirdPartyVerifier([]string{"firebase", "wechat"}))
	if _, err := verifier.Verify(context.Background(), "firebase", "client-supplied-subject"); !xerr.IsCode(err, xerr.AuthFailed) {
		t.Fatalf("firebase must not fall back to static verifier, got %v", err)
	}
	profile, err := verifier.Verify(context.Background(), "wechat", "mock-openid")
	if err != nil || profile.ProviderSubject != "mock-openid" {
		t.Fatalf("non-firebase mock provider should still use fallback: profile=%+v err=%v", profile, err)
	}
}

func firebaseTestKeyPair(t *testing.T, now time.Time) (*rsa.PrivateKey, string) {
	t.Helper()
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("generate rsa key failed: %v", err)
	}
	certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
		SerialNumber: big.NewInt(1),
		NotBefore:    now.Add(-time.Hour),
		NotAfter:     now.Add(time.Hour),
	}, &x509.Certificate{
		SerialNumber: big.NewInt(1),
		NotBefore:    now.Add(-time.Hour),
		NotAfter:     now.Add(time.Hour),
	}, &privateKey.PublicKey, privateKey)
	if err != nil {
		t.Fatalf("create certificate failed: %v", err)
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
	return privateKey, string(certPEM)
}

func firebaseCertsServer(t *testing.T, kid string, certPEM string) *httptest.Server {
	t.Helper()
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("Cache-Control", "public, max-age=3600")
		_ = json.NewEncoder(writer).Encode(map[string]string{kid: certPEM})
	}))
	t.Cleanup(server.Close)
	return server
}

func signFirebaseTestToken(t *testing.T, privateKey *rsa.PrivateKey, kid string, claims map[string]any) string {
	t.Helper()
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(claims))
	token.Header["kid"] = kid
	signed, err := token.SignedString(privateKey)
	if err != nil {
		t.Fatalf("sign firebase token failed: %v", err)
	}
	return signed
}

func signFirebaseProjectToken(t *testing.T, privateKey *rsa.PrivateKey, kid string, projectID string, subject string, now time.Time) string {
	t.Helper()
	return signFirebaseTestToken(t, privateKey, kid, map[string]any{
		"iss":       "https://securetoken.google.com/" + projectID,
		"aud":       projectID,
		"sub":       subject,
		"iat":       now.Unix(),
		"exp":       now.Add(time.Hour).Unix(),
		"auth_time": now.Add(-time.Minute).Unix(),
		"firebase":  map[string]any{"sign_in_provider": "google.com"},
	})
}

func cloneFirebaseClaims(input map[string]any) map[string]any {
	result := make(map[string]any, len(input))
	maps.Copy(result, input)
	return result
}
