package auth

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

func TestOIDCPublicClientUsesAuthorizationCodePKCES256WithoutSecret(t *testing.T) {
	const pkceVerifier = "test-pkce-verifier-with-at-least-43-characters-1234"
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]string{
				"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
				"token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks",
				"revocation_endpoint":  issuer + "/revoke",
				"end_session_endpoint": issuer + "/logout",
			})
		case "/token":
			body, _ := io.ReadAll(r.Body)
			values, _ := url.ParseQuery(string(body))
			if values.Get("client_secret") != "" || strings.Contains(r.Header.Get("Authorization"), "Basic") {
				t.Error("public client token request must not contain client credentials")
				http.Error(w, "invalid client authentication", http.StatusBadRequest)
				return
			}
			if values.Get("grant_type") != "authorization_code" || values.Get("client_id") != "gateway-public" || values.Get("code_verifier") != pkceVerifier {
				t.Error("token request omitted authorization code, public client id, or PKCE verifier")
				http.Error(w, "invalid token request", http.StatusBadRequest)
				return
			}
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]any{
				"access_token": "access-token", "refresh_token": "refresh-token",
				"id_token": "id-token", "expires_in": 300, "token_type": "Bearer",
			})
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()
	issuer = server.URL

	client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
		AppEnv: "test",
		Issuer: issuer, ClientID: "gateway-public", RedirectURI: "https://gateway.example.com/gateway-api/api/v1/auth/oidc/callback",
		PostLogoutRedirectURI: "https://gateway.example.com/", Scopes: []string{"openid", "profile", "gateway.access"}, HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := client.ValidateConfiguration(context.Background()); err != nil {
		t.Fatalf("ValidateConfiguration() error = %v", err)
	}
	tenantHint := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
	authorizationURL, err := client.AuthorizationURL(
		context.Background(), "state", "nonce", pkceVerifier, "tenant", tenantHint,
	)
	if err != nil {
		t.Fatal(err)
	}
	parsed, _ := url.Parse(authorizationURL)
	query := parsed.Query()
	digest := sha256.Sum256([]byte(pkceVerifier))
	expectedChallenge := base64.RawURLEncoding.EncodeToString(digest[:])
	if query.Get("response_type") != "code" || query.Get("code_challenge_method") != "S256" || query.Get("code_challenge") != expectedChallenge {
		t.Fatalf("authorization request is not PKCE S256: %v", query)
	}
	if query.Get("tenant_hint") != tenantHint {
		t.Fatalf("tenant_hint = %q, want %q", query.Get("tenant_hint"), tenantHint)
	}
	if query.Get("context_type") != "tenant" {
		t.Fatalf("context_type = %q, want tenant", query.Get("context_type"))
	}
	if _, err := client.ExchangeCode(context.Background(), "authorization-code", pkceVerifier); err != nil {
		t.Fatal(err)
	}
}

func TestOIDCPublicClientOmitsOptionalContextForUnifiedLogin(t *testing.T) {
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/.well-known/openid-configuration" {
			http.NotFound(w, r)
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]string{
			"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
			"token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks",
		})
	}))
	defer server.Close()
	issuer = server.URL

	client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
		AppEnv: "test", Issuer: issuer, ClientID: "gateway-public",
		RedirectURI:           "https://gateway.example.com/callback",
		PostLogoutRedirectURI: "https://gateway.example.com/",
		HTTPClient:            server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	authorizationURL, err := client.AuthorizationURL(
		context.Background(), "state", "nonce",
		"test-pkce-verifier-with-at-least-43-characters-1234", "", "",
	)
	if err != nil {
		t.Fatal(err)
	}
	parsed, _ := url.Parse(authorizationURL)
	if parsed.Query().Has("context_type") || parsed.Query().Has("tenant_hint") {
		t.Fatalf("unified login leaked optional context binding: %v", parsed.Query())
	}
}

func TestOIDCPublicClientRejectsOfflineAccess(t *testing.T) {
	_, err := NewOIDCPublicClient(OIDCPublicClientConfig{
		AppEnv: "production",
		Issuer: "https://auth.example.com", ClientID: "gateway-public",
		RedirectURI:           "https://gateway.example.com/api/v1/auth/oidc/callback",
		PostLogoutRedirectURI: "https://gateway.example.com/",
		Scopes:                []string{"openid", "gateway.access", "offline_access"},
	})
	if err == nil || !strings.Contains(err.Error(), "offline_access") {
		t.Fatalf("NewOIDCPublicClient() error = %v, want offline_access rejection", err)
	}
}

