package runtimeidentity

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

	featureauth "github.com/nexuscrm/auth"
	"github.com/nexuscrm/backend/internal/runtimeapi"
	kernelerrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/authorizationv1"
)

type authorizationOAuthStub struct {
	app                      *models.SystemConnectedApp
	appErr                   error
	createResult             *featureauth.AuthorizeRequestCreateResult
	createErr                error
	createRequest            featureauth.AuthorizeRequestCreateRequest
	createCalls              int
	resumeResult             *featureauth.AuthorizeRequestResumeResult
	resumeErr                error
	resumeRequest            featureauth.AuthorizeRequestResumeRequest
	exchangeToken            *models.SessionToken
	exchangeErr              error
	exchangeRequest          featureauth.TokenExchangeRequest
	refreshRequest           featureauth.RefreshTokenExchangeRequest
	refreshToken             *models.SessionToken
	refreshErr               error
	federatedBeginResult     *featureauth.SSOLoginTransaction
	federatedBeginErr        error
	federatedBeginRedirect   string
	federatedConsumeErr      error
	federatedConsumeState    string
	federatedConsumeRedirect string
	clientRequest            featureauth.ClientCredentialsExchangeRequest
	clientResult             *featureauth.ClientCredentialsExchangeResult
	clientErr                error
}

func (s *authorizationOAuthStub) GetConnectedApp(context.Context, string) (*models.SystemConnectedApp, error) {
	return s.app, s.appErr
}
func (s *authorizationOAuthStub) CreateAuthorizeRequestHandle(_ context.Context, request featureauth.AuthorizeRequestCreateRequest) (*featureauth.AuthorizeRequestCreateResult, error) {
	s.createCalls++
	s.createRequest = request
	return s.createResult, s.createErr
}
func (s *authorizationOAuthStub) ResumeAuthorizeRequest(_ context.Context, request featureauth.AuthorizeRequestResumeRequest) (*featureauth.AuthorizeRequestResumeResult, error) {
	s.resumeRequest = request
	return s.resumeResult, s.resumeErr
}
func (s *authorizationOAuthStub) ExchangeTokenForRequest(_ context.Context, request featureauth.TokenExchangeRequest) (*models.SessionToken, error) {
	s.exchangeRequest = request
	return s.exchangeToken, s.exchangeErr
}
func (s *authorizationOAuthStub) ExchangeRefreshToken(_ context.Context, request featureauth.RefreshTokenExchangeRequest) (*models.SessionToken, error) {
	s.refreshRequest = request
	return s.refreshToken, s.refreshErr
}
func (s *authorizationOAuthStub) ExchangeClientCredentials(_ context.Context, request featureauth.ClientCredentialsExchangeRequest) (*featureauth.ClientCredentialsExchangeResult, error) {
	s.clientRequest = request
	return s.clientResult, s.clientErr
}

func authorizationTestAuthority() runtimeapi.RuntimeWorkloadCall {
	return runtimeapi.RuntimeWorkloadCall{OrgID: "org-1", CallerWorkloadID: "sfcrm",
		Capability: semantic.CapabilityName, SecurityEpoch: 7}
}

func authorizationTestSecurity(now time.Time) IssuanceSecuritySource {
	return func(context.Context, string, string) (IssuanceSecurityState, error) {
		return IssuanceSecurityState{StateFound: true, BindingFound: true, BindingActive: true, OrgID: "org-1",
			Capabilities: []string{semantic.CapabilityName}, SecurityEpoch: 7, LeaseFound: true,
			LeaseSecurityEpoch: 7, LeaseExpiresAt: now.Add(time.Minute)}, nil
	}
}

func newAuthorizationLifecycleTest(t *testing.T, auth *sessionAuthStub, oauth *authorizationOAuthStub, now time.Time) *AuthorizationLifecycle {
	t.Helper()
	owner, err := NewAuthorizationLifecycle(AuthorizationLifecycleConfig{Auth: auth, OAuth: oauth,
		Security: authorizationTestSecurity(now), AppInstallationID: "installation-1", AuthorizedWorkloadID: "sfcrm",
		Now: func() time.Time { return now }})
	if err != nil {
		t.Fatal(err)
	}
	return owner
}

