package auth

import (
	"context"
	"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 TestOIDCIDTokenVerifierValidatesSignatureAndIdentityClaims(t *testing.T) {
	key := oidcTestKey(t)
	server := oidcTestJWKSServer(t, "key-1", &key.PublicKey, http.StatusOK)
	now := time.Unix(1_800_000_000, 0)
	verifier := oidcTestVerifier(t, server.URL, now)
	raw := oidcTestToken(t, key, "key-1", jwt.MapClaims{
		"iss": "https://issuer.example", "aud": "client-1", "sub": "subject-1",
		"email": "ada@example.test", "email_verified": true,
		"iat": now.Add(-time.Minute).Unix(), "exp": now.Add(time.Hour).Unix(),
	})
	identity, err := verifier.Verify(context.Background(), raw)
	if err != nil {
		t.Fatal(err)
	}
	if identity.Subject != "subject-1" || identity.Email != "ada@example.test" {
		t.Fatalf("identity = %#v", identity)
	}
}

func TestOIDCIDTokenVerifierRejectsWrongTrustClaims(t *testing.T) {
	key := oidcTestKey(t)
	server := oidcTestJWKSServer(t, "key-1", &key.PublicKey, http.StatusOK)
	now := time.Unix(1_800_000_000, 0)
	verifier := oidcTestVerifier(t, server.URL, now)
	tests := []struct {
		name   string
		claims jwt.MapClaims
	}{
		{name: "issuer", claims: jwt.MapClaims{
			"iss": "https://attacker.example", "aud": "client-1", "sub": "subject-1",
			"email": "ada@example.test", "email_verified": true, "exp": now.Add(time.Hour).Unix(),
		}},
		{name: "audience", claims: jwt.MapClaims{
			"iss": "https://issuer.example", "aud": "other-client", "sub": "subject-1",
			"email": "ada@example.test", "email_verified": true, "exp": now.Add(time.Hour).Unix(),
		}},
		{name: "authorized party", claims: jwt.MapClaims{
			"iss": "https://issuer.example", "aud": []string{"client-1", "other-client"}, "azp": "other-client", "sub": "subject-1",
			"email": "ada@example.test", "email_verified": true, "exp": now.Add(time.Hour).Unix(),
		}},
		{name: "unverified email", claims: jwt.MapClaims{
			"iss": "https://issuer.example", "aud": "client-1", "sub": "subject-1",
			"email": "ada@example.test", "email_verified": false, "exp": now.Add(time.Hour).Unix(),
		}},
		{name: "expired", claims: jwt.MapClaims{
			"iss": "https://issuer.example", "aud": "client-1", "sub": "subject-1",
			"email": "ada@example.test", "email_verified": true, "exp": now.Add(-time.Hour).Unix(),
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			raw := oidcTestToken(t, key, "key-1", test.claims)
			_, err := verifier.Verify(context.Background(), raw)
			var invalid *OIDCInvalidTokenError
			if !errors.As(err, &invalid) {
				t.Fatalf("error = %T %v", err, err)
			}
		})
	}
}

func TestOIDCIDTokenVerifierClassifiesJWKSOutageAndHonorsCancellation(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	key := oidcTestKey(t)
	server := oidcTestJWKSServer(t, "key-1", &key.PublicKey, http.StatusServiceUnavailable)
	verifier := oidcTestVerifier(t, server.URL, now)
	raw := oidcTestToken(t, key, "key-1", jwt.MapClaims{
		"iss": "https://issuer.example", "aud": "client-1", "sub": "subject-1",
		"email": "ada@example.test", "email_verified": true, "exp": now.Add(time.Hour).Unix(),
	})
	_, err := verifier.Verify(context.Background(), raw)
	var unavailable *OIDCProviderUnavailableError
	if !errors.As(err, &unavailable) {
		t.Fatalf("outage error = %T %v", err, err)
	}
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	if _, err := verifier.Verify(canceled, raw); !errors.Is(err, context.Canceled) {
		t.Fatalf("cancellation error = %v", err)
	}
}

