import { expect, type Page } from '@playwright/test';
import { test } from '../fixtures/ids';
import { CleanupRegistry } from '../fixtures/cleanup';
import { adminApi } from '../fixtures/api';

// Phase B7 — Profile + API Keys (/admin/profile). Covers the
// mint/reveal/revoke flow, the app-binding mutual-exclusion guard,
// and adversarial inputs.

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 openMintForm(page: Page): Promise<void> {
	await page.goto('/admin/profile');
	await page.getByRole('button', { name: /\+ Mint API key/ }).click();
}

async function registerKeyCleanupByName(name: string): Promise<void> {
	const api = await adminApi();
	try {
		const res = await api.get('/api/v1/admin/api-keys');
		const all = (await res.json()) as Array<{ id: string; name: string }>;
		const k = all.find((x) => x.name === name);
		if (k) cleanup.apiKey(k.id);
	} finally {
		await api.dispose();
	}
}

test.describe('B7 profile + API keys', () => {
	test('mint instance-wide key: reveal → ack → key appears in list', async ({ page }) => {
		const name = `e2e-mint-${Date.now()}`;
		await openMintForm(page);
		await page.locator('form.mint').getByPlaceholder('e.g. ci-deploy').fill(name);
		// Pick a non-instance scope so we don't need to worry about
		// mutual exclusion here. The scope-chip is a <label> wrapping
		// the checkbox — clicking the label toggles it.
		await page.locator('label.scope-chip', { hasText: 'script:read' }).click();
		await page.getByRole('button', { name: /^Mint key$/ }).click();

		const reveal = page.locator('.reveal');
		await expect(reveal).toBeVisible();
		await expect(reveal.locator('code.token')).toContainText(/\S{16,}/);
		await expect(reveal.getByRole('button', { name: /^Done$/ })).toBeDisabled();
		await reveal.getByRole('checkbox', { name: /saved this token/i }).check();
		await reveal.getByRole('button', { name: /^Done$/ }).click();

		await registerKeyCleanupByName(name);
		await expect(page.getByText(name)).toBeVisible();
	});

	test('binding to an app disables instance scopes', async ({ page, uniqueSlug }) => {
		const slug = uniqueSlug('keyapp');
		const appId = await createApp(slug);
		cleanup.app(slug);

		await openMintForm(page);

		// Default binding is Instance-wide — instance scopes are
		// enabled.
		const instChip = page.locator('label.scope-chip', { hasText: 'instance:admin' });
		await expect(instChip).not.toHaveClass(/disabled/);

		// Switch binding to the app. The chip becomes disabled.
		await page.getByLabel(/Binding/i).selectOption(appId);
		await expect(instChip).toHaveClass(/disabled/);
	});

	test('revoke key removes it from the list', async ({ page }) => {
		const name = `e2e-revoke-${Date.now()}`;
		// Seed a key via API so the test focuses on the revoke UI.
		const api = await adminApi();
		try {
			const res = await api.post('/api/v1/admin/api-keys', {
				data: { name, scopes: ['script:read'] }
			});
			expect(res.ok()).toBe(true);
			const body = (await res.json()) as { id: string };
			cleanup.apiKey(body.id);
		} finally {
			await api.dispose();
		}

		await page.goto('/admin/profile');
		const revokeBtn = page.getByRole('button', { name: `Revoke ${name}` });
		await expect(revokeBtn).toBeVisible();
		await revokeBtn.click();

		const dialog = page.getByRole('dialog');
		await dialog.getByRole('button', { name: /^Revoke$/ }).click();
		// Assert the row's revoke button is gone (the flash banner
		// also mentions the name, so a plain getByText would still
		// match — anchor on the row-scoped button instead).
		await expect(revokeBtn).toHaveCount(0);
	});

	test('denied=users banner shows when arriving from the users redirect', async ({ page }) => {
		await page.goto('/admin/profile?denied=users');
		await expect(page.getByText(/don.?t have access to the Users page/i)).toBeVisible();
	});
});

test.describe('B7 profile adversarial', () => {
	test('empty name keeps the mint button disabled', async ({ page }) => {
		await openMintForm(page);
		// Trying to click would HTML5-validate; instead verify the
		// button is disabled while name is empty.
		await page.locator('label.scope-chip', { hasText: 'script:read' }).click();
		await expect(page.getByRole('button', { name: /^Mint key$/ })).toBeDisabled();
	});

	test('copy-token button copies the full token, not a truncated form', async ({
		page,
		context
	}) => {
		// Permission must be granted explicitly; chromium will throw
		// otherwise when calling navigator.clipboard.readText().
		await context.grantPermissions(['clipboard-read', 'clipboard-write']);

		const name = `e2e-copy-${Date.now()}`;
		await openMintForm(page);
		await page.locator('form.mint').getByPlaceholder('e.g. ci-deploy').fill(name);
		await page.locator('label.scope-chip', { hasText: 'script:read' }).click();
		await page.getByRole('button', { name: /^Mint key$/ }).click();

		const reveal = page.locator('.reveal');
		const tokenInDom = await reveal.locator('code.token').textContent();
		expect(tokenInDom).toBeTruthy();
		await reveal.getByRole('button', { name: /^Copy$/ }).click();
		const copied = await page.evaluate(() => navigator.clipboard.readText());
		expect(copied).toBe(tokenInDom);

		await reveal.getByRole('checkbox', { name: /saved this token/i }).check();
		await reveal.getByRole('button', { name: /^Done$/ }).click();
		await registerKeyCleanupByName(name);
	});
});