func authorizationBeginRequest() semantic.BeginRequest {
	return semantic.BeginRequest{AppInstallationID: "installation-1", ClientID: "public-client",
		RedirectURI: "https://client.example/callback", ResponseType: "code", Scope: "api", State: "state-1",
		CodeChallenge: "challenge", CodeChallengeMethod: "S256", Issuer: "https://crm.example"}
}

func TestAuthorizationLifecycleReusesDurableOwnerAcrossPublicJourney(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	redirects := `["https://client.example/callback"]`
	oauth := &authorizationOAuthStub{app: &models.SystemConnectedApp{ConsumerKey: "public-client", RedirectURIs: &redirects},
		createResult: &featureauth.AuthorizeRequestCreateResult{Handle: "handle-1", ExpiresAt: now.Add(time.Minute)},
		resumeResult: &featureauth.AuthorizeRequestResumeResult{Code: "code-1", RedirectURI: "https://client.example/callback", State: "state-1"},
		exchangeToken: &models.SessionToken{AccessToken: "access-token", RefreshToken: "refresh-token", TokenType: "Bearer",
			ExpiresIn: 3600, Scope: "api"}}
	validation := sessionTestValidation(now)
	validation.Claims.User.SessionClass = featureauth.OAuthSessionClassPlatformBrowser
	auth := &sessionAuthStub{validation: validation}
	owner := newAuthorizationLifecycleTest(t, auth, oauth, now)
	ctx := sessionTestContext(t)

	begin, err := owner.BeginAuthorization(ctx, authorizationTestAuthority(), authorizationBeginRequest())
	if err != nil || begin.RequestHandle != "handle-1" || oauth.createRequest.Issuer != "https://crm.example" {
		t.Fatalf("begin=%#v request=%#v err=%v", begin, oauth.createRequest, err)
	}
	complete, completed, err := owner.CompleteAuthorization(ctx, authorizationTestAuthority(), runtimegrpc.SessionPresentedCredentials{AccessToken: "browser-token"},
		semantic.CompleteRequest{AppInstallationID: "installation-1", RequestHandle: begin.RequestHandle})
	if err != nil || !complete.AuthorizationCodeIssued || completed.AuthorizationCode != "code-1" || oauth.resumeRequest.UserID != "user-1" {
		t.Fatalf("complete=%#v issued=%#v request=%#v err=%v", complete, completed, oauth.resumeRequest, err)
	}
	exchange, issued, err := owner.ExchangeAuthorizationToken(ctx, authorizationTestAuthority(),
		runtimegrpc.AuthorizationExchangeCredentials{AuthorizationCode: completed.AuthorizationCode, CodeVerifier: "verifier"},
		semantic.ExchangeRequest{AppInstallationID: "installation-1", GrantType: semantic.GrantTypeAuthorizationCode,
			ClientID: "public-client", RedirectURI: complete.RedirectURI, Issuer: "https://crm.example"})
	if err != nil || exchange.Token == nil || issued.AccessToken != "access-token" || issued.RefreshToken != "refresh-token" ||
		oauth.exchangeRequest.CodeVerifier != "verifier" {
		t.Fatalf("exchange=%#v issued=%#v request=%#v err=%v", exchange, issued, oauth.exchangeRequest, err)
	}
}

func TestAuthorizationLifecycleReturnsOnlyVerifiedRedirects(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	redirects := `["https://client.example/callback"]`
	for _, test := range []struct {
		name         string
		app          *models.SystemConnectedApp
		error        error
		wantCode     string
		wantRedirect string
		unregistered bool
		missingType  bool
	}{
		{name: "unknown client", error: kernelerrors.NewNotFoundError("connected app", "public-client"), wantCode: "invalid_client"},
		{name: "invalid scope", app: &models.SystemConnectedApp{RedirectURIs: &redirects},
			error:    &featureauth.OAuthProtocolError{Status: 400, Code: "invalid_scope", SafeDescription: "scope is not allowed"},
			wantCode: "invalid_scope", wantRedirect: "https://client.example/callback"},
		{name: "missing response type after verified client", app: &models.SystemConnectedApp{RedirectURIs: &redirects},
			error:    &featureauth.OAuthProtocolError{Status: 400, Code: "invalid_request", SafeDescription: "response type is required"},
			wantCode: "invalid_request", wantRedirect: "https://client.example/callback", missingType: true},
		{name: "unregistered redirect", app: &models.SystemConnectedApp{RedirectURIs: &redirects},
			error:    &featureauth.OAuthProtocolError{Status: 400, Code: "invalid_request", SafeDescription: "redirect is not registered"},
			wantCode: "invalid_request", unregistered: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			oauth := &authorizationOAuthStub{app: test.app, createErr: test.error}
			owner := newAuthorizationLifecycleTest(t, &sessionAuthStub{}, oauth, now)
			request := authorizationBeginRequest()
			if test.missingType {
				request.ResponseType = ""
			}
			if test.unregistered {
				request.RedirectURI = "https://attacker.example/callback"
			}
			response, err := owner.BeginAuthorization(sessionTestContext(t), authorizationTestAuthority(), request)
			if err != nil || response.ProtocolError == nil || response.ProtocolError.Code != test.wantCode ||
				response.ProtocolError.TrustedRedirectURI != test.wantRedirect {
				t.Fatalf("response=%#v err=%v", response, err)
			}
		})
	}
}

