package grpc

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
	"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	"github.com/netbirdio/netbird/management/server/types"
)

type mockReverseProxyManager struct {
	proxiesByAccount map[string][]*service.Service
	err              error
}

func (m *mockReverseProxyManager) DeleteAllServices(ctx context.Context, accountID, userID string) error {
	return nil
}

func (m *mockReverseProxyManager) GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error) {
	if m.err != nil {
		return nil, m.err
	}
	return m.proxiesByAccount[accountID], nil
}

func (m *mockReverseProxyManager) GetGlobalServices(ctx context.Context) ([]*service.Service, error) {
	return nil, nil
}

func (m *mockReverseProxyManager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) {
	return []*service.Service{}, nil
}

func (m *mockReverseProxyManager) GetService(ctx context.Context, accountID, userID, reverseProxyID string) (*service.Service, error) {
	return &service.Service{}, nil
}

func (m *mockReverseProxyManager) CreateService(ctx context.Context, accountID, userID string, rp *service.Service) (*service.Service, error) {
	return &service.Service{}, nil
}

func (m *mockReverseProxyManager) UpdateService(ctx context.Context, accountID, userID string, rp *service.Service) (*service.Service, error) {
	return &service.Service{}, nil
}

func (m *mockReverseProxyManager) DeleteService(ctx context.Context, accountID, userID, reverseProxyID string) error {
	return nil
}

func (m *mockReverseProxyManager) DeleteAccountCluster(_ context.Context, _, _, _ string) error {
	return nil
}

func (m *mockReverseProxyManager) SetCertificateIssuedAt(ctx context.Context, accountID, reverseProxyID string) error {
	return nil
}

func (m *mockReverseProxyManager) SetStatus(ctx context.Context, accountID, reverseProxyID string, status service.Status) error {
	return nil
}

func (m *mockReverseProxyManager) ReloadAllServicesForAccount(ctx context.Context, accountID string) error {
	return nil
}

func (m *mockReverseProxyManager) ReloadService(ctx context.Context, accountID, reverseProxyID string) error {
	return nil
}

func (m *mockReverseProxyManager) GetServiceByID(ctx context.Context, accountID, reverseProxyID string) (*service.Service, error) {
	return &service.Service{}, nil
}

func (m *mockReverseProxyManager) GetServiceIDByTargetID(_ context.Context, _, _ string) (string, error) {
	return "", nil
}

func (m *mockReverseProxyManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *service.ExposeServiceRequest) (*service.ExposeServiceResponse, error) {
	return &service.ExposeServiceResponse{}, nil
}

