package runtimeapi

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	identitysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/identityv1"
	sessionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/sessionv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
)

type sessionProcessEngine struct {
	t            *testing.T
	mu           sync.Mutex
	calls        []string
	blockStarted chan struct{}
	blockDone    chan error
}

func (engine *sessionProcessEngine) CreateSession(
	ctx context.Context,
	authority RuntimeWorkloadCall,
	credentials runtimegrpc.SessionCreateCredentials,
	request sessionsemantic.CreateRequest,
) (sessionsemantic.CreateResponse, runtimegrpc.SessionIssuedCredentials, error) {
	engine.assertBoundary(ctx, authority, request.AppInstallationID)
	if credentials.Identity != "user@example.test" ||
		(credentials.Secret != "password" && credentials.Secret != "block-password") ||
		credentials.ClientSecret != " client-secret " {
		engine.t.Errorf("create credentials = %#v", credentials)
	}
	if credentials.Secret == "block-password" {
		close(engine.blockStarted)
		<-ctx.Done()
		engine.blockDone <- ctx.Err()
		return sessionsemantic.CreateResponse{}, runtimegrpc.SessionIssuedCredentials{}, ctx.Err()
	}
	engine.record("create")
	return sessionsemantic.CreateResponse{
		Session: sessionProcessView(), AccessTokenIssued: true, ControlProofIssued: true,
	}, runtimegrpc.SessionIssuedCredentials{AccessToken: "access-token", ControlProof: "control-proof"}, nil
}

func (engine *sessionProcessEngine) ReadSession(
	ctx context.Context,
	authority RuntimeWorkloadCall,
	credentials runtimegrpc.SessionPresentedCredentials,
	request sessionsemantic.ReadRequest,
) (sessionsemantic.ReadResponse, runtimegrpc.SessionIssuedCredentials, error) {
	engine.assertBoundary(ctx, authority, request.AppInstallationID)
	if credentials.AccessToken != "access-token" || credentials.ControlProof != "control-proof" {
		engine.t.Errorf("read credentials = %#v", credentials)
	}
	engine.record("read")
	return sessionsemantic.ReadResponse{
		Session: sessionProcessView(), ReplacementAccessTokenIssued: true,
		ControlProofExpiresAtUnixSecond: time.Now().Add(time.Hour).Unix(),
	}, runtimegrpc.SessionIssuedCredentials{AccessToken: "replacement-token"}, nil
}

func (engine *sessionProcessEngine) RevokeSession(
	ctx context.Context,
	authority RuntimeWorkloadCall,
	credentials runtimegrpc.SessionPresentedCredentials,
	request sessionsemantic.RevokeRequest,
) error {
	engine.assertBoundary(ctx, authority, request.AppInstallationID)
	if credentials.AccessToken != "replacement-token" || credentials.ControlProof != "" {
		engine.t.Errorf("revoke credentials = %#v", credentials)
	}
	engine.record("revoke")
	return nil
}

func (engine *sessionProcessEngine) assertBoundary(ctx context.Context, authority RuntimeWorkloadCall, appInstallationID string) {
	engine.t.Helper()
	if authority.OrgID != testOrgID || authority.CallerWorkloadID != testWorkloadID ||
		authority.Capability != sessionsemantic.CapabilityName || authority.SecurityEpoch != 1 ||
		appInstallationID != testInstallationID {
		engine.t.Errorf("authority=%#v installation=%q", authority, appInstallationID)
	}
	if _, ok := runtimegrpc.RuntimeSessionCreateCredentials(ctx); ok {
		engine.t.Error("create credentials remained in engine context")
	}
	if _, ok := runtimegrpc.RuntimeSessionPresentedCredentials(ctx); ok {
		engine.t.Error("presented credentials remained in engine context")
	}
	if _, ok := runtimegrpc.EndUserBearer(ctx); ok {
		engine.t.Error("end-user bearer remained in engine context")
	}
	values, _ := metadata.FromIncomingContext(ctx)
	for _, key := range []string{
		runtimegrpc.SessionIdentityMetadataKey,
		runtimegrpc.SessionSecretMetadataKey,
		runtimegrpc.SessionClientSecretMetadataKey,
		runtimegrpc.SessionControlProofMetadataKey,
		envelope.RuntimeDelegationMetadataKey,
	} {
		if len(values.Get(key)) != 0 {
			engine.t.Errorf("engine metadata retained %q", key)
		}
	}
}

