import { defineComponent } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { screen, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import { type FrontendSettings, type Role, ROLE, type UsersList } from '@n8n/api-types';
import type { IUser } from '@n8n/rest-api-client/api/users';
import { EnterpriseEditionFeature } from '@/app/constants';
import { INVITE_USER_MODAL_KEY, DELETE_USER_MODAL_KEY } from '../users.constants';
import SettingsUsersView from './SettingsUsersView.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { useEmitters } from '@/__tests__/utils';
import { mockedStore, type MockedStore } from '@/__tests__/utils';
import { useUsersStore } from '../users.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useSSOStore } from '@/features/settings/sso/sso.store';
import * as permissions from '@/app/utils/rbac/permissions';
import { TAMPER_PROOF_INVITE_LINKS } from '@/app/constants/experiments';
import type { PermissionTypeOptions } from '@/app/types/rbac';

const { emitters, addEmitter } = useEmitters<'settingsUsersTable'>();

// Mock the SettingsUsersTable component to emit events and render an accessible actions list per user
vi.mock('../components/SettingsUsersTable.vue', () => ({
	default: defineComponent({
		name: 'SettingsUsersTableStub',
		props: {
			data: { type: Object, required: false },
			actions: { type: Array, required: false },
		},
		setup(_, { emit }) {
			addEmitter('settingsUsersTable', emit);
		},
		template: `
			<div data-test-id="settings-users-table">
				<div v-for="u in (data?.items || [])" :key="u.id" :data-test-id="'user-' + u.id">
					<ul :aria-label="'Actions for user ' + u.id" role="list" :data-test-id="'actions-for-' + u.id">
						<li v-for="a in (actions || []).filter(act => !act?.guard || act.guard(u))" :key="a.value" role="listitem">
							<button type="button" :data-test-id="'action-' + a.value + '-' + u.id">{{ a.label || a.value }}</button>
						</li>
					</ul>
				</div>
			</div>
		`,
	}),
}));

const mockToast = {
	showToast: vi.fn(),
	showError: vi.fn(),
};

const mockClipboard = {
	copy: vi.fn(),
};

const mockPageRedirectionHelper = {
	goToUpgrade: vi.fn(),
};

vi.mock('@/app/composables/useToast', () => ({
	useToast: vi.fn(() => mockToast),
}));

vi.mock('@/app/composables/useClipboard', () => ({
	useClipboard: vi.fn(() => mockClipboard),
}));

vi.mock('@/app/composables/useDocumentTitle', () => ({
	useDocumentTitle: vi.fn(() => ({
		set: vi.fn(),
	})),
}));

vi.mock('@/app/composables/usePageRedirectionHelper', () => ({
	usePageRedirectionHelper: vi.fn(() => mockPageRedirectionHelper),
}));

const mockIsVariantEnabled = vi.fn().mockReturnValue(false);

vi.mock('@/app/stores/posthog.store', () => ({
	usePostHog: vi.fn(() => ({
		isVariantEnabled: mockIsVariantEnabled,
	})),
}));

const mockUsersList: UsersList = {
	items: [
		{
			id: '1',
			email: 'admin@example.com',
			firstName: 'Admin',
			lastName: 'User',
			role: ROLE.Admin,
			isOwner: true,
			isPending: false,
			settings: {},
		},
		{
			id: '2',
			email: 'member@example.com',
			firstName: 'Member',
			lastName: 'User',
			role: ROLE.Member,
			isOwner: false,
			isPending: false,
			settings: {},
		},
		{
			id: '3',
			email: 'pending@example.com',
			firstName: '',
			lastName: '',
			role: ROLE.Member,
			isOwner: false,
			isPending: true,
			settings: {},
			inviteAcceptUrl: 'https://example.com/invite/123',
		},
	],
	count: 3,
};

let renderComponent: ReturnType<typeof createComponentRenderer>;
let usersStore: MockedStore<typeof useUsersStore>;
let uiStore: MockedStore<typeof useUIStore>;
let settingsStore: MockedStore<typeof useSettingsStore>;
let ssoStore: MockedStore<typeof useSSOStore>;

describe('SettingsUsersView', () => {
	beforeEach(() => {
		// Reset mock to default state before each test
		mockIsVariantEnabled.mockReset();
		mockIsVariantEnabled.mockReturnValue(false);

		renderComponent = createComponentRenderer(SettingsUsersView, {
			pinia: createTestingPinia(),
		});

		usersStore = mockedStore(useUsersStore);
		uiStore = mockedStore(useUIStore);
		settingsStore = mockedStore(useSettingsStore);
		ssoStore = mockedStore(useSSOStore);

		// Setup default store states
		usersStore.usersLimitNotReached = true;
		usersStore.usersList = {
			state: mockUsersList,
			isLoading: false,
			execute: vi.fn(),
			isReady: true,
			error: null,
			then: vi.fn(),
		};
		usersStore.currentUserId = '1';
		usersStore.reinviteUser = vi.fn().mockResolvedValue(undefined);
		usersStore.getUserPasswordResetLink = vi
			.fn()
			.mockResolvedValue({ link: 'https://example.com/reset/123' });
		usersStore.updateOtherUserSettings = vi.fn().mockResolvedValue(undefined);
		usersStore.updateGlobalRole = vi.fn().mockResolvedValue(undefined);
		usersStore.generateInviteLink = vi
			.fn()
			.mockResolvedValue({ link: 'https://example.com/signup?token=generated-token' });

		settingsStore.isSmtpSetup = true;
		settingsStore.settings.enterprise = {} as FrontendSettings['enterprise'];
		settingsStore.settings.enterprise[EnterpriseEditionFeature.AdvancedPermissions] = true;
		ssoStore.isSamlLoginEnabled = false;
		ssoStore.isOidcLoginEnabled = false;
	});

	afterEach(() => {
		vi.clearAllMocks();
		// Reset mock implementation to default (feature flag disabled)
		mockIsVariantEnabled.mockReset();
		mockIsVariantEnabled.mockReturnValue(false);
	});

	it('should handle missing user settings in SSO actions', async () => {
		// Remove settings from user
		usersStore.usersList.state.items[1].settings = undefined;

		renderComponent();

		emitters.settingsUsersTable.emit('action', { action: 'allowSSOManualLogin', userId: '2' });

		expect(usersStore.updateOtherUserSettings).toHaveBeenCalledWith('2', {
			allowSSOManualLogin: true,
		});
	});

	it('should render correctly with users list', () => {
		renderComponent();

		expect(screen.getByRole('heading', { name: /users/i })).toBeInTheDocument();
		expect(screen.getByTestId('users-list-search')).toBeInTheDocument();
		expect(screen.getByTestId('settings-users-invite-button')).toBeInTheDocument();
	});

	it('should open invite modal when invite button is clicked', async () => {
		renderComponent();

		const inviteButton = screen.getByTestId('settings-users-invite-button');
		await userEvent.click(inviteButton);

		expect(uiStore.openModalWithData).toHaveBeenCalledWith({
			name: INVITE_USER_MODAL_KEY,
			data: {
				afterInvite: expect.any(Function),
			},
		});
	});

	it('should disable invite button when SAML SSO is enabled', () => {
		ssoStore.isSamlLoginEnabled = true;

		renderComponent();

		const inviteButton = screen.getByTestId('settings-users-invite-button');
		expect(inviteButton).toBeDisabled();
	});

	it('should disable invite button when OIDC SSO is enabled', () => {
		ssoStore.isOidcLoginEnabled = true;

		renderComponent();

		const inviteButton = screen.getByTestId('settings-users-invite-button');
		expect(inviteButton).toBeDisabled();
	});

	it('should disable invite button when users limit is reached', () => {
		usersStore.usersLimitNotReached = false;

		renderComponent();

		const inviteButton = screen.getByTestId('settings-users-invite-button');
		expect(inviteButton).toBeDisabled();
	});

	it('should handle search input with debouncing', async () => {
		renderComponent();

		const searchInput = screen.getByTestId('users-list-search');
		await userEvent.type(searchInput, 'test search');

		await waitFor(() => {
			expect(usersStore.usersList.execute).toHaveBeenCalled();
		});
	});

	it('should show upgrade banner when users limit is reached', () => {
		usersStore.usersLimitNotReached = false;

		const { getByTestId } = renderComponent();

		expect(getByTestId('action-box')).toBeInTheDocument();
	});

	it('should show advanced permissions notice when feature is disabled', async () => {
		settingsStore.settings.enterprise[EnterpriseEditionFeature.AdvancedPermissions] = false;

		renderComponent();

		expect(screen.getByTestId('upgrade-permissions-link')).toBeInTheDocument();
		await userEvent.click(screen.getByTestId('upgrade-permissions-link'));

		expect(mockPageRedirectionHelper.goToUpgrade).toHaveBeenCalled();
	});

	it('should not show users table when limit is reached and only one user exists', () => {
		usersStore.usersLimitNotReached = false;
		usersStore.usersList.state = {
			...mockUsersList,
			count: 1,
		};

		const { queryByTestId } = renderComponent();

		expect(queryByTestId('settings-users-table')).not.toBeInTheDocument();
	});

	it('should show users table when limit is reached but multiple users exist', () => {
		usersStore.usersLimitNotReached = false;
		usersStore.usersList.state = {
			...mockUsersList,
			count: 3,
		};

		const { getByTestId } = renderComponent();

		// The users container should be visible when there are multiple users
		expect(getByTestId('settings-users-table')).toBeInTheDocument();
	});

	it('should include delete action and pass guard for non-current user', () => {
		const spy = vi
			.spyOn(permissions, 'hasPermission')
			.mockImplementation((features: string[]) => features.includes('rbac'));
		renderComponent();

		// ensure the member (id: 2) has Delete action rendered in the accessible list
		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.getByTestId('action-delete-2')).toBeInTheDocument();
		spy.mockRestore();
	});

	it('should show allow SSO manual login action when SAML is enabled', () => {
		ssoStore.isSamlLoginEnabled = true;
		// Ensure user 2 doesn't have allowSSOManualLogin set
		usersStore.usersList.state.items[1].settings = {};

		renderComponent();

		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.getByTestId('action-allowSSOManualLogin-2')).toBeInTheDocument();
	});

	it('should show allow SSO manual login action when OIDC is enabled', () => {
		ssoStore.isOidcLoginEnabled = true;
		// Ensure user 2 doesn't have allowSSOManualLogin set
		usersStore.usersList.state.items[1].settings = {};

		renderComponent();

		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.getByTestId('action-allowSSOManualLogin-2')).toBeInTheDocument();
	});

	it('should not show allow SSO manual login action when SSO is disabled', () => {
		ssoStore.isSamlLoginEnabled = false;
		ssoStore.isOidcLoginEnabled = false;

		renderComponent();

		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.queryByTestId('action-allowSSOManualLogin-2')).not.toBeInTheDocument();
	});

	it('should show disallow SSO manual login action when SAML is enabled and user has allowSSOManualLogin', () => {
		ssoStore.isSamlLoginEnabled = true;
		usersStore.usersList.state.items[1].settings = { allowSSOManualLogin: true };

		renderComponent();

		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.getByTestId('action-disallowSSOManualLogin-2')).toBeInTheDocument();
	});

	it('should show disallow SSO manual login action when OIDC is enabled and user has allowSSOManualLogin', () => {
		ssoStore.isOidcLoginEnabled = true;
		usersStore.usersList.state.items[1].settings = { allowSSOManualLogin: true };

		renderComponent();

		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.getByTestId('action-disallowSSOManualLogin-2')).toBeInTheDocument();
	});

	it('should not show disallow SSO manual login action when SSO is disabled', () => {
		ssoStore.isSamlLoginEnabled = false;
		ssoStore.isOidcLoginEnabled = false;
		usersStore.usersList.state.items[1].settings = { allowSSOManualLogin: true };

		renderComponent();

		const actionsList = screen.getByTestId('actions-for-2');
		expect(actionsList).toBeInTheDocument();
		expect(screen.queryByTestId('action-disallowSSOManualLogin-2')).not.toBeInTheDocument();
	});

	describe('search functionality', () => {
		it('should handle empty search', async () => {
			renderComponent();

			const searchInput = screen.getByTestId('users-list-search');
			await userEvent.clear(searchInput);

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
					skip: 0,
					take: 10,
					sortBy: ['firstName:asc', 'lastName:asc', 'email:asc'],
					expand: ['projectRelations'],
					filter: {
						fullText: '',
					},
				});
			});
		});

		it('should handle search with special characters', async () => {
			renderComponent();

			const searchInput = screen.getByTestId('users-list-search');
			await userEvent.type(searchInput, '@#$%^&*()');

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
					skip: 0,
					take: 10,
					sortBy: ['firstName:asc', 'lastName:asc', 'email:asc'],
					expand: ['projectRelations'],
					filter: {
						fullText: '@#$%^&*()',
					},
				});
			});
		});

		it('should reset to first page when searching', async () => {
			renderComponent();

			// Set table to page 2
			emitters.settingsUsersTable.emit('update:options', {
				page: 2,
				itemsPerPage: 10,
				sortBy: [{ id: 'email', desc: false }],
			});

			// Now search - should reset to page 0
			const searchInput = screen.getByTestId('users-list-search');
			await userEvent.type(searchInput, 'test');

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
					skip: 0, // Should be 0, not 20 (page 2)
					take: 10,
					sortBy: ['email:asc'],
					expand: ['projectRelations'],
					filter: {
						fullText: 'test',
					},
				});
			});
		});

		it('should handle search with whitespace only', async () => {
			renderComponent();

			const searchInput = screen.getByTestId('users-list-search');
			await userEvent.type(searchInput, '   ');

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
					skip: 0,
					take: 10,
					sortBy: ['firstName:asc', 'lastName:asc', 'email:asc'],
					expand: ['projectRelations'],
					filter: {
						fullText: '', // Should be trimmed to empty string
					},
				});
			});
		});
	});

	describe('component lifecycle', () => {
		it('should not load users data when showing UM setup warning', async () => {
			usersStore.currentUser = { isDefaultUser: true } as IUser;
			renderComponent();

			// Should not call execute when showing setup warning
			expect(usersStore.usersList.execute).not.toHaveBeenCalled();
		});

		it('should load users data on mount when not showing setup warning', async () => {
			renderComponent();

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
					skip: 0,
					take: 10,
					sortBy: ['firstName:asc', 'lastName:asc', 'email:asc'],
					expand: ['projectRelations'],
					filter: {
						fullText: '',
					},
				});
			});
		});
	});

	describe('user actions', () => {
		it('should handle delete user action', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'delete', userId: '2' });

			expect(uiStore.openModalWithData).toHaveBeenCalledWith({
				name: DELETE_USER_MODAL_KEY,
				data: {
					userId: '2',
					afterDelete: expect.any(Function),
				},
			});
		});

		it('should handle reinvite user action', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'reinvite', userId: '3' });

			expect(usersStore.reinviteUser).toHaveBeenCalledWith({
				email: 'pending@example.com',
				role: ROLE.Member,
			});
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should handle copy invite link action', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'copyInviteLink', userId: '3' });

			expect(mockClipboard.copy).toHaveBeenCalledWith('https://example.com/invite/123');
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should show copy invite link action when feature flag is disabled', () => {
			// Ensure feature flag is disabled - reset mock to ensure clean state
			// The beforeEach already sets this, but be explicit to avoid test order issues
			mockIsVariantEnabled.mockReset();
			mockIsVariantEnabled.mockReturnValue(false);

			renderComponent();

			// User 3 has inviteAcceptUrl and no firstName, so copyInviteLink should show
			// when feature flag is disabled
			const actionsList = screen.getByTestId('actions-for-3');
			expect(actionsList).toBeInTheDocument();
			expect(screen.getByTestId('action-copyInviteLink-3')).toBeInTheDocument();
		});

		it('should handle generate invite link action when feature flag is enabled', async () => {
			mockIsVariantEnabled.mockImplementation(
				(experiment: string, variant: string) =>
					experiment === TAMPER_PROOF_INVITE_LINKS.name &&
					variant === TAMPER_PROOF_INVITE_LINKS.variant,
			);

			const spy = vi
				.spyOn(permissions, 'hasPermission')
				.mockImplementation((features: string[], options?: Partial<PermissionTypeOptions>) => {
					if (features.includes('rbac') && options?.rbac?.scope === 'user:generateInviteLink') {
						return true;
					}
					return false;
				});

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'generateInviteLink', userId: '3' });

			expect(usersStore.generateInviteLink).toHaveBeenCalledWith({ id: '3' });
			await waitFor(() => {
				expect(mockClipboard.copy).toHaveBeenCalledWith(
					'https://example.com/signup?token=generated-token',
				);
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});

			spy.mockRestore();
		});

		it('should handle generate invite link error when feature flag is enabled', async () => {
			mockIsVariantEnabled.mockImplementation(
				(experiment: string, variant: string) =>
					experiment === TAMPER_PROOF_INVITE_LINKS.name &&
					variant === TAMPER_PROOF_INVITE_LINKS.variant,
			);

			usersStore.generateInviteLink = vi
				.fn()
				.mockRejectedValue(new Error('Failed to generate link'));

			const spy = vi
				.spyOn(permissions, 'hasPermission')
				.mockImplementation((features: string[], options?: Partial<PermissionTypeOptions>) => {
					if (features.includes('rbac') && options?.rbac?.scope === 'user:generateInviteLink') {
						return true;
					}
					return false;
				});

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'generateInviteLink', userId: '3' });

			await waitFor(() => {
				expect(mockToast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String));
			});

			spy.mockRestore();
		});

		it('should handle copy password reset link action', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'copyPasswordResetLink', userId: '2' });

			expect(usersStore.getUserPasswordResetLink).toHaveBeenCalledWith(mockUsersList.items[1]);
			await waitFor(() => {
				expect(mockClipboard.copy).toHaveBeenCalledWith('https://example.com/reset/123');
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should handle allow SSO manual login action when SAML is enabled', async () => {
			ssoStore.isSamlLoginEnabled = true;

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'allowSSOManualLogin', userId: '2' });

			expect(usersStore.updateOtherUserSettings).toHaveBeenCalledWith('2', {
				allowSSOManualLogin: true,
			});
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should handle allow SSO manual login action when OIDC is enabled', async () => {
			ssoStore.isOidcLoginEnabled = true;

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'allowSSOManualLogin', userId: '2' });

			expect(usersStore.updateOtherUserSettings).toHaveBeenCalledWith('2', {
				allowSSOManualLogin: true,
			});
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should handle disallow SSO manual login action when SAML is enabled', async () => {
			ssoStore.isSamlLoginEnabled = true;
			// Set user to have SSO manual login enabled
			usersStore.usersList.state.items[1].settings = { allowSSOManualLogin: true };

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'disallowSSOManualLogin', userId: '2' });

			expect(usersStore.updateOtherUserSettings).toHaveBeenCalledWith('2', {
				allowSSOManualLogin: false,
			});
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should handle disallow SSO manual login action when OIDC is enabled', async () => {
			ssoStore.isOidcLoginEnabled = true;
			// Set user to have SSO manual login enabled
			usersStore.usersList.state.items[1].settings = { allowSSOManualLogin: true };

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'disallowSSOManualLogin', userId: '2' });

			expect(usersStore.updateOtherUserSettings).toHaveBeenCalledWith('2', {
				allowSSOManualLogin: false,
			});
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should handle reinvite user error', async () => {
			usersStore.reinviteUser = vi.fn().mockRejectedValue(new Error('Failed to reinvite'));

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'reinvite', userId: '3' });

			await waitFor(() => {
				expect(mockToast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String));
			});
		});

		it('should handle copy invite link with missing inviteAcceptUrl', async () => {
			// Remove invite URL from user
			usersStore.usersList.state.items[2].inviteAcceptUrl = undefined;

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'copyInviteLink', userId: '3' });

			// Should not call clipboard.copy or show toast
			expect(mockClipboard.copy).not.toHaveBeenCalled();
			expect(mockToast.showToast).not.toHaveBeenCalled();
		});

		it('should hide copy invite link action when feature flag is enabled', () => {
			mockIsVariantEnabled.mockImplementation(
				(experiment: string, variant: string) =>
					experiment === TAMPER_PROOF_INVITE_LINKS.name &&
					variant === TAMPER_PROOF_INVITE_LINKS.variant,
			);

			renderComponent();

			// User 3 has inviteAcceptUrl and no firstName, so copyInviteLink would normally show
			const actionsList = screen.getByTestId('actions-for-3');
			expect(actionsList).toBeInTheDocument();
			// Copy invite link should not be in the actions list
			expect(screen.queryByTestId('action-copyInviteLink-3')).not.toBeInTheDocument();
		});

		it('should show generate invite link action when feature flag is enabled', () => {
			mockIsVariantEnabled.mockImplementation(
				(experiment: string, variant: string) =>
					experiment === TAMPER_PROOF_INVITE_LINKS.name &&
					variant === TAMPER_PROOF_INVITE_LINKS.variant,
			);

			const spy = vi
				.spyOn(permissions, 'hasPermission')
				.mockImplementation((features: string[], options?: Partial<PermissionTypeOptions>) => {
					if (features.includes('rbac') && options?.rbac?.scope === 'user:generateInviteLink') {
						return true;
					}
					return false;
				});

			renderComponent();

			// User 3 is not the current user, so generateInviteLink should show
			const actionsList = screen.getByTestId('actions-for-3');
			expect(actionsList).toBeInTheDocument();
			expect(screen.getByTestId('action-generateInviteLink-3')).toBeInTheDocument();

			spy.mockRestore();
		});

		it('should hide generate invite link action when feature flag is disabled', () => {
			mockIsVariantEnabled.mockImplementation(
				(experiment: string, variant: string) =>
					!(
						experiment === TAMPER_PROOF_INVITE_LINKS.name &&
						variant === TAMPER_PROOF_INVITE_LINKS.variant
					),
			);

			const spy = vi
				.spyOn(permissions, 'hasPermission')
				.mockImplementation((features: string[], options?: Partial<PermissionTypeOptions>) => {
					if (features.includes('rbac') && options?.rbac?.scope === 'user:generateInviteLink') {
						return true;
					}
					return false;
				});

			renderComponent();

			// Generate invite link should not be in the actions list when feature flag is disabled
			expect(screen.queryByTestId('action-generateInviteLink-3')).not.toBeInTheDocument();

			spy.mockRestore();
		});

		it('should hide generate invite link action when user has already accepted invite', () => {
			mockIsVariantEnabled.mockImplementation(
				(experiment: string, variant: string) =>
					experiment === TAMPER_PROOF_INVITE_LINKS.name &&
					variant === TAMPER_PROOF_INVITE_LINKS.variant,
			);

			const spy = vi
				.spyOn(permissions, 'hasPermission')
				.mockImplementation((features: string[], options?: Partial<PermissionTypeOptions>) => {
					if (features.includes('rbac') && options?.rbac?.scope === 'user:generateInviteLink') {
						return true;
					}
					return false;
				});

			// Set user with firstName (already accepted)
			usersStore.usersList.state.items[2].firstName = 'John';

			renderComponent();

			// Generate invite link should not be in the actions list when user has accepted
			expect(screen.queryByTestId('action-generateInviteLink-3')).not.toBeInTheDocument();

			spy.mockRestore();
		});

		it('should handle copy password reset link error', async () => {
			usersStore.getUserPasswordResetLink = vi
				.fn()
				.mockRejectedValue(new Error('Reset link failed'));

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'copyPasswordResetLink', userId: '2' });

			await waitFor(() => {
				expect(usersStore.getUserPasswordResetLink).toHaveBeenCalled();
				expect(mockClipboard.copy).not.toHaveBeenCalled();
				expect(mockToast.showToast).not.toHaveBeenCalled();
			});
		});

		it('should handle reinvite with invalid role', async () => {
			// Set user with invalid role
			usersStore.usersList.state.items[2].role = 'invalid-role' as Role;

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'reinvite', userId: '3' });

			// Should not call reinviteUser with invalid role
			expect(usersStore.reinviteUser).not.toHaveBeenCalled();
		});

		it('should handle reinvite with missing user data', async () => {
			// Remove email from user
			usersStore.usersList.state.items[2].email = undefined;

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'reinvite', userId: '3' });

			// Should not call reinviteUser
			expect(usersStore.reinviteUser).not.toHaveBeenCalled();
		});

		it('should handle disallow SSO manual login with missing settings', async () => {
			// Remove settings from user
			usersStore.usersList.state.items[1].settings = undefined;

			renderComponent();

			emitters.settingsUsersTable.emit('action', { action: 'disallowSSOManualLogin', userId: '2' });

			// Should not call updateOtherUserSettings
			expect(usersStore.updateOtherUserSettings).not.toHaveBeenCalled();
		});
	});

	describe('role updates', () => {
		it('should handle role update', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:role', { role: ROLE.Admin, userId: '2' });

			expect(usersStore.updateGlobalRole).toHaveBeenCalledWith({
				id: '2',
				newRoleName: ROLE.Admin,
			});
			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.any(String),
				});
			});
		});

		it('should refresh the users list after a successful role update', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:role', { role: ROLE.Admin, userId: '2' });

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalled();
			});
		});

		it('should handle role update error', async () => {
			usersStore.updateGlobalRole = vi.fn().mockRejectedValue(new Error('Failed to update role'));

			renderComponent();

			emitters.settingsUsersTable.emit('update:role', { role: ROLE.Admin, userId: '2' });
			await waitFor(() => {
				expect(mockToast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String));
			});
		});

		it('should handle role update for non-existent user', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:role', { role: ROLE.Admin, userId: 'non-existent' });

			await waitFor(() => {
				expect(mockToast.showError).toHaveBeenCalledWith(expect.any(Error), expect.any(String));
			});

			// Should not call updateGlobalRole
			expect(usersStore.updateGlobalRole).not.toHaveBeenCalled();
		});

		it('should use email as fallback when user has no first/last name', async () => {
			// Set user with no first/last name
			usersStore.usersList.state.items[1].firstName = '';
			usersStore.usersList.state.items[1].lastName = '';

			renderComponent();

			emitters.settingsUsersTable.emit('update:role', { role: ROLE.Admin, userId: '2' });

			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.stringContaining('member@example.com'),
				});
			});
		});
	});

	describe('table functionality', () => {
		it('should handle table options update', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 1,
				itemsPerPage: 20,
				sortBy: [{ id: 'role', desc: true }],
			});

			expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
				skip: 20,
				take: 20,
				sortBy: ['role:desc'],
				expand: ['projectRelations'],
				filter: {
					fullText: '',
				},
			});
		});

		it('should transform name sort to firstName, lastName, email', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 0,
				itemsPerPage: 10,
				sortBy: [{ id: 'name', desc: false }],
			});

			expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
				skip: 0,
				take: 10,
				sortBy: ['firstName:asc', 'lastName:asc', 'email:asc'],
				expand: ['projectRelations'],
				filter: {
					fullText: '',
				},
			});
		});

		it('should filter out invalid sort keys', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 0,
				itemsPerPage: 10,
				sortBy: [{ id: 'invalidKey', desc: false }],
			});

			expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
				skip: 0,
				take: 10,
				sortBy: [], // Invalid keys should be filtered out
				expand: ['projectRelations'],
				filter: {
					fullText: '',
				},
			});
		});

		it('should handle multiple mixed sort keys', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 0,
				itemsPerPage: 10,
				sortBy: [
					{ id: 'name', desc: false },
					{ id: 'email', desc: true },
					{ id: 'invalidKey', desc: false },
					{ id: 'role', desc: true },
				],
			});

			expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
				skip: 0,
				take: 10,
				sortBy: ['firstName:asc', 'lastName:asc', 'email:asc', 'email:desc', 'role:desc'],
				expand: ['projectRelations'],
				filter: {
					fullText: '',
				},
			});
		});

		it('should handle pagination correctly', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 3,
				itemsPerPage: 25,
				sortBy: [{ id: 'email', desc: false }],
			});

			expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
				skip: 75, // page 3 * 25 items per page
				take: 25,
				sortBy: ['email:asc'],
				expand: ['projectRelations'],
				filter: {
					fullText: '',
				},
			});
		});
	});

	describe('upgrade functionality', () => {
		it('should handle upgrade banner click', async () => {
			usersStore.usersLimitNotReached = false;

			renderComponent();

			const upgradeButton = screen.getByRole('button', { name: /view plans/i });
			await userEvent.click(upgradeButton);

			expect(mockPageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith(
				'settings-users',
				'upgrade-users',
			);
		});

		it('should handle advanced permissions upgrade click', async () => {
			settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AdvancedPermissions] =
				false;

			renderComponent();

			const upgradeLink = screen.getByTestId('upgrade-permissions-link');
			await userEvent.click(upgradeLink);

			expect(mockPageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith(
				'settings-users',
				'upgrade-advanced-permissions',
			);
		});
	});

	describe('edge cases', () => {
		it('should handle empty users list', () => {
			usersStore.usersList.state = {
				items: [],
				count: 0,
			};

			renderComponent();

			expect(screen.getByTestId('settings-users-table')).toBeInTheDocument();
		});

		it('should handle loading state', () => {
			usersStore.usersList.isLoading = true;

			const { getByTestId } = renderComponent();

			expect(getByTestId('settings-users-table')).toBeInTheDocument();
		});

		it('should handle user with missing email', async () => {
			usersStore.usersList.state.items[1].email = undefined;

			renderComponent();

			emitters.settingsUsersTable.emit('update:role', { role: ROLE.Admin, userId: '2' });

			await waitFor(() => {
				expect(mockToast.showToast).toHaveBeenCalledWith({
					type: 'success',
					title: expect.any(String),
					message: expect.stringContaining(''),
				});
			});
		});

		it('should handle SSO actions with user not found', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('action', {
				action: 'allowSSOManualLogin',
				userId: 'non-existent',
			});

			// Should not call updateOtherUserSettings
			expect(usersStore.updateOtherUserSettings).not.toHaveBeenCalled();
		});

		it('should handle copy password reset link with user not found', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('action', {
				action: 'copyPasswordResetLink',
				userId: 'non-existent',
			});

			// Should not call getUserPasswordResetLink
			expect(usersStore.getUserPasswordResetLink).not.toHaveBeenCalled();
		});

		it('should handle table data update with network error', async () => {
			usersStore.usersList.execute = vi.fn().mockRejectedValue(new Error('Network error'));

			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 0,
				itemsPerPage: 10,
				sortBy: [{ id: 'email', desc: false }],
			});

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalled();
			});
		});

		it('should handle sort by multiple fields with same direction', async () => {
			renderComponent();

			emitters.settingsUsersTable.emit('update:options', {
				page: 0,
				itemsPerPage: 10,
				sortBy: [
					{ id: 'email', desc: false },
					{ id: 'firstName', desc: false },
					{ id: 'lastName', desc: false },
				],
			});

			expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
				skip: 0,
				take: 10,
				sortBy: ['email:asc', 'firstName:asc', 'lastName:asc'],
				expand: ['projectRelations'],
				filter: {
					fullText: '',
				},
			});
		});

		it('should handle rapid consecutive search inputs', async () => {
			renderComponent();

			const searchInput = screen.getByTestId('users-list-search');

			// Type rapidly
			await userEvent.type(searchInput, 'a');
			await userEvent.type(searchInput, 'b');
			await userEvent.type(searchInput, 'c');

			await waitFor(() => {
				expect(usersStore.usersList.execute).toHaveBeenCalledTimes(2); // Once on mount, once on search because of debounce
				expect(usersStore.usersList.execute).toHaveBeenCalledWith(0, {
					skip: 0,
					take: 10,
					sortBy: ['firstName:asc', 'lastName:asc', 'email:asc'],
					expand: ['projectRelations'],
					filter: {
						fullText: 'abc',
					},
				});
			});
		});
	});
});