func TestAuthorizationLifecycleStaleSecurityStopsBeforeOAuth(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	oauth := &authorizationOAuthStub{}
	owner := newAuthorizationLifecycleTest(t, &sessionAuthStub{}, oauth, now)
	authority := authorizationTestAuthority()
	authority.SecurityEpoch = 6
	_, err := owner.BeginAuthorization(sessionTestContext(t), authority, authorizationBeginRequest())
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureStaleState || oauth.createCalls != 0 {
		t.Fatalf("failure=%#v calls=%d", failure, oauth.createCalls)
	}
}

func TestAuthorizationLifecycleDelegatesClientCredentialsToDurableOAuthOwner(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	oauth := &authorizationOAuthStub{clientResult: &featureauth.ClientCredentialsExchangeResult{
		Token:  &models.SessionToken{AccessToken: "machine-access-token", TokenType: "Bearer", ExpiresIn: 3600, Scope: "api.read"},
		UserID: "run-as-user", ConnectedAppID: "app-1",
	}}
	owner := newAuthorizationLifecycleTest(t, &sessionAuthStub{}, oauth, now)
	request := semantic.ExchangeRequest{AppInstallationID: "installation-1", GrantType: semantic.GrantTypeClientCredentials,
		ClientID: "confidential-client", RequestedScope: "api.read", Issuer: "https://crm.example"}
	response, issued, err := owner.ExchangeAuthorizationToken(sessionTestContext(t), authorizationTestAuthority(),
		runtimegrpc.AuthorizationExchangeCredentials{ClientSecret: "machine-secret"}, request)
	if err != nil || response.Token == nil || response.Token.RefreshTokenIssued || issued.AccessToken != "machine-access-token" || issued.RefreshToken != "" {
		t.Fatalf("response=%#v issued=%#v err=%v", response, issued, err)
	}
	if oauth.clientRequest.ClientID != request.ClientID || oauth.clientRequest.ClientSecret != "machine-secret" ||
		oauth.clientRequest.RequestedScope != "api.read" || oauth.clientRequest.Issuer != request.Issuer ||
		oauth.clientRequest.UserAgent != "RuntimeAuthorizationLifecycle" {
		t.Fatalf("client request=%#v", oauth.clientRequest)
	}
}

func TestAuthorizationLifecycleRejectsMixedClientCredentialsBeforeOwner(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	oauth := &authorizationOAuthStub{}
	owner := newAuthorizationLifecycleTest(t, &sessionAuthStub{}, oauth, now)
	response, issued, err := owner.ExchangeAuthorizationToken(sessionTestContext(t), authorizationTestAuthority(),
		runtimegrpc.AuthorizationExchangeCredentials{ClientSecret: "machine-secret", RefreshToken: "foreign-refresh"},
		semantic.ExchangeRequest{AppInstallationID: "installation-1", GrantType: semantic.GrantTypeClientCredentials,
			ClientID: "confidential-client", Issuer: "https://crm.example"})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureUnauthenticated || response != (semantic.ExchangeResponse{}) ||
		issued != (runtimegrpc.IssuedAuthorizationCredentials{}) || oauth.clientRequest != (featureauth.ClientCredentialsExchangeRequest{}) {
		t.Fatalf("response=%#v issued=%#v failure=%#v clientRequest=%#v", response, issued, failure, oauth.clientRequest)
	}
}