func (engine *sessionProcessEngine) record(call string) {
	engine.mu.Lock()
	engine.calls = append(engine.calls, call)
	engine.mu.Unlock()
}

func sessionProcessView() sessionsemantic.Session {
	return sessionsemantic.Session{Principal: sessionsemantic.Principal{
		PrincipalID: "user-1", DisplayName: "Test User", Email: "user@example.test", ProfileID: "profile-1",
	}, ExpiresAtUnixSecond: time.Now().Add(time.Hour).Unix()}
}

func startSessionProcess(t *testing.T, engine SessionLifecycleEngine) (*runtimegrpc.SessionLifecycleClient, func()) {
	t.Helper()
	process, err := Assemble(ProcessConfig{
		Version: "session-process-test", AdminAddress: "127.0.0.1:0", GRPCAddress: "127.0.0.1:0",
		AdmissionLimit: 2, ServingEnabled: true,
		WorkloadAuthority: func(ctx context.Context, _ string) (runtimegrpc.VerifiedCaller, error) {
			values, _ := metadata.FromIncomingContext(ctx)
			if got := values.Get("authorization"); len(got) != 1 || got[0] != "Bearer workload-secret" {
				t.Errorf("workload authorization = %#v", got)
			}
			for _, key := range []string{
				runtimegrpc.SessionIdentityMetadataKey,
				runtimegrpc.SessionSecretMetadataKey,
				runtimegrpc.SessionClientSecretMetadataKey,
				runtimegrpc.SessionControlProofMetadataKey,
			} {
				if len(values.Get(key)) != 0 {
					t.Errorf("workload authority received %q", key)
				}
			}
			return runtimegrpc.VerifiedCaller(testWorkloadID), nil
		},
		SecurityAdmission: testSecurityAdmission(t),
		IdentityExchange: func(context.Context, identitysemantic.Request) (identitysemantic.Response, error) {
			return identitysemantic.Response{}, errors.New("identity exchange is not part of this session test")
		},
		SessionLifecycle: engine, IdentityAttemptCap: time.Second,
		Receipts: ReceiptSourceFunc(BootstrapReceipts),
	}, &readerStub{})
	if err != nil {
		t.Fatal(err)
	}
	if err := process.Start(); err != nil {
		t.Fatal(err)
	}
	connection, err := grpc.NewClient("dns:///"+process.GRPCAddress(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultCallOptions(
			grpc.MaxCallSendMsgSize(envelope.MaxRequestWireBytes),
			grpc.MaxCallRecvMsgSize(envelope.MaxResponseWireBytes),
			grpc.WaitForReady(false),
		),
	)
	if err != nil {
		stopProcess(t, process)
		t.Fatal(err)
	}
	wireClient := runtimev1.NewSessionLifecycleServiceClient(connection)
	client, err := runtimegrpc.NewSessionLifecycleClient(
		wireClient.CreateSession, wireClient.ReadSession, wireClient.RevokeSession,
	)
	if err != nil {
		_ = connection.Close()
		stopProcess(t, process)
		t.Fatal(err)
	}
	return client, func() {
		_ = connection.Close()
		stopProcess(t, process)
	}
}

func sessionProcessContext(parent context.Context) context.Context {
	return metadata.AppendToOutgoingContext(parent, "authorization", "Bearer workload-secret")
}

func TestRuntimeAPIProcessServesSessionLifecycleOnExistingEndpoint(t *testing.T) {
	engine := &sessionProcessEngine{t: t}
	client, cleanup := startSessionProcess(t, engine)
	defer cleanup()
	ctx, cancel := context.WithTimeout(sessionProcessContext(context.Background()), time.Second)
	defer cancel()
	created, issued, err := client.CreateSession(ctx, runtimegrpc.SessionCreateCredentials{
		Identity: "user@example.test", Secret: "password", ClientSecret: " client-secret ",
	}, sessionsemantic.CreateRequest{
		AppInstallationID: testInstallationID, Method: sessionsemantic.AuthenticationMethodPassword,
		ApplicationClientID: "client-1", IssueControlProof: true,
	})
	if err != nil || !created.AccessTokenIssued || !created.ControlProofIssued ||
		issued.AccessToken != "access-token" || issued.ControlProof != "control-proof" {
		t.Fatalf("create=%#v issued=%#v err=%v", created, issued, err)
	}
	read, replacement, err := client.ReadSession(ctx, runtimegrpc.SessionPresentedCredentials{
		AccessToken: issued.AccessToken, ControlProof: issued.ControlProof,
	}, sessionsemantic.ReadRequest{
		AppInstallationID: testInstallationID, AllowRenewal: true, UseControlProof: true,
	})
	if err != nil || !read.ReplacementAccessTokenIssued || replacement.AccessToken != "replacement-token" {
		t.Fatalf("read=%#v replacement=%#v err=%v", read, replacement, err)
	}
	if err := client.RevokeSession(ctx, runtimegrpc.SessionPresentedCredentials{
		AccessToken: replacement.AccessToken,
	}, sessionsemantic.RevokeRequest{AppInstallationID: testInstallationID}); err != nil {
		t.Fatal(err)
	}
	engine.mu.Lock()
	calls := strings.Join(engine.calls, ",")
	engine.mu.Unlock()
	if calls != "create,read,revoke" {
		t.Fatalf("engine calls = %q", calls)
	}
}

func TestRuntimeAPIProcessRejectsDelegationAndPropagatesSessionCancellation(t *testing.T) {
	engine := &sessionProcessEngine{
		t: t, blockStarted: make(chan struct{}), blockDone: make(chan error, 1),
	}
	client, cleanup := startSessionProcess(t, engine)
	defer cleanup()

	delegatedCtx, delegatedCancel := context.WithTimeout(metadata.AppendToOutgoingContext(
		sessionProcessContext(context.Background()), envelope.RuntimeDelegationMetadataKey, "must-not-be-accepted",
	), time.Second)
	defer delegatedCancel()
	_, _, err := client.CreateSession(delegatedCtx, runtimegrpc.SessionCreateCredentials{
		Identity: "user@example.test", Secret: "password", ClientSecret: " client-secret ",
	}, sessionsemantic.CreateRequest{
		AppInstallationID: testInstallationID, Method: sessionsemantic.AuthenticationMethodPassword,
		ApplicationClientID: "client-1", IssueControlProof: true,
	})
	var failure *sessionsemantic.Failure
	if !errors.As(err, &failure) || failure.Category != sessionsemantic.FailureUnauthenticated {
		t.Fatalf("delegated session call error = %#v", err)
	}

	base, timeoutCancel := context.WithTimeout(sessionProcessContext(context.Background()), 2*time.Second)
	defer timeoutCancel()
	parent, cancel := context.WithCancel(base)
	done := make(chan error, 1)
	go func() {
		_, _, callErr := client.CreateSession(parent, runtimegrpc.SessionCreateCredentials{
			Identity: "user@example.test", Secret: "block-password", ClientSecret: " client-secret ",
		}, sessionsemantic.CreateRequest{
			AppInstallationID: testInstallationID, Method: sessionsemantic.AuthenticationMethodPassword,
			ApplicationClientID: "client-1", IssueControlProof: true,
		})
		done <- callErr
	}()
	select {
	case <-engine.blockStarted:
	case <-time.After(time.Second):
		t.Fatal("session engine did not start")
	}
	cancel()
	select {
	case ownerErr := <-engine.blockDone:
		if !errors.Is(ownerErr, context.Canceled) {
			t.Fatalf("engine cancellation = %v", ownerErr)
		}
	case <-time.After(time.Second):
		t.Fatal("session engine did not observe cancellation")
	}
	select {
	case callErr := <-done:
		failure = nil
		if !errors.As(callErr, &failure) || failure.Category != sessionsemantic.FailureCanceled {
			t.Fatalf("client cancellation = %#v", callErr)
		}
	case <-time.After(time.Second):
		t.Fatal("session client did not return after cancellation")
	}
}
