package auth

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"errors"
	"math/big"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

func TestOIDCVerifierAcceptsRS256AndES256StableClaims(t *testing.T) {
	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]any{"issuer": issuer, "jwks_uri": issuer + "/jwks"})
		case "/jwks":
			_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{
				rsaJWK("rsa-key", &rsaKey.PublicKey), ecJWK("ec-key", &ecKey.PublicKey),
			}})
		default:
			http.NotFound(w, request)
		}
	}))
	defer server.Close()
	issuer = server.URL
	verifier, err := NewOIDCVerifier(OIDCConfig{
		AppEnv: "test",
		Issuer: issuer, Audience: "gateway-api", TenantID: "tenant-1",
		RolePrefix: "gateway.", RequiredScopes: []string{"gateway.access"}, HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := verifier.ValidateConfiguration(context.Background()); err != nil {
		t.Fatalf("ValidateConfiguration() error = %v", err)
	}
	for _, test := range []struct {
		name   string
		kid    string
		method jwt.SigningMethod
		key    any
	}{{"rs256", "rsa-key", jwt.SigningMethodRS256, rsaKey}, {"es256", "ec-key", jwt.SigningMethodES256, ecKey}} {
		t.Run(test.name, func(t *testing.T) {
			token := signedOIDCToken(t, issuer, test.kid, test.method, test.key, nil)
			user, err := verifier.Verify(context.Background(), token)
			if err != nil {
				t.Fatal(err)
			}
			if user.ID != "platform-subject" || user.TenantID != "tenant-1" || user.Source != "oidc" ||
				user.Username != "acceptance" || user.DisplayName != "王小明" ||
				user.Email != "real.user@example.test" || user.Phone != "+8613800000000" ||
				user.AvatarURL != "https://static.example.test/avatar.png" ||
				len(user.Roles) != 1 || user.Roles[0] != "admin" {
				t.Fatalf("unexpected OIDC user: %#v", user)
			}
		})
	}

	unverified := signedOIDCToken(t, issuer, "rsa-key", jwt.SigningMethodRS256, rsaKey, func(claims jwt.MapClaims) {
		claims["email_verified"] = false
		claims["phone_number_verified"] = false
		claims["picture"] = "javascript:alert(1)"
	})
	user, err := verifier.Verify(context.Background(), unverified)
	if err != nil {
		t.Fatal(err)
	}
	if user.Email != "" || user.Phone != "" || user.AvatarURL != "" {
		t.Fatalf("untrusted profile claims were accepted: %#v", user)
	}
}

func TestOIDCVerifierRejectsMissingOrMismatchedSecurityClaims(t *testing.T) {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		if request.URL.Path == "/.well-known/openid-configuration" {
			_ = json.NewEncoder(w).Encode(map[string]any{"issuer": issuer, "jwks_uri": issuer + "/jwks"})
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{ecJWK("ec-key", &key.PublicKey)}})
	}))
	defer server.Close()
	issuer = server.URL
	verifier, _ := NewOIDCVerifier(OIDCConfig{
		AppEnv: "test",
		Issuer: issuer, Audience: "gateway-api", TenantID: "tenant-1", RolePrefix: "gateway.",
		RequiredScopes: []string{"gateway.access"}, HTTPClient: server.Client(),
	})
	tests := []struct {
		name, wantCategory string
		mutate             func(jwt.MapClaims)
	}{
		{"missing nbf", "NBF_MISSING", func(claims jwt.MapClaims) { delete(claims, "nbf") }},
		{"missing exp", "EXP_MISSING", func(claims jwt.MapClaims) { delete(claims, "exp") }},
		{"missing issuer", "ISSUER_MISSING", func(claims jwt.MapClaims) { delete(claims, "iss") }},
		{"missing audience", "AUDIENCE_MISSING", func(claims jwt.MapClaims) { delete(claims, "aud") }},
		{"wrong audience", "AUDIENCE_INVALID", func(claims jwt.MapClaims) { claims["aud"] = "other-api" }},
		{"missing context type", "STABLE_IDENTITY_CLAIMS_INVALID", func(claims jwt.MapClaims) { delete(claims, "context_type") }},
		{"unknown context type", "STABLE_IDENTITY_CLAIMS_INVALID", func(claims jwt.MapClaims) { claims["context_type"] = "account" }},
		{"wrong tenant", "STABLE_IDENTITY_CLAIMS_INVALID", func(claims jwt.MapClaims) { claims["tid"] = "tenant-2" }},
		{"missing scope", "REQUIRED_SCOPE_MISSING", func(claims jwt.MapClaims) { claims["scope"] = "openid" }},
		{"unmapped role", "MAPPED_ROLE_MISSING", func(claims jwt.MapClaims) { claims["roles"] = []string{"other.admin"} }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			raw := signedOIDCToken(t, issuer, "ec-key", jwt.SigningMethodES256, key, test.mutate)
			if _, err := verifier.Verify(context.Background(), raw); err == nil {
				t.Fatal("expected token to be rejected")
			} else {
				if !errors.Is(err, ErrUnauthorized) {
					t.Fatalf("validation error no longer wraps ErrUnauthorized: %v", err)
				}
				if category := OIDCValidationCategory(err); category != test.wantCategory {
					t.Fatalf("validation category=%q, want %q", category, test.wantCategory)
				}
			}
		})
	}
}

