package runtimegrpc

import (
	"context"
	"net"
	"testing"
	"time"

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/authorizationv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/test/bufconn"
)

type authorizationLifecycleTestServer struct {
	runtimev1.UnimplementedAuthorizationLifecycleServiceServer
	t *testing.T
}

type clientCredentialsAuthorizationTestServer struct {
	runtimev1.UnimplementedAuthorizationLifecycleServiceServer
	t            *testing.T
	issueRefresh bool
}

func (server *clientCredentialsAuthorizationTestServer) ExchangeAuthorizationToken(ctx context.Context, request *runtimev1.ExchangeAuthorizationTokenRequest) (*runtimev1.ExchangeAuthorizationTokenResponse, error) {
	credentials, ok := RuntimeAuthorizationExchangeCredentials(ctx)
	if !ok || credentials.ClientSecret != "machine-secret" || credentials.AuthorizationCode != "" || credentials.CodeVerifier != "" || credentials.RefreshToken != "" {
		server.t.Fatalf("client credentials=%#v ok=%t", credentials, ok)
	}
	values, _ := metadata.FromIncomingContext(ctx)
	for _, key := range []string{AuthorizationClientSecretMetadataKey, AuthorizationCodeMetadataKey, AuthorizationCodeVerifierMetadataKey, AuthorizationRefreshTokenMetadataKey} {
		if got := values.Get(key); len(got) != 0 {
			server.t.Fatalf("handler retained sensitive metadata %q: %#v", key, got)
		}
	}
	semanticRequest, err := mapping.FromWireExchangeAuthorizationTokenRequest(ctx, request)
	if err != nil || semanticRequest.GrantType != semantic.GrantTypeClientCredentials {
		server.t.Fatalf("client-credentials request=%#v err=%v", semanticRequest, err)
	}
	issued := IssuedAuthorizationCredentials{AccessToken: "machine-access-token"}
	if server.issueRefresh {
		issued.RefreshToken = "forbidden-refresh-token"
	}
	if err := SetIssuedAuthorizationCredentials(ctx, issued); err != nil {
		server.t.Fatal(err)
	}
	return mapping.ToWireExchangeAuthorizationTokenResponse(semantic.ExchangeResponse{Token: &semantic.Token{
		TokenType: "Bearer", ExpiresInSecond: 3600, AccessTokenIssued: true, RefreshTokenIssued: server.issueRefresh}})
}

func (server *authorizationLifecycleTestServer) BeginAuthorization(ctx context.Context, request *runtimev1.BeginAuthorizationRequest) (*runtimev1.BeginAuthorizationResponse, error) {
	if _, ok := RuntimeAuthorizationExchangeCredentials(ctx); ok {
		server.t.Fatal("begin received exchange credentials")
	}
	values, _ := metadata.FromIncomingContext(ctx)
	if got := values.Get(EndUserAuthorizationMetadataKey); len(got) != 0 {
		server.t.Fatalf("begin inherited end-user credentials: %#v", got)
	}
	semanticRequest, err := mapping.FromWireBeginAuthorizationRequest(ctx, request)
	if err != nil || semanticRequest.ClientID != "public-client" {
		server.t.Fatalf("begin request=%#v err=%v", semanticRequest, err)
	}
	return mapping.ToWireBeginAuthorizationResponse(semantic.BeginResponse{RequestHandle: "handle-1", ExpiresAtUnixSecond: time.Now().Add(time.Minute).Unix()})
}

func (server *authorizationLifecycleTestServer) CompleteAuthorization(ctx context.Context, request *runtimev1.CompleteAuthorizationRequest) (*runtimev1.CompleteAuthorizationResponse, error) {
	presented, ok := RuntimeSessionPresentedCredentials(ctx)
	if !ok || presented.AccessToken != "browser-token" {
		server.t.Fatalf("presented credentials=%#v ok=%t", presented, ok)
	}
	if _, err := mapping.FromWireCompleteAuthorizationRequest(ctx, request); err != nil {
		server.t.Fatal(err)
	}
	if err := SetIssuedAuthorizationCredentials(ctx, IssuedAuthorizationCredentials{AuthorizationCode: "code-1"}); err != nil {
		server.t.Fatal(err)
	}
	return mapping.ToWireCompleteAuthorizationResponse(semantic.CompleteResponse{AuthorizationCodeIssued: true,
		RedirectURI: "https://client.example/callback", State: "state-1"})
}

