package runtimegrpc

import (
	"context"
	"errors"
	"testing"
	"time"

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/federatedloginv1"
	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/metadata"
)

const federatedLoginTestState = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

func TestFederatedLoginTransactionClientRoundTrip(t *testing.T) {
	var beginCalls, consumeCalls int
	client, err := NewFederatedLoginTransactionClient(
		func(ctx context.Context, request *runtimev1.BeginFederatedLoginRequest, _ ...grpc.CallOption) (*runtimev1.BeginFederatedLoginResponse, error) {
			beginCalls++
			assertFederatedLoginClientProtocol(t, ctx)
			semanticRequest, err := mapping.FromWireBeginFederatedLoginRequest(ctx, request)
			if err != nil || semanticRequest.RedirectURI != "https://crm.example.test/oauth/callback" {
				t.Fatalf("begin request=%#v err=%v", semanticRequest, err)
			}
			return mapping.ToWireBeginFederatedLoginResponse(semantic.BeginResponse{
				State: federatedLoginTestState, ExpiresAtUnixSecond: time.Now().Add(time.Minute).Unix(),
			})
		},
		func(ctx context.Context, request *runtimev1.ConsumeFederatedLoginRequest, _ ...grpc.CallOption) (*runtimev1.ConsumeFederatedLoginResponse, error) {
			consumeCalls++
			assertFederatedLoginClientProtocol(t, ctx)
			semanticRequest, err := mapping.FromWireConsumeFederatedLoginRequest(ctx, request)
			if err != nil || semanticRequest.State != federatedLoginTestState {
				t.Fatalf("consume request=%#v err=%v", semanticRequest, err)
			}
			return mapping.ToWireConsumeFederatedLoginResponse(semantic.ConsumeResponse{})
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	base := metadata.AppendToOutgoingContext(context.Background(), EndUserAuthorizationMetadataKey, "Bearer must-not-cross")
	ctx, cancel := context.WithTimeout(base, time.Second)
	defer cancel()
	begin, err := client.Begin(ctx, semantic.BeginRequest{
		AppInstallationID: "installation-1", RedirectURI: "https://crm.example.test/oauth/callback",
	})
	if err != nil || begin.State != federatedLoginTestState {
		t.Fatalf("begin=%#v err=%v", begin, err)
	}
	if err := client.Consume(ctx, semantic.ConsumeRequest{
		AppInstallationID: "installation-1", State: begin.State,
		RedirectURI: "https://crm.example.test/oauth/callback",
	}); err != nil {
		t.Fatal(err)
	}
	if beginCalls != 1 || consumeCalls != 1 {
		t.Fatalf("begin calls=%d consume calls=%d", beginCalls, consumeCalls)
	}
}

func TestFederatedLoginTransactionClientMakesStateRejectionsIndistinguishable(t *testing.T) {
	client, err := NewFederatedLoginTransactionClient(
		func(context.Context, *runtimev1.BeginFederatedLoginRequest, ...grpc.CallOption) (*runtimev1.BeginFederatedLoginResponse, error) {
			return nil, errors.New("not used")
		},
		func(context.Context, *runtimev1.ConsumeFederatedLoginRequest, ...grpc.CallOption) (*runtimev1.ConsumeFederatedLoginResponse, error) {
			return nil, mapping.ToFederatedLoginConsumeStatusError(&semantic.Failure{
				Category: semantic.FailureMissing, CanonicalField: "state-must-not-cross",
			})
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	err = client.Consume(ctx, semantic.ConsumeRequest{
		AppInstallationID: "installation-1", State: federatedLoginTestState,
		RedirectURI: "https://crm.example.test/oauth/callback",
	})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureUnauthenticated {
		t.Fatalf("err=%#v", err)
	}
	if failure.CanonicalField != "" || failure.InvalidComponent != "" || failure.RequestID != "" {
		t.Fatalf("state rejection disclosed details: %#v", failure)
	}
}

func assertFederatedLoginClientProtocol(t *testing.T, ctx context.Context) {
	t.Helper()
	values, _ := metadata.FromOutgoingContext(ctx)
	if got := values.Get(EndUserAuthorizationMetadataKey); len(got) != 0 {
		t.Fatalf("federated login inherited end-user credentials: %#v", got)
	}
}
