package auth

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ============================================================================
// OAuth Mock Repositories
// ============================================================================

// mockSystemRepository implements ports.SystemRepository for unit testing.
type mockSystemRepository struct {
	getConnectedAppByClientIDFn func(ctx context.Context, clientID string) (*models.SystemConnectedApp, error)
}

func (m *mockSystemRepository) GetLogs(ctx context.Context, limit int) ([]*models.SystemLog, error) {
	return nil, nil
}
func (m *mockSystemRepository) GetRecentItems(ctx context.Context, userID string, limit int) ([]*models.SystemRecent, error) {
	return nil, nil
}
func (m *mockSystemRepository) GetConfig(ctx context.Context, key string) (*string, error) {
	return nil, nil
}
func (m *mockSystemRepository) GetAllConfigs(ctx context.Context) ([]*models.SystemConfig, error) {
	return nil, nil
}
func (m *mockSystemRepository) GetConfigID(ctx context.Context, key string) (string, error) {
	return "", nil
}
func (m *mockSystemRepository) UpsertConfig(ctx context.Context, config *models.SystemConfig) error {
	return nil
}
func (m *mockSystemRepository) DeleteConfig(ctx context.Context, key string) error {
	return nil
}
func (m *mockSystemRepository) GetProfileIDByName(ctx context.Context, name string) (string, error) {
	return "", nil
}
func (m *mockSystemRepository) GetUserIDByEmail(ctx context.Context, email string) (string, error) {
	return "", nil
}
func (m *mockSystemRepository) BatchUpsertProfiles(ctx context.Context, profiles []models.Profile) error {
	return nil
}
func (m *mockSystemRepository) RenameDuplicateProfilesByName(ctx context.Context, bootstrapID, currentName, newName string) (int64, error) {
	return 0, nil
}
func (m *mockSystemRepository) BatchUpsertUsers(ctx context.Context, users []models.User) error {
	return nil
}
func (m *mockSystemRepository) ForceUpdateUserPassword(ctx context.Context, email string, hashedPassword string) error {
	return nil
}
func (m *mockSystemRepository) GetConnectedAppByClientID(ctx context.Context, clientID string) (*models.SystemConnectedApp, error) {
	if m.getConnectedAppByClientIDFn != nil {
		return m.getConnectedAppByClientIDFn(ctx, clientID)
	}
	return nil, nil
}

func (m *mockSystemRepository) IsUserPermittedForApp(ctx context.Context, appID string, userID string) (bool, error) {
	return true, nil
}

// mockOAuthRepository implements ports.OAuthRepository for unit testing.
type mockOAuthRepository struct {
	storeAuthCodeFn       func(ctx context.Context, code, clientID, redirectURI, scope, userID, nonce string, expiresAt time.Time) error
	consumeAuthCodeFn     func(ctx context.Context, code string) (*ports.OAuthCodeData, error)
	cleanupExpiredCodesFn func(ctx context.Context) (int64, error)
}

func (m *mockOAuthRepository) StoreAuthCode(ctx context.Context, code, clientID, redirectURI, scope, userID, nonce string, expiresAt time.Time) error {
	if m.storeAuthCodeFn != nil {
		return m.storeAuthCodeFn(ctx, code, clientID, redirectURI, scope, userID, nonce, expiresAt)
	}
	return nil
}

func (m *mockOAuthRepository) ConsumeAuthCode(ctx context.Context, code string) (*ports.OAuthCodeData, error) {
	if m.consumeAuthCodeFn != nil {
		return m.consumeAuthCodeFn(ctx, code)
	}
	return nil, nil
}

func (m *mockOAuthRepository) CleanupExpiredCodes(ctx context.Context) (int64, error) {
	if m.cleanupExpiredCodesFn != nil {
		return m.cleanupExpiredCodesFn(ctx)
	}
	return 0, nil
}

// ============================================================================
// OAuth Test Helpers
// ============================================================================

func newTestOAuthService() (*OAuthService, *mockUserRepository, *mockSystemRepository, *mockSessionRepository, *mockOAuthRepository) {
	userRepo := &mockUserRepository{}
	systemRepo := &mockSystemRepository{}
	sessionRepo := &mockSessionRepository{}
	permRepo := &mockPermissionRepository{}
	authSvc := NewAuthService(userRepo, sessionRepo, permRepo)
	oauthRepo := &mockOAuthRepository{}

	oauthSvc := NewOAuthService(userRepo, systemRepo, sessionRepo, authSvc, oauthRepo)
	return oauthSvc, userRepo, systemRepo, sessionRepo, oauthRepo
}