func (server *authorizationLifecycleTestServer) ExchangeAuthorizationToken(ctx context.Context, request *runtimev1.ExchangeAuthorizationTokenRequest) (*runtimev1.ExchangeAuthorizationTokenResponse, error) {
	credentials, ok := RuntimeAuthorizationExchangeCredentials(ctx)
	if !ok || credentials.ClientSecret != "client-secret" || credentials.AuthorizationCode != "code-1" || credentials.CodeVerifier != "verifier" {
		server.t.Fatalf("exchange credentials=%#v ok=%t", credentials, ok)
	}
	if _, err := mapping.FromWireExchangeAuthorizationTokenRequest(ctx, request); err != nil {
		server.t.Fatal(err)
	}
	if err := SetIssuedAuthorizationCredentials(ctx, IssuedAuthorizationCredentials{AccessToken: "access-token", RefreshToken: "refresh-token"}); err != nil {
		server.t.Fatal(err)
	}
	return mapping.ToWireExchangeAuthorizationTokenResponse(semantic.ExchangeResponse{Token: &semantic.Token{
		TokenType: "Bearer", ExpiresInSecond: 3600, AccessTokenIssued: true, RefreshTokenIssued: true}})
}

func TestAuthorizationLifecycleClientKeepsSecretsOutOfMessages(t *testing.T) {
	policy, err := NewPolicyWithIdentityAndOptions(Config{Authority: func(ctx context.Context, _ string) (VerifiedCaller, error) {
		values, _ := metadata.FromIncomingContext(ctx)
		if got := values.Get("authorization"); len(got) != 1 || got[0] != "Bearer workload-secret" {
			t.Fatalf("workload authorization=%#v", got)
		}
		return "sfcrm", nil
	}, AdmissionLimit: 2, AttemptCap: time.Second}, time.Second)
	if err != nil {
		t.Fatal(err)
	}
	listener := bufconn.Listen(4 * envelope.MaxResponseWireBytes)
	server := grpc.NewServer(policy.ServerOptions()...)
	runtimev1.RegisterAuthorizationLifecycleServiceServer(server, &authorizationLifecycleTestServer{t: t})
	go func() { _ = server.Serve(listener) }()
	t.Cleanup(func() { server.Stop(); _ = listener.Close() })

	dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
	connection, err := grpc.DialContext(dialCtx, "passthrough:///authorization-bufnet",
		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
		grpc.WithTransportCredentials(insecure.NewCredentials()))
	cancelDial()
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = connection.Close() })
	wireClient := runtimev1.NewAuthorizationLifecycleServiceClient(connection)
	client, err := NewAuthorizationLifecycleClient(wireClient.BeginAuthorization, wireClient.CompleteAuthorization, wireClient.ExchangeAuthorizationToken)
	if err != nil {
		t.Fatal(err)
	}
	base := metadata.AppendToOutgoingContext(
		context.Background(),
		"authorization", "Bearer workload-secret",
		EndUserAuthorizationMetadataKey, "Bearer stale-user-secret",
	)
	ctx, cancel := context.WithTimeout(base, time.Second)
	defer cancel()
	begin := semantic.BeginRequest{AppInstallationID: "installation-1", ClientID: "public-client",
		RedirectURI: "https://client.example/callback", ResponseType: "code", Scope: "api",
		CodeChallenge: "challenge", CodeChallengeMethod: "S256", Issuer: "https://crm.example"}
	beginResponse, err := client.Begin(ctx, begin)
	if err != nil || beginResponse.RequestHandle != "handle-1" {
		t.Fatalf("begin=%#v err=%v", beginResponse, err)
	}
	complete, completed, err := client.Complete(ctx, SessionPresentedCredentials{AccessToken: "browser-token"}, semantic.CompleteRequest{
		AppInstallationID: "installation-1", RequestHandle: beginResponse.RequestHandle})
	if err != nil || !complete.AuthorizationCodeIssued || completed.AuthorizationCode != "code-1" {
		t.Fatalf("complete=%#v issued=%#v err=%v", complete, completed, err)
	}
	exchange, issued, err := client.Exchange(ctx, AuthorizationExchangeCredentials{ClientSecret: "client-secret",
		AuthorizationCode: completed.AuthorizationCode, CodeVerifier: "verifier"}, semantic.ExchangeRequest{
		AppInstallationID: "installation-1", GrantType: semantic.GrantTypeAuthorizationCode,
		ClientID: "public-client", RedirectURI: begin.RedirectURI, Issuer: begin.Issuer})
	if err != nil || exchange.Token == nil || issued.AccessToken != "access-token" || issued.RefreshToken != "refresh-token" {
		t.Fatalf("exchange=%#v issued=%#v err=%v", exchange, issued, err)
	}
}

func TestAuthorizationLifecycleClientRejectsWrongCredentialProfile(t *testing.T) {
	called := false
	client, err := NewAuthorizationLifecycleClient(
		func(context.Context, *runtimev1.BeginAuthorizationRequest, ...grpc.CallOption) (*runtimev1.BeginAuthorizationResponse, error) {
			called = true
			return nil, nil
		},
		func(context.Context, *runtimev1.CompleteAuthorizationRequest, ...grpc.CallOption) (*runtimev1.CompleteAuthorizationResponse, error) {
			called = true
			return nil, nil
		},
		func(context.Context, *runtimev1.ExchangeAuthorizationTokenRequest, ...grpc.CallOption) (*runtimev1.ExchangeAuthorizationTokenResponse, error) {
			called = true
			return nil, nil
		})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	_, _, err = client.Exchange(ctx, AuthorizationExchangeCredentials{RefreshToken: "refresh", AuthorizationCode: "code"}, semantic.ExchangeRequest{
		AppInstallationID: "installation-1", GrantType: semantic.GrantTypeRefreshToken, ClientID: "client", Issuer: "https://crm.example"})
	if err == nil || called {
		t.Fatalf("invalid profile err=%v called=%t", err, called)
	}
}

