package auth

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
)

func clientCredentialsConnectedApp() *models.SystemConnectedApp {
	clientType := OAuthClientTypeConfidential
	tokenAuth := OAuthTokenAuthClientSecretPost
	grants := OAuthGrantClientCredentials
	scopes := "api api.read"
	runAs := "user-1"
	permittedUsers := "AdminApproved"
	sessionClass := "salesforce_compat"
	return &models.SystemConnectedApp{
		ID: "app-1", Name: "Machine Client", ConsumerKey: "client-1", ConsumerSecret: "secret-1",
		Scopes: &scopes, IsActive: true, ClientType: &clientType, TokenEndpointAuthMethod: &tokenAuth,
		AllowedGrantTypes: &grants, RunAsUserID: &runAs, PermittedUsers: &permittedUsers,
		SessionIssuerClass: &sessionClass,
	}
}

func newClientCredentialsOAuthService(app *models.SystemConnectedApp, permitted bool, user *models.User, sessions *sessionRepoStub) *OAuthService {
	authService := NewAuthService(&userRepoStub{user: user}, sessions, nil)
	return NewOAuthService(nil, &systemRepoStub{app: app, permitted: permitted}, sessions, authService, &oauthRepoStub{})
}

func TestExchangeClientCredentialsIssuesRunAsSessionUnderConnectedAppPolicy(t *testing.T) {
	sessions := &sessionRepoStub{}
	service := newClientCredentialsOAuthService(clientCredentialsConnectedApp(), true, &models.User{
		ID: "user-1", Username: "machine@example.test", Email: "machine@example.test", ProfileID: "profile-1", IsActive: true,
	}, sessions)

	result, err := service.ExchangeClientCredentials(context.Background(), ClientCredentialsExchangeRequest{
		ClientID: "client-1", ClientSecret: "secret-1", RequestedScope: "api.read",
		SourceAddress: "127.0.0.1", UserAgent: "sfcrm-appservice",
	})
	if err != nil {
		t.Fatalf("ExchangeClientCredentials: %v", err)
	}
	if result == nil || result.Token == nil || result.Token.AccessToken == "" || result.Token.RefreshToken != "" || result.Token.Scope != "api.read" ||
		result.UserID != "user-1" || result.ConnectedAppID != "app-1" || result.SessionIssuerClass != "salesforce_compat" {
		t.Fatalf("result = %#v", result)
	}
	if sessions.inserted == nil || sessions.inserted.UserID != "user-1" || sessions.inserted.ConnectedAppID == nil ||
		*sessions.inserted.ConnectedAppID != "app-1" || sessions.inserted.Scope == nil || *sessions.inserted.Scope != "api.read" ||
		sessions.inserted.GrantType == nil || *sessions.inserted.GrantType != OAuthGrantClientCredentials ||
		sessions.inserted.ControlProofHash != nil {
		t.Fatalf("inserted session = %#v", sessions.inserted)
	}
}

