import { expect } from '@playwright/test';
import { test } from '../fixtures/ids';
import { CleanupRegistry } from '../fixtures/cleanup';
import { adminApi } from '../fixtures/api';
import { loginAsUserToken, pageWithUserToken } from '../fixtures/role-page';

// Phase B5 — App Members. Setup creates one or two extra admin
// users via the API; tests drive the Members tab through the
// dashboard like a real app admin would.

const cleanup = new CleanupRegistry();
test.afterEach(async () => {
	await cleanup.run();
});

async function createApp(slug: string): Promise<string> {
	const api = await adminApi();
	try {
		const res = await api.post('/api/v1/admin/apps', { data: { slug, name: slug } });
		expect(res.ok()).toBe(true);
		return ((await res.json()) as { id: string }).id;
	} finally {
		await api.dispose();
	}
}

async function createMemberUser(username: string): Promise<string> {
	const api = await adminApi();
	try {
		const res = await api.post('/api/v1/admin/admins', {
			data: { username, password: 'e2e-member-pw', instance_role: 'member' }
		});
		expect(res.ok()).toBe(true);
		return ((await res.json()) as { id: string }).id;
	} finally {
		await api.dispose();
	}
}

test.describe('B5 app members', () => {
	test('invite a member-role user, then remove them', async ({ page, uniqueSlug, uniqueUsername }) => {
		const slug = uniqueSlug('mem');
		const username = uniqueUsername('inv');
		await createApp(slug);
		const userId = await createMemberUser(username);
		cleanup.app(slug);
		cleanup.adminUser(userId);

		await page.goto(`/admin/apps/${slug}`);
		await page.getByRole('link', { name: 'Members' }).click();

		// Invite. Both selects sit in `form.create-form`; locate them
		// by position to avoid getByLabel ambiguity (the Svelte
		// markup nests both labels in a flex row, which makes their
		// accessible names overlap).
		const form = page.locator('form.create-form');
		await form.locator('select').nth(0).selectOption({ label: username });
		await form.locator('select').nth(1).selectOption('editor');
		await page.getByRole('button', { name: /^Add member$/ }).click();
		await expect(page.locator('.member-row')).toContainText(username);

		// Remove via action menu + confirm modal.
		await page.getByRole('button', { name: new RegExp(`Member actions for ${username}`) }).click();
		await page.getByRole('menuitem', { name: /^Remove from app$/ }).click();
		const dialog = page.getByRole('dialog');
		await expect(dialog).toBeVisible();
		await dialog.getByRole('button', { name: /^Remove member$/ }).click();
		await expect(page.locator('.member-row')).toHaveCount(0);
	});

	test('role change via action menu updates the role chip', async ({
		page,
		uniqueSlug,
		uniqueUsername
	}) => {
		const slug = uniqueSlug('mem');
		const username = uniqueUsername('role');
		await createApp(slug);
		const userId = await createMemberUser(username);
		cleanup.app(slug);
		cleanup.adminUser(userId);

		// Seed the membership via API to skip the invite UI.
		const api = await adminApi();
		try {
			const res = await api.post(`/api/v1/admin/apps/${slug}/members`, {
				data: { user_id: userId, role: 'viewer' }
			});
			expect(res.ok()).toBe(true);
		} finally {
			await api.dispose();
		}

		await page.goto(`/admin/apps/${slug}`);
		await page.getByRole('link', { name: 'Members' }).click();
		await page.getByRole('button', { name: new RegExp(`Member actions for ${username}`) }).click();
		await page.getByRole('menuitem', { name: /^Make editor$/ }).click();

		const row = page.locator('.member-row', { hasText: username });
		await expect(row).toContainText(/editor/i);
	});

	test('non-app-admin viewers do not see the Members tab', async ({
		browser,
		uniqueSlug,
		uniqueUsername
	}) => {
		const slug = uniqueSlug('mem');
		const username = uniqueUsername('viewer');
		const password = 'e2e-member-pw';
		await createApp(slug);
		const userId = await createMemberUser(username);
		cleanup.app(slug);
		cleanup.adminUser(userId);

		// Grant viewer membership (not app_admin) so the user can see
		// the app at all.
		const api = await adminApi();
		try {
			const res = await api.post(`/api/v1/admin/apps/${slug}/members`, {
				data: { user_id: userId, role: 'viewer' }
			});
			expect(res.ok()).toBe(true);
		} finally {
			await api.dispose();
		}

		const token = await loginAsUserToken(username, password);
		const viewerPage = await pageWithUserToken(browser, token);
		try {
			await viewerPage.goto(`/admin/apps/${slug}`);
			// Scripts tab loads — that's what a viewer sees.
			await expect(
				viewerPage.getByRole('link', { name: 'Scripts' })
			).toBeVisible();
			// Members tab button is absent for non-app-admins.
			await expect(
				viewerPage.getByRole('link', { name: 'Members' })
			).toHaveCount(0);
		} finally {
			await viewerPage.context().close();
		}
	});
});

test.describe('B5 app members adversarial', () => {
	test('role dropdown exposes only the documented values', async ({
		page,
		uniqueSlug,
		uniqueUsername
	}) => {
		const slug = uniqueSlug('mem');
		const username = uniqueUsername('rolelist');
		await createApp(slug);
		const userId = await createMemberUser(username);
		cleanup.app(slug);
		cleanup.adminUser(userId);

		await page.goto(`/admin/apps/${slug}`);
		await page.getByRole('link', { name: 'Members' }).click();
		const form = page.locator('form.create-form');
		const roleSelect = form.locator('select').nth(1);
		const optionValues = await roleSelect.evaluate((el: HTMLSelectElement) =>
			Array.from(el.options).map((o) => o.value)
		);
		expect(optionValues.sort()).toEqual(['app_admin', 'editor', 'viewer']);
	});
});