func TestAuthorizationLifecycleClientSupportsClientCredentialsWithoutExposingSecrets(t *testing.T) {
	for _, test := range []struct {
		name         string
		issueRefresh bool
		wantError    bool
	}{
		{name: "access token only"},
		{name: "refresh token rejected", issueRefresh: true, wantError: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			policy, err := NewPolicyWithIdentityAndOptions(Config{Authority: func(context.Context, string) (VerifiedCaller, error) {
				return "sfcrm", nil
			}, AdmissionLimit: 1, AttemptCap: time.Second}, time.Second)
			if err != nil {
				t.Fatal(err)
			}
			listener := bufconn.Listen(4 * envelope.MaxResponseWireBytes)
			server := grpc.NewServer(policy.ServerOptions()...)
			runtimev1.RegisterAuthorizationLifecycleServiceServer(server, &clientCredentialsAuthorizationTestServer{
				t: t, issueRefresh: test.issueRefresh,
			})
			go func() { _ = server.Serve(listener) }()
			t.Cleanup(func() { server.Stop(); _ = listener.Close() })

			dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
			connection, err := grpc.DialContext(dialCtx, "passthrough:///client-credentials-bufnet",
				grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
				grpc.WithTransportCredentials(insecure.NewCredentials()))
			cancelDial()
			if err != nil {
				t.Fatal(err)
			}
			t.Cleanup(func() { _ = connection.Close() })
			wireClient := runtimev1.NewAuthorizationLifecycleServiceClient(connection)
			client, err := NewAuthorizationLifecycleClient(wireClient.BeginAuthorization, wireClient.CompleteAuthorization, wireClient.ExchangeAuthorizationToken)
			if err != nil {
				t.Fatal(err)
			}
			ctx, cancel := context.WithTimeout(metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer workload-secret"), time.Second)
			defer cancel()
			response, issued, err := client.Exchange(ctx, AuthorizationExchangeCredentials{ClientSecret: "machine-secret"}, semantic.ExchangeRequest{
				AppInstallationID: "installation-1", GrantType: semantic.GrantTypeClientCredentials,
				ClientID: "confidential-client", Issuer: "https://crm.example", RequestedScope: "api",
			})
			if test.wantError {
				if err == nil || response.Token != nil || issued != (IssuedAuthorizationCredentials{}) {
					t.Fatalf("refresh response=%#v issued=%#v err=%v", response, issued, err)
				}
				return
			}
			if err != nil || response.Token == nil || issued.AccessToken != "machine-access-token" || issued.RefreshToken != "" {
				t.Fatalf("client credentials response=%#v issued=%#v err=%v", response, issued, err)
			}
		})
	}
}

func TestAuthorizationLifecycleClientRejectsInvalidClientCredentialsProfiles(t *testing.T) {
	for name, credentials := range map[string]AuthorizationExchangeCredentials{
		"missing secret":     {},
		"authorization code": {ClientSecret: "machine-secret", AuthorizationCode: "code"},
		"code verifier":      {ClientSecret: "machine-secret", CodeVerifier: "verifier"},
		"refresh token":      {ClientSecret: "machine-secret", RefreshToken: "refresh"},
	} {
		t.Run(name, func(t *testing.T) {
			called := false
			client, err := NewAuthorizationLifecycleClient(
				func(context.Context, *runtimev1.BeginAuthorizationRequest, ...grpc.CallOption) (*runtimev1.BeginAuthorizationResponse, error) {
					return nil, nil
				},
				func(context.Context, *runtimev1.CompleteAuthorizationRequest, ...grpc.CallOption) (*runtimev1.CompleteAuthorizationResponse, error) {
					return nil, nil
				},
				func(context.Context, *runtimev1.ExchangeAuthorizationTokenRequest, ...grpc.CallOption) (*runtimev1.ExchangeAuthorizationTokenResponse, error) {
					called = true
					return nil, nil
				})
			if err != nil {
				t.Fatal(err)
			}
			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()
			_, _, err = client.Exchange(ctx, credentials, semantic.ExchangeRequest{
				AppInstallationID: "installation-1", GrantType: semantic.GrantTypeClientCredentials,
				ClientID: "confidential-client", Issuer: "https://crm.example",
			})
			if err == nil || called {
				t.Fatalf("invalid client credentials profile err=%v called=%t", err, called)
			}
		})
	}
}