func TestOIDCIDTokenVerifierRejectsJWKSRedirectWithoutContactingTarget(t *testing.T) {
	key := oidcTestKey(t)
	redirectTargetCalls := 0
	redirectTarget := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		redirectTargetCalls++
		writeOIDCTestJWKS(t, writer, "key-1", &key.PublicKey)
	}))
	t.Cleanup(redirectTarget.Close)
	redirectSource := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		http.Redirect(writer, request, redirectTarget.URL, http.StatusFound)
	}))
	t.Cleanup(redirectSource.Close)

	now := time.Unix(1_800_000_000, 0)
	sharedClient := &http.Client{}
	originalRedirectPolicyCalls := 0
	sharedClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
		originalRedirectPolicyCalls++
		return nil
	}
	verifier, err := NewOIDCIDTokenVerifier(OIDCProviderConfig{
		Audience: "client-1", TrustedIssuers: []string{"https://issuer.example"},
		JWKSURL: redirectSource.URL, HTTPClient: sharedClient,
		Now: func() time.Time { return now },
	})
	if err != nil {
		t.Fatal(err)
	}
	raw := oidcTestToken(t, key, "key-1", jwt.MapClaims{
		"iss": "https://issuer.example", "aud": "client-1", "sub": "subject-1",
		"email": "ada@example.test", "email_verified": true, "exp": now.Add(time.Hour).Unix(),
	})
	_, err = verifier.Verify(context.Background(), raw)
	var unavailable *OIDCProviderUnavailableError
	if !errors.As(err, &unavailable) {
		t.Fatalf("redirect error = %T %v", err, err)
	}
	if redirectTargetCalls != 0 || originalRedirectPolicyCalls != 0 {
		t.Fatalf("redirect escaped configured trust anchor: target=%d shared-policy=%d", redirectTargetCalls, originalRedirectPolicyCalls)
	}
	response, err := sharedClient.Get(redirectSource.URL)
	if err != nil {
		t.Fatal(err)
	}
	response.Body.Close()
	if redirectTargetCalls != 1 || originalRedirectPolicyCalls != 1 {
		t.Fatalf("verifier mutated caller-owned redirect policy: target=%d shared-policy=%d", redirectTargetCalls, originalRedirectPolicyCalls)
	}
}

func TestOIDCIDTokenVerifierSlowRefreshDoesNotBlockCanceledPeer(t *testing.T) {
	key := oidcTestKey(t)
	started := make(chan struct{}, 2)
	release := make(chan struct{})
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		started <- struct{}{}
		<-release
		writeOIDCTestJWKS(t, writer, "key-1", &key.PublicKey)
	}))
	t.Cleanup(server.Close)
	now := time.Unix(1_800_000_000, 0)
	verifier := oidcTestVerifier(t, server.URL, now)
	raw := oidcTestToken(t, key, "key-1", jwt.MapClaims{
		"iss": "https://issuer.example", "aud": "client-1", "sub": "subject-1",
		"email": "ada@example.test", "email_verified": true, "exp": now.Add(time.Hour).Unix(),
	})
	firstDone := make(chan error, 1)
	go func() {
		_, err := verifier.Verify(context.Background(), raw)
		firstDone <- err
	}()
	<-started
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	secondDone := make(chan error, 1)
	go func() {
		_, err := verifier.Verify(canceled, raw)
		secondDone <- err
	}()
	select {
	case err := <-secondDone:
		if !errors.Is(err, context.Canceled) {
			t.Fatalf("canceled peer error = %v", err)
		}
	case <-time.After(250 * time.Millisecond):
		t.Fatal("canceled verification remained blocked behind JWKS refresh")
	}
	close(release)
	if err := <-firstDone; err != nil {
		t.Fatal(err)
	}
}

func TestOIDCIDTokenVerifierCapsHugeCacheAgeWithoutOverflow(t *testing.T) {
	verifier := &OIDCIDTokenVerifier{defaultTTL: time.Hour, maxTTL: 24 * time.Hour}
	if got := verifier.cacheTTL("max-age=9223372036854775807"); got != 24*time.Hour {
		t.Fatalf("cache TTL = %v", got)
	}
}

func oidcTestVerifier(t *testing.T, jwksURL string, now time.Time) *OIDCIDTokenVerifier {
	t.Helper()
	verifier, err := NewOIDCIDTokenVerifier(OIDCProviderConfig{
		Audience: "client-1", TrustedIssuers: []string{"https://issuer.example"},
		JWKSURL: jwksURL, Now: func() time.Time { return now },
	})
	if err != nil {
		t.Fatal(err)
	}
	return verifier
}

func oidcTestKey(t *testing.T) *rsa.PrivateKey {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	return key
}

func oidcTestToken(t *testing.T, key *rsa.PrivateKey, kid string, claims jwt.MapClaims) string {
	t.Helper()
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	token.Header["kid"] = kid
	raw, err := token.SignedString(key)
	if err != nil {
		t.Fatal(err)
	}
	return raw
}

func oidcTestJWKSServer(t *testing.T, kid string, key *rsa.PublicKey, status int) *httptest.Server {
	t.Helper()
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		if status != http.StatusOK {
			writer.WriteHeader(status)
			return
		}
		writer.Header().Set("Cache-Control", "public, max-age=3600")
		writeOIDCTestJWKS(t, writer, kid, key)
	}))
	t.Cleanup(server.Close)
	return server
}

func writeOIDCTestJWKS(t *testing.T, writer http.ResponseWriter, kid string, key *rsa.PublicKey) {
	t.Helper()
	exponent := big.NewInt(int64(key.E)).Bytes()
	payload := map[string]any{"keys": []map[string]string{{
		"kty": "RSA", "kid": kid, "alg": "RS256", "use": "sig",
		"n": base64.RawURLEncoding.EncodeToString(key.N.Bytes()),
		"e": base64.RawURLEncoding.EncodeToString(exponent),
	}}}
	if err := json.NewEncoder(writer).Encode(payload); err != nil {
		t.Errorf("write JWKS: %v", err)
	}
}