func (m *mockReverseProxyManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error {
	return nil
}

func (m *mockReverseProxyManager) StopServiceFromPeer(_ context.Context, _, _, _ string) error {
	return nil
}

func (m *mockReverseProxyManager) StartExposeReaper(_ context.Context) {}

func (m *mockReverseProxyManager) GetServiceByDomain(_ context.Context, domain string) (*service.Service, error) {
	if m.err != nil {
		return nil, m.err
	}
	for _, services := range m.proxiesByAccount {
		for _, svc := range services {
			if svc.Domain == domain {
				return svc, nil
			}
		}
	}
	return nil, errors.New("service not found for domain: " + domain)
}

func (m *mockReverseProxyManager) GetClusters(_ context.Context, _, _ string) ([]proxy.Cluster, error) {
	return nil, nil
}

type mockUsersManager struct {
	users map[string]*types.User
	err   error
}

func (m *mockUsersManager) GetUser(ctx context.Context, userID string) (*types.User, error) {
	if m.err != nil {
		return nil, m.err
	}
	user, ok := m.users[userID]
	if !ok {
		return nil, errors.New("user not found")
	}
	return user, nil
}

func (m *mockUsersManager) GetUserWithGroups(ctx context.Context, userID string) (*types.User, []*types.Group, error) {
	user, err := m.GetUser(ctx, userID)
	if err != nil {
		return nil, nil, err
	}
	return user, nil, nil
}

func TestValidateUserGroupAccess(t *testing.T) {
	tests := []struct {
		name             string
		domain           string
		userID           string
		proxiesByAccount map[string][]*service.Service
		users            map[string]*types.User
		proxyErr         error
		userErr          error
		expectErr        bool
		expectErrMsg     string
	}{
		{
			name:   "user not found",
			domain: "app.example.com",
			userID: "unknown-user",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{Domain: "app.example.com", AccountID: "account1"}},
			},
			users:        map[string]*types.User{},
			expectErr:    true,
			expectErrMsg: "user not found",
		},
		{
			name:             "proxy not found in user's account",
			domain:           "app.example.com",
			userID:           "user1",
			proxiesByAccount: map[string][]*service.Service{},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr:    true,
			expectErrMsg: "service not found",
		},
		{
			name:   "proxy exists in different account - not accessible",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account2": {{Domain: "app.example.com", AccountID: "account2"}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr:    true,
			expectErrMsg: "service not found",
		},
		{
			name:   "no bearer auth configured - same account allows access",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{Domain: "app.example.com", AccountID: "account1", Auth: service.AuthConfig{}}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr: false,
		},
		{
			name:   "bearer auth disabled - same account allows access",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{
					Domain:    "app.example.com",
					AccountID: "account1",
					Auth: service.AuthConfig{
						BearerAuth: &service.BearerAuthConfig{Enabled: false},
					},
				}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr: false,
		},
		{
			name:   "bearer auth enabled but no groups configured - same account allows access",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{
					Domain:    "app.example.com",
					AccountID: "account1",
					Auth: service.AuthConfig{
						BearerAuth: &service.BearerAuthConfig{
							Enabled:            true,
							DistributionGroups: []string{},
						},
					},
				}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr: false,
		},
		{
			name:   "user not in allowed groups",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{
					Domain:    "app.example.com",
					AccountID: "account1",
					Auth: service.AuthConfig{
						BearerAuth: &service.BearerAuthConfig{
							Enabled:            true,
							DistributionGroups: []string{"group1", "group2"},
						},
					},
				}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1", AutoGroups: []string{"group3", "group4"}},
			},
			expectErr:    true,
			expectErrMsg: "not in allowed groups",
		},
		{
			name:   "user in one of the allowed groups - allow access",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{
					Domain:    "app.example.com",
					AccountID: "account1",
					Auth: service.AuthConfig{
						BearerAuth: &service.BearerAuthConfig{
							Enabled:            true,
							DistributionGroups: []string{"group1", "group2"},
						},
					},
				}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1", AutoGroups: []string{"group2", "group3"}},
			},
			expectErr: false,
		},
		{
			name:   "user in all allowed groups - allow access",
			domain: "app.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{
					Domain:    "app.example.com",
					AccountID: "account1",
					Auth: service.AuthConfig{
						BearerAuth: &service.BearerAuthConfig{
							Enabled:            true,
							DistributionGroups: []string{"group1", "group2"},
						},
					},
				}},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1", AutoGroups: []string{"group1", "group2", "group3"}},
			},
			expectErr: false,
		},
		{
			name:             "proxy manager error",
			domain:           "app.example.com",
			userID:           "user1",
			proxiesByAccount: nil,
			proxyErr:         errors.New("database error"),
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr:    true,
			expectErrMsg: "get account services",
		},
		{
			name:   "multiple proxies in account - finds correct one",
			domain: "app2.example.com",
			userID: "user1",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {
					{Domain: "app1.example.com", AccountID: "account1"},
					{Domain: "app2.example.com", AccountID: "account1", Auth: service.AuthConfig{}},
					{Domain: "app3.example.com", AccountID: "account1"},
				},
			},
			users: map[string]*types.User{
				"user1": {Id: "user1", AccountID: "account1"},
			},
			expectErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := &ProxyServiceServer{
				serviceManager: &mockReverseProxyManager{
					proxiesByAccount: tt.proxiesByAccount,
					err:              tt.proxyErr,
				},
				usersManager: &mockUsersManager{
					users: tt.users,
					err:   tt.userErr,
				},
			}

			err := server.ValidateUserGroupAccess(context.Background(), tt.domain, tt.userID)

			if tt.expectErr {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.expectErrMsg)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestGetAccountProxyByDomain(t *testing.T) {
	tests := []struct {
		name             string
		accountID        string
		domain           string
		proxiesByAccount map[string][]*service.Service
		err              error
		expectProxy      bool
		expectErr        bool
	}{
		{
			name:      "proxy found",
			accountID: "account1",
			domain:    "app.example.com",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {
					{Domain: "other.example.com", AccountID: "account1"},
					{Domain: "app.example.com", AccountID: "account1"},
				},
			},
			expectProxy: true,
			expectErr:   false,
		},
		{
			name:      "proxy not found in account",
			accountID: "account1",
			domain:    "unknown.example.com",
			proxiesByAccount: map[string][]*service.Service{
				"account1": {{Domain: "app.example.com", AccountID: "account1"}},
			},
			expectProxy: false,
			expectErr:   true,
		},
		{
			name:             "empty proxy list for account",
			accountID:        "account1",
			domain:           "app.example.com",
			proxiesByAccount: map[string][]*service.Service{},
			expectProxy:      false,
			expectErr:        true,
		},
		{
			name:             "manager error",
			accountID:        "account1",
			domain:           "app.example.com",
			proxiesByAccount: nil,
			err:              errors.New("database error"),
			expectProxy:      false,
			expectErr:        true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := &ProxyServiceServer{
				serviceManager: &mockReverseProxyManager{
					proxiesByAccount: tt.proxiesByAccount,
					err:              tt.err,
				},
			}

			proxy, err := server.getAccountServiceByDomain(context.Background(), tt.accountID, tt.domain)

			if tt.expectErr {
				require.Error(t, err)
				assert.Nil(t, proxy)
			} else {
				require.NoError(t, err)
				require.NotNil(t, proxy)
				assert.Equal(t, tt.domain, proxy.Domain)
			}
		})
	}
}