func TestExchangeClientCredentialsFailsClosedBeforeSessionIssuance(t *testing.T) {
	for _, test := range []struct {
		name            string
		mutateApp       func(*models.SystemConnectedApp)
		secret          string
		scope           string
		resource        string
		resourcePresent bool
		permitted       bool
		user            *models.User
		wantCode        string
	}{
		{name: "public app", mutateApp: func(app *models.SystemConnectedApp) { value := OAuthClientTypePublic; app.ClientType = &value }, secret: "secret-1", permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_client"},
		{name: "wrong secret", secret: "wrong-secret", permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_client"},
		{name: "grant not allowed", mutateApp: func(app *models.SystemConnectedApp) {
			value := OAuthGrantAuthorizationCode
			app.AllowedGrantTypes = &value
		}, secret: "secret-1", permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_grant"},
		{name: "inactive app", mutateApp: func(app *models.SystemConnectedApp) { app.IsActive = false }, secret: "secret-1", permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_client"},
		{name: "run as missing", mutateApp: func(app *models.SystemConnectedApp) { app.RunAsUserID = nil }, secret: "secret-1", permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_client"},
		{name: "scope rejected", secret: "secret-1", scope: "unknown.scope", permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_scope"},
		{name: "resource rejected", secret: "secret-1", resource: "https://crm.example/mcp", resourcePresent: true, permitted: true, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "invalid_target"},
		{name: "access policy rejected", secret: "secret-1", permitted: false, user: &models.User{ID: "user-1", IsActive: true}, wantCode: "unauthorized_client"},
		{name: "inactive run as user", secret: "secret-1", permitted: true, user: &models.User{ID: "user-1", IsActive: false}},
		{name: "deleted run as user", secret: "secret-1", permitted: true, user: &models.User{ID: "user-1", IsActive: true, IsDeleted: true}},
	} {
		t.Run(test.name, func(t *testing.T) {
			app := clientCredentialsConnectedApp()
			if test.mutateApp != nil {
				test.mutateApp(app)
			}
			sessions := &sessionRepoStub{}
			service := newClientCredentialsOAuthService(app, test.permitted, test.user, sessions)
			result, err := service.ExchangeClientCredentials(context.Background(), ClientCredentialsExchangeRequest{
				ClientID: "client-1", ClientSecret: test.secret, RequestedScope: test.scope,
				Resource: test.resource, ResourcePresent: test.resourcePresent,
			})
			if err == nil || result != nil || sessions.inserted != nil {
				t.Fatalf("result=%#v err=%v inserted=%#v", result, err, sessions.inserted)
			}
			if test.wantCode != "" {
				oauthError, ok := OAuthProtocolErrorFrom(err)
				if !ok || oauthError.Code != test.wantCode {
					t.Fatalf("error=%#v, want OAuth code %q", err, test.wantCode)
				}
			}
		})
	}
}

func TestExchangeClientCredentialsReturnsNoBearerWhenSessionPersistenceFails(t *testing.T) {
	sessions := &sessionRepoStub{insertErr: errors.New("session write rejected")}
	service := newClientCredentialsOAuthService(clientCredentialsConnectedApp(), true, &models.User{
		ID: "user-1", IsActive: true,
	}, sessions)

	result, err := service.ExchangeClientCredentials(context.Background(), ClientCredentialsExchangeRequest{
		ClientID: "client-1", ClientSecret: "secret-1",
	})
	if err == nil || result != nil || sessions.inserted != nil {
		t.Fatalf("result=%#v err=%v inserted=%#v", result, err, sessions.inserted)
	}
}

func TestExchangeClientCredentialsRevalidatesConnectedAppInsideIssuanceLock(t *testing.T) {
	app := clientCredentialsConnectedApp()
	repository := &systemRepoStub{app: app, permitted: true}
	repository.beforeCredentialIssuance = func() { repository.app = nil }
	sessions := &sessionRepoStub{}
	service := NewOAuthService(nil, repository, sessions, NewAuthService(&userRepoStub{user: &models.User{
		ID: "user-1", IsActive: true,
	}}, sessions, nil), &oauthRepoStub{})

	result, err := service.ExchangeClientCredentials(context.Background(), ClientCredentialsExchangeRequest{
		ClientID: "client-1", ClientSecret: "secret-1",
	})
	if err == nil || result != nil || sessions.inserted != nil {
		t.Fatalf("result=%#v err=%v inserted=%#v", result, err, sessions.inserted)
	}
	oauthError, ok := OAuthProtocolErrorFrom(err)
	if !ok || oauthError.Code != "invalid_client" {
		t.Fatalf("error=%#v, want invalid_client", err)
	}
}

func TestExchangeClientCredentialsSerializesIssuanceWithAccessRevocation(t *testing.T) {
	state := newOAuthCredentialRevocationRaceState()
	app := clientCredentialsConnectedApp()
	systemRepository := &oauthCredentialRaceSystemRepo{
		systemRepoStub: &systemRepoStub{app: app, permitted: true},
		state:          state,
	}
	sessionRepository := &oauthCredentialRaceSessionRepo{sessionRepoStub: &sessionRepoStub{}, state: state}
	service := NewOAuthService(nil, systemRepository, sessionRepository, NewAuthService(&userRepoStub{user: &models.User{
		ID: "user-1", IsActive: true,
	}}, sessionRepository, nil), &oauthRepoStub{})

	go state.revokeConnectedAppCredentials()
	result, err := service.ExchangeClientCredentials(context.Background(), ClientCredentialsExchangeRequest{
		ClientID: "client-1", ClientSecret: "secret-1",
	})
	if err != nil || result == nil || result.Token == nil {
		t.Fatalf("result=%#v err=%v", result, err)
	}
	<-state.revocationDone
	state.mu.Lock()
	active := state.activeSession
	state.mu.Unlock()
	if active {
		t.Fatal("access revocation raced with client-credentials issuance and left a session active")
	}
}