func TestOIDCVerifierAcceptsExplicitPlatformAndTenantContextsForMultiTenantApplication(t *testing.T) {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		if request.URL.Path == "/.well-known/openid-configuration" {
			_ = json.NewEncoder(w).Encode(map[string]any{
				"issuer":   issuer,
				"jwks_uri": issuer + "/jwks",
			})
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"keys": []any{ecJWK("ec-key", &key.PublicKey)},
		})
	}))
	defer server.Close()
	issuer = server.URL
	verifier, err := NewOIDCVerifier(OIDCConfig{
		AppEnv: "test",
		Issuer: issuer, Audience: "gateway-api",
		TenantMode:    "multi_tenant",
		ApplicationID: "11111111-1111-4111-8111-111111111111",
		RolePrefix:    "gateway.", HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	platformToken := signedOIDCToken(
		t,
		issuer,
		"ec-key",
		jwt.SigningMethodES256,
		key,
		func(claims jwt.MapClaims) {
			claims["context_type"] = "platform"
			delete(claims, "tid")
		},
	)
	platformUser, err := verifier.Verify(context.Background(), platformToken)
	if err != nil {
		t.Fatalf("platform context rejected: %v", err)
	}
	if platformUser.ContextType != "platform" || platformUser.TenantID != "" {
		t.Fatalf("platform user=%#v", platformUser)
	}
	tenantID := "22222222-2222-4222-8222-222222222222"
	tenantToken := signedOIDCToken(
		t,
		issuer,
		"ec-key",
		jwt.SigningMethodES256,
		key,
		func(claims jwt.MapClaims) {
			claims["context_type"] = "tenant"
			claims["tid"] = tenantID
		},
	)
	tenantUser, err := verifier.Verify(context.Background(), tenantToken)
	if err != nil {
		t.Fatalf("tenant context rejected: %v", err)
	}
	if tenantUser.ContextType != "tenant" || tenantUser.TenantID != tenantID {
		t.Fatalf("tenant user=%#v", tenantUser)
	}
	for name, mutate := range map[string]func(jwt.MapClaims){
		"platform with tenant": func(claims jwt.MapClaims) {
			claims["context_type"] = "platform"
		},
		"tenant without tenant": func(claims jwt.MapClaims) {
			claims["context_type"] = "tenant"
			delete(claims, "tid")
		},
	} {
		t.Run(name, func(t *testing.T) {
			raw := signedOIDCToken(
				t, issuer, "ec-key", jwt.SigningMethodES256, key, mutate,
			)
			if _, err := verifier.Verify(context.Background(), raw); err == nil {
				t.Fatal("context/tenant mismatch was accepted")
			}
		})
	}
}

func TestOIDCVerifierFailsClosedWhenIntrospectionMarksSessionInactive(t *testing.T) {
	key, _ := rsa.GenerateKey(rand.Reader, 2048)
	active := true
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]any{
				"issuer": issuer, "jwks_uri": issuer + "/jwks",
				"introspection_endpoint": issuer + "/introspect",
			})
		case "/jwks":
			_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{rsaJWK("rsa-key", &key.PublicKey)}})
		case "/introspect":
			clientID, clientSecret, ok := request.BasicAuth()
			if !ok || clientID != "gateway-api" || clientSecret != "introspection-secret" {
				w.WriteHeader(http.StatusUnauthorized)
				return
			}
			if err := request.ParseForm(); err != nil || request.Form.Get("token") == "" {
				w.WriteHeader(http.StatusBadRequest)
				return
			}
			_ = json.NewEncoder(w).Encode(map[string]any{"active": active})
		default:
			http.NotFound(w, request)
		}
	}))
	defer server.Close()
	issuer = server.URL
	verifier, err := NewOIDCVerifier(OIDCConfig{
		AppEnv: "test",
		Issuer: issuer, Audience: "gateway-api", TenantID: "tenant-1", RolePrefix: "gateway.",
		RequiredScopes: []string{"gateway.access"}, HTTPClient: server.Client(),
		IntrospectionEnabled: true,
		IntrospectionCredentialProvider: func(context.Context) (string, []byte, error) {
			return "gateway-api", []byte("introspection-secret"), nil
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	raw := signedOIDCToken(t, issuer, "rsa-key", jwt.SigningMethodRS256, key, nil)
	if _, err := verifier.Verify(context.Background(), raw); err != nil {
		t.Fatalf("active token rejected: %v", err)
	}
	active = false
	if _, err := verifier.Verify(context.Background(), raw); err == nil {
		t.Fatal("inactive token was accepted")
	}
}

func TestOIDCSecurityEventEvaluationUsesWatermarkAndFallbackIntrospection(t *testing.T) {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	active, introspectionAvailable, introspectionCalls := true, true, 0
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]any{"issuer": issuer, "jwks_uri": issuer + "/jwks", "introspection_endpoint": issuer + "/introspect"})
		case "/jwks":
			_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{ecJWK("ec-key", &key.PublicKey)}})
		case "/introspect":
			introspectionCalls++
			if !introspectionAvailable {
				w.WriteHeader(http.StatusServiceUnavailable)
				return
			}
			_ = json.NewEncoder(w).Encode(map[string]any{"active": active})
		}
	}))
	defer server.Close()
	issuer = server.URL
	evaluation := OIDCSecurityEventEvaluation{RequireIntrospection: true}
	verifier, err := NewOIDCVerifier(OIDCConfig{
		AppEnv: "test",
		Issuer: issuer, Audience: "gateway-api", TenantID: "tenant-1", RolePrefix: "gateway.",
		RequiredScopes: []string{"gateway.access"}, HTTPClient: server.Client(),
		IntrospectionEnabled:  true,
		IntrospectionClientID: "gateway-introspection", IntrospectionClientSecret: "introspection-secret",
		SecurityEventEvaluator: func(_ context.Context, identity OIDCSecurityEventIdentity) (OIDCSecurityEventEvaluation, error) {
			if identity.Subject != "platform-subject" {
				t.Fatal("security event evaluator received incomplete identity")
			}
			return evaluation, nil
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	raw := signedOIDCToken(t, issuer, "ec-key", jwt.SigningMethodES256, key, nil)
	if _, err := verifier.Verify(context.Background(), raw); err != nil || introspectionCalls != 1 {
		t.Fatalf("fallback token error=%v calls=%d", err, introspectionCalls)
	}
	evaluation = OIDCSecurityEventEvaluation{Enabled: true}
	if _, err := verifier.Verify(context.Background(), raw); err != nil || introspectionCalls != 1 {
		t.Fatalf("healthy push token error=%v calls=%d", err, introspectionCalls)
	}
	evaluation = OIDCSecurityEventEvaluation{Revoked: true}
	if _, err := verifier.Verify(context.Background(), raw); err == nil || introspectionCalls != 1 {
		t.Fatalf("revoked token error=%v calls=%d", err, introspectionCalls)
	}
	evaluation = OIDCSecurityEventEvaluation{RequireIntrospection: true}
	introspectionAvailable = false
	_, err = verifier.Verify(context.Background(), raw)
	var requestError *RequestAuthError
	if !errors.As(err, &requestError) || requestError.Status != http.StatusServiceUnavailable {
		t.Fatalf("introspection outage error=%T %v", err, err)
	}
	withoutIssuedAt := signedOIDCToken(t, issuer, "ec-key", jwt.SigningMethodES256, key, func(claims jwt.MapClaims) { delete(claims, "iat") })
	evaluation = OIDCSecurityEventEvaluation{}
	introspectionAvailable = true
	if _, err := verifier.Verify(context.Background(), withoutIssuedAt); err != nil {
		t.Fatalf("unconfigured security events unexpectedly required iat: %v", err)
	}
	evaluation = OIDCSecurityEventEvaluation{Enabled: true}
	if _, err := verifier.Verify(context.Background(), withoutIssuedAt); err == nil {
		t.Fatal("configured security events accepted a token without iat")
	}
}

func TestOIDCVerifierRejectsLoopbackHTTPDiscoveryEndpointsInProduction(t *testing.T) {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	var issuer, jwksURI, introspectionEndpoint string
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]string{
				"issuer": issuer, "jwks_uri": jwksURI, "introspection_endpoint": introspectionEndpoint,
			})
		case "/jwks":
			_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{rsaJWK("rsa-key", &key.PublicKey)}})
		default:
			http.NotFound(w, request)
		}
	}))
	defer server.Close()
	issuer = server.URL

	for _, test := range []struct {
		name          string
		jwks          string
		introspection string
	}{
		{name: "JWKS", jwks: "http://127.0.0.1:1/jwks"},
		{name: "introspection", jwks: issuer + "/jwks", introspection: "http://localhost:1/introspect"},
	} {
		t.Run(test.name, func(t *testing.T) {
			jwksURI, introspectionEndpoint = test.jwks, test.introspection
			config := OIDCConfig{
				AppEnv: "production", Issuer: issuer, Audience: "gateway-api", TenantID: "tenant-1", RolePrefix: "gateway.",
				HTTPClient: server.Client(),
			}
			if test.introspection != "" {
				config.IntrospectionEnabled = true
				config.IntrospectionCredentialProvider = func(context.Context) (string, []byte, error) {
					return "gateway-machine", []byte("machine-secret-long-enough"), nil
				}
			}
			verifier, createErr := NewOIDCVerifier(config)
			if createErr != nil {
				t.Fatal(createErr)
			}
			if validateErr := verifier.ValidateConfiguration(context.Background()); validateErr == nil {
				t.Fatalf("production accepted loopback HTTP %s endpoint", test.name)
			}
		})
	}
}

func TestOIDCURLPolicyAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) {
	for _, appEnv := range []string{"", "production", "staging"} {
		if err := validatePublicURL("http://127.0.0.2:18003/issuer/easyai", appEnv); err == nil {
			t.Fatalf("%s accepted loopback HTTP OIDC URL", appEnv)
		}
	}
	for _, appEnv := range []string{"local", "development", "dev", "test"} {
		if err := validatePublicURL("http://127.0.0.2:18003/issuer/easyai", appEnv); err != nil {
			t.Fatalf("%s rejected loopback HTTP OIDC URL: %v", appEnv, err)
		}
	}
}

func signedOIDCToken(t *testing.T, issuer, kid string, method jwt.SigningMethod, key any, mutate func(jwt.MapClaims)) string {
	t.Helper()
	now := time.Now()
	claims := jwt.MapClaims{
		"iss": issuer, "aud": "gateway-api", "sub": "platform-subject", "tid": "tenant-1",
		"context_type":       "tenant",
		"preferred_username": "acceptance", "roles": []string{"gateway.admin"},
		"name": "王小明", "email": "real.user@example.test", "email_verified": true,
		"phone_number": "+8613800000000", "phone_number_verified": true,
		"picture": "https://static.example.test/avatar.png",
		"scope":   "openid gateway.access", "iat": now.Unix(), "nbf": now.Add(-time.Second).Unix(),
		"exp": now.Add(time.Hour).Unix(),
	}
	if mutate != nil {
		mutate(claims)
	}
	token := jwt.NewWithClaims(method, claims)
	token.Header["kid"] = kid
	raw, err := token.SignedString(key)
	if err != nil {
		t.Fatal(err)
	}
	return raw
}

func rsaJWK(kid string, key *rsa.PublicKey) map[string]any {
	return map[string]any{
		"kid": kid, "kty": "RSA", "use": "sig", "alg": "RS256",
		"n": base64.RawURLEncoding.EncodeToString(key.N.Bytes()),
		"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.E)).Bytes()),
	}
}

func ecJWK(kid string, key *ecdsa.PublicKey) map[string]any {
	return map[string]any{
		"kid": kid, "kty": "EC", "use": "sig", "alg": "ES256", "crv": "P-256",
		"x": base64.RawURLEncoding.EncodeToString(key.X.FillBytes(make([]byte, 32))),
		"y": base64.RawURLEncoding.EncodeToString(key.Y.FillBytes(make([]byte, 32))),
	}
}