func TestOIDCPublicClientVerifiesIDTokenNonceAndAudience(t *testing.T) {
	key, 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, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]any{
				"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
				"token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks",
				"id_token_signing_alg_values_supported": []string{"RS256", "ES256"},
			})
		case "/jwks":
			_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{ecJWK("ec-key", &key.PublicKey)}})
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()
	issuer = server.URL

	client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
		AppEnv: "test",
		Issuer: issuer, ClientID: "gateway-public-client", RedirectURI: "https://gateway.example.com/callback",
		PostLogoutRedirectURI: "https://gateway.example.com/", HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	idToken := signedOIDCToken(t, issuer, "ec-key", jwt.SigningMethodES256, key, func(claims jwt.MapClaims) {
		claims["aud"] = "gateway-public-client"
		claims["nonce"] = "expected-nonce"
	})
	subject, err := client.VerifyIDToken(context.Background(), idToken, "expected-nonce")
	if err != nil || subject != "platform-subject" {
		t.Fatalf("VerifyIDToken() subject=%q err=%v", subject, err)
	}
	if _, err := client.VerifyIDToken(context.Background(), idToken, "wrong-nonce"); err == nil {
		t.Fatal("ID token with mismatched nonce was accepted")
	}
	for _, test := range []struct {
		name   string
		mutate func(jwt.MapClaims)
	}{
		{name: "wrong audience", mutate: func(claims jwt.MapClaims) { claims["aud"] = "other-client" }},
		{name: "missing nbf", mutate: func(claims jwt.MapClaims) { delete(claims, "nbf") }},
	} {
		t.Run(test.name, func(t *testing.T) {
			invalid := signedOIDCToken(t, issuer, "ec-key", jwt.SigningMethodES256, key, func(claims jwt.MapClaims) {
				claims["aud"] = "gateway-public-client"
				claims["nonce"] = "expected-nonce"
				test.mutate(claims)
			})
			if _, err := client.VerifyIDToken(context.Background(), invalid, "expected-nonce"); err == nil {
				t.Fatal("invalid ID token was accepted")
			}
		})
	}
}

func TestOIDCPublicClientRefreshAndRevokeNeverSendSecret(t *testing.T) {
	var issuer string
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/.well-known/openid-configuration":
			_ = json.NewEncoder(w).Encode(map[string]string{
				"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
				"token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks",
				"revocation_endpoint":  issuer + "/revoke",
				"end_session_endpoint": issuer + "/logout",
			})
		case "/token", "/revoke":
			requests++
			body, _ := io.ReadAll(r.Body)
			values, _ := url.ParseQuery(string(body))
			if values.Get("client_secret") != "" || r.Header.Get("Authorization") != "" {
				t.Error("public client request contained client authentication")
				http.Error(w, "invalid client authentication", http.StatusBadRequest)
				return
			}
			if r.URL.Path == "/token" {
				if values.Get("grant_type") != "refresh_token" || values.Get("refresh_token") != "old-refresh" {
					t.Error("refresh request omitted grant type or refresh token")
					http.Error(w, "invalid refresh request", http.StatusBadRequest)
					return
				}
				w.Header().Set("Content-Type", "application/json")
				_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "new-access", "refresh_token": "new-refresh", "expires_in": 300})
				return
			}
			w.WriteHeader(http.StatusOK)
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()
	issuer = server.URL
	client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
		AppEnv: "test",
		Issuer: issuer, ClientID: "gateway-public", RedirectURI: "https://gateway.example.com/callback",
		PostLogoutRedirectURI: "https://gateway.example.com/", HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	if _, err := client.Refresh(context.Background(), "old-refresh"); err != nil {
		t.Fatal(err)
	}
	if err := client.RevokeRefreshToken(context.Background(), "new-refresh"); err != nil {
		t.Fatal(err)
	}
	if requests != 2 {
		t.Fatalf("requests = %d, want 2", requests)
	}
}

func TestOIDCPublicClientMapsInvalidGrantWithoutLeakingProviderResponse(t *testing.T) {
	var issuer string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/.well-known/openid-configuration":
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{
				"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
				"token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks",
			})
		case "/token":
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte(`{"error":"invalid_grant","error_description":"sensitive-provider-detail"}`))
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()
	issuer = server.URL

	client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
		AppEnv: "test",
		Issuer: issuer, ClientID: "gateway-public", RedirectURI: "https://gateway.example.com/callback",
		PostLogoutRedirectURI: "https://gateway.example.com/", HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	_, err = client.Refresh(context.Background(), "redacted-refresh-token")
	if !errors.Is(err, ErrOIDCInvalidGrant) || strings.Contains(err.Error(), "sensitive-provider-detail") {
		t.Fatalf("invalid_grant mapping was not stable and redacted: %v", err)
	}
}

func TestOIDCPublicClientRejectsLoopbackHTTPDiscoveryEndpointsInProduction(t *testing.T) {
	var issuer, insecureField string
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		if r.URL.Path != "/.well-known/openid-configuration" {
			http.NotFound(w, r)
			return
		}
		metadata := map[string]any{
			"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
			"token_endpoint": issuer + "/token", "jwks_uri": issuer + "/jwks",
			"revocation_endpoint": issuer + "/revoke", "end_session_endpoint": issuer + "/logout",
			"id_token_signing_alg_values_supported": []string{"RS256", "ES256"},
		}
		metadata[insecureField] = "http://127.0.0.1:1/oidc-endpoint"
		_ = json.NewEncoder(w).Encode(metadata)
	}))
	defer server.Close()
	issuer = server.URL

	for _, field := range []string{
		"authorization_endpoint", "token_endpoint", "jwks_uri", "revocation_endpoint", "end_session_endpoint",
	} {
		t.Run(field, func(t *testing.T) {
			insecureField = field
			client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
				AppEnv: "production", Issuer: issuer, ClientID: "gateway-public",
				RedirectURI: "https://gateway.example.com/callback", PostLogoutRedirectURI: "https://gateway.example.com/",
				HTTPClient: server.Client(),
			})
			if err != nil {
				t.Fatal(err)
			}
			if err := client.ValidateConfiguration(context.Background()); err == nil {
				t.Fatalf("production accepted loopback HTTP %s", field)
			}
		})
	}
}