// testConnectedApp returns a standard test connected app.
func testConnectedApp() *models.SystemConnectedApp {
	return &models.SystemConnectedApp{
		ID:             "app-001",
		Name:           "Test App",
		ConsumerKey:    "client-id-123",
		ConsumerSecret: "super-secret",
		CallbackURL:    strPtr("https://app.example.com/callback"),
		IsActive:       true,
	}
}

// ============================================================================
// Tests — GenerateAuthCode
// ============================================================================

func TestGenerateAuthCode(t *testing.T) {
	app := testConnectedApp()

	tests := []struct {
		name        string
		clientID    string
		redirectURI string
		scope       string
		userID      string
		setupMocks  func(*mockSystemRepository, *mockOAuthRepository)
		wantErr     bool
		errContains string
	}{
		{
			name:        "successful auth code generation",
			clientID:    app.ConsumerKey,
			redirectURI: *app.CallbackURL,
			scope:       "full",
			userID:      "user-001",
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
				or.storeAuthCodeFn = func(_ context.Context, code, clientID, redirectURI, scope, userID, nonce string, expiresAt time.Time) error {
					assert.NotEmpty(t, code)
					assert.Equal(t, app.ConsumerKey, clientID)
					assert.True(t, expiresAt.After(time.Now()))
					return nil
				}
			},
			wantErr: false,
		},
		{
			name:        "redirect URI mismatch returns validation error",
			clientID:    app.ConsumerKey,
			redirectURI: "https://evil.example.com/callback",
			scope:       "full",
			userID:      "user-001",
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
			},
			wantErr:     true,
			errContains: "Mismatch",
		},
		{
			name:        "invalid client ID propagates error",
			clientID:    "unknown-client",
			redirectURI: *app.CallbackURL,
			scope:       "full",
			userID:      "user-001",
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return nil, fmt.Errorf("app not found")
				}
			},
			wantErr:     true,
			errContains: "app not found",
		},
		{
			name:        "database error storing code propagates",
			clientID:    app.ConsumerKey,
			redirectURI: *app.CallbackURL,
			scope:       "full",
			userID:      "user-001",
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
				or.storeAuthCodeFn = func(_ context.Context, _, _, _, _, _, _ string, _ time.Time) error {
					return fmt.Errorf("write error")
				}
			},
			wantErr:     true,
			errContains: "write error",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			oauthSvc, _, systemRepo, _, oauthRepo := newTestOAuthService()
			if tc.setupMocks != nil {
				tc.setupMocks(systemRepo, oauthRepo)
			}

			code, err := oauthSvc.GenerateAuthCode(context.Background(), tc.clientID, tc.redirectURI, tc.scope, tc.userID)

			if tc.wantErr {
				assert.Error(t, err)
				if tc.errContains != "" {
					assert.Contains(t, err.Error(), tc.errContains)
				}
				assert.Empty(t, code)
			} else {
				require.NoError(t, err)
				assert.NotEmpty(t, code)
			}
		})
	}
}

// ============================================================================
// Tests — ExchangeToken
// ============================================================================