func TestCheckPeerGroupAccess(t *testing.T) {
	t.Run("private with empty AccessGroups denies", func(t *testing.T) {
		svc := &service.Service{Private: true, AccessGroups: nil}
		err := checkPeerGroupAccess(svc, []string{"grp-admins"})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "no access groups")
	})

	t.Run("private with peer in AccessGroups allows", func(t *testing.T) {
		svc := &service.Service{Private: true, AccessGroups: []string{"grp-admins", "grp-ops"}}
		assert.NoError(t, checkPeerGroupAccess(svc, []string{"grp-other", "grp-ops"}))
	})

	t.Run("private with peer outside AccessGroups denies", func(t *testing.T) {
		svc := &service.Service{Private: true, AccessGroups: []string{"grp-admins"}}
		assert.Error(t, checkPeerGroupAccess(svc, []string{"grp-other"}))
	})

	t.Run("bearer enabled with empty DistributionGroups allows", func(t *testing.T) {
		svc := &service.Service{
			Auth: service.AuthConfig{BearerAuth: &service.BearerAuthConfig{Enabled: true}},
		}
		assert.NoError(t, checkPeerGroupAccess(svc, []string{"grp-anyone"}))
	})

	t.Run("bearer enabled gates on DistributionGroups", func(t *testing.T) {
		svc := &service.Service{
			Auth: service.AuthConfig{
				BearerAuth: &service.BearerAuthConfig{
					Enabled:            true,
					DistributionGroups: []string{"grp-allowed"},
				},
			},
		}
		assert.NoError(t, checkPeerGroupAccess(svc, []string{"grp-allowed"}))
		assert.Error(t, checkPeerGroupAccess(svc, []string{"grp-other"}))
	})

	t.Run("non-private non-bearer is open", func(t *testing.T) {
		assert.NoError(t, checkPeerGroupAccess(&service.Service{}, nil))
	})
}