func TestExchangeToken(t *testing.T) {
	app := testConnectedApp()

	tests := []struct {
		name        string
		clientID    string
		secret      string
		code        string
		redirectURI string
		setupMocks  func(*mockSystemRepository, *mockOAuthRepository, *mockUserRepository, *mockSessionRepository)
		wantErr     bool
		errContains string
	}{
		{
			name:        "successful token exchange",
			clientID:    app.ConsumerKey,
			secret:      app.ConsumerSecret,
			code:        "valid-code",
			redirectURI: *app.CallbackURL,
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository, ur *mockUserRepository, sess *mockSessionRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
				or.consumeAuthCodeFn = func(_ context.Context, code string) (*ports.OAuthCodeData, error) {
					return &ports.OAuthCodeData{
						ClientID:    app.ConsumerKey,
						RedirectURI: *app.CallbackURL,
						Scope:       "full",
						UserID:      "user-001",
						ExpiresAt:   time.Now().Add(5 * time.Minute),
					}, nil
				}
				ur.getUserByIDFn = func(_ context.Context, id string) (*models.User, error) {
					return &models.User{
						ID:        id,
						Username:  "oauth-user@example.com",
						Email:     "oauth-user@example.com",
						ProfileID: "standard_user",
						IsActive:  true,
					}, nil
				}
			},
			wantErr: false,
		},
		{
			name:        "wrong client secret returns unauthorized",
			clientID:    app.ConsumerKey,
			secret:      "wrong-secret",
			code:        "valid-code",
			redirectURI: *app.CallbackURL,
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository, ur *mockUserRepository, sess *mockSessionRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
			},
			wantErr:     true,
			errContains: "Invalid client secret",
		},
		{
			name:        "expired or used code returns validation error",
			clientID:    app.ConsumerKey,
			secret:      app.ConsumerSecret,
			code:        "expired-code",
			redirectURI: *app.CallbackURL,
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository, ur *mockUserRepository, sess *mockSessionRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
				or.consumeAuthCodeFn = func(_ context.Context, _ string) (*ports.OAuthCodeData, error) {
					return nil, nil // code not found / expired
				}
			},
			wantErr:     true,
			errContains: "Invalid or expired",
		},
		{
			name:        "client ID mismatch in stored code",
			clientID:    app.ConsumerKey,
			secret:      app.ConsumerSecret,
			code:        "other-client-code",
			redirectURI: *app.CallbackURL,
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository, ur *mockUserRepository, sess *mockSessionRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
				or.consumeAuthCodeFn = func(_ context.Context, _ string) (*ports.OAuthCodeData, error) {
					return &ports.OAuthCodeData{
						ClientID:    "different-client",
						RedirectURI: *app.CallbackURL,
						UserID:      "user-001",
					}, nil
				}
			},
			wantErr:     true,
			errContains: "different client",
		},
		{
			name:        "redirect URI mismatch in stored code",
			clientID:    app.ConsumerKey,
			secret:      app.ConsumerSecret,
			code:        "mismatch-redirect-code",
			redirectURI: "https://different.example.com/callback",
			setupMocks: func(sr *mockSystemRepository, or *mockOAuthRepository, ur *mockUserRepository, sess *mockSessionRepository) {
				sr.getConnectedAppByClientIDFn = func(_ context.Context, _ string) (*models.SystemConnectedApp, error) {
					return app, nil
				}
				or.consumeAuthCodeFn = func(_ context.Context, _ string) (*ports.OAuthCodeData, error) {
					return &ports.OAuthCodeData{
						ClientID:    app.ConsumerKey,
						RedirectURI: *app.CallbackURL,
						UserID:      "user-001",
					}, nil
				}
			},
			wantErr:     true,
			errContains: "Redirect URI mismatch",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			oauthSvc, userRepo, systemRepo, sessionRepo, oauthRepo := newTestOAuthService()
			if tc.setupMocks != nil {
				tc.setupMocks(systemRepo, oauthRepo, userRepo, sessionRepo)
			}

			token, err := oauthSvc.ExchangeToken(context.Background(), tc.clientID, tc.secret, tc.code, tc.redirectURI)

			if tc.wantErr {
				assert.Error(t, err)
				if tc.errContains != "" {
					assert.Contains(t, err.Error(), tc.errContains)
				}
				assert.Nil(t, token)
			} else {
				require.NoError(t, err)
				require.NotNil(t, token)
				assert.NotEmpty(t, token.AccessToken)
				assert.Equal(t, "Bearer", token.TokenType)
				assert.NotEmpty(t, token.ID)
				assert.NotEmpty(t, token.InstanceURL)
				assert.NotEmpty(t, token.IssuedAt)
				assert.NotEmpty(t, token.Signature)
			}
		})
	}
}

// ============================================================================
// Tests — computeTokenSignature
// ============================================================================

func TestComputeTokenSignature(t *testing.T) {
	tests := []struct {
		name           string
		consumerSecret string
		tokenID        string
		issuedAt       string
	}{
		{
			name:           "produces consistent signature for same inputs",
			consumerSecret: "my-secret",
			tokenID:        "http://127.0.0.1:3001/id/org/user1",
			issuedAt:       "1700000000000",
		},
		{
			name:           "different secrets produce different signatures",
			consumerSecret: "different-secret",
			tokenID:        "http://127.0.0.1:3001/id/org/user1",
			issuedAt:       "1700000000000",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			sig1 := computeTokenSignature(tc.consumerSecret, tc.tokenID, tc.issuedAt)
			sig2 := computeTokenSignature(tc.consumerSecret, tc.tokenID, tc.issuedAt)

			assert.NotEmpty(t, sig1)
			assert.Equal(t, sig1, sig2, "same inputs should produce same signature")
		})
	}

	// Cross-check: different secret must give different signature
	sig1 := computeTokenSignature("secret-A", "token-id", "1234")
	sig2 := computeTokenSignature("secret-B", "token-id", "1234")
	assert.NotEqual(t, sig1, sig2, "different secrets must produce different signatures")
}

// ============================================================================
// Tests — NewOAuthService constructor
// ============================================================================

func TestNewOAuthService(t *testing.T) {
	oauthSvc, _, _, _, _ := newTestOAuthService()
	require.NotNil(t, oauthSvc, "NewOAuthService should return a non-nil service")
	assert.NotNil(t, oauthSvc.authService)
	assert.NotNil(t, oauthSvc.userRepo)
	assert.NotNil(t, oauthSvc.systemRepo)
	assert.NotNil(t, oauthSvc.sessionRepo)
	assert.NotNil(t, oauthSvc.oauthRepo)
}
