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

// Full-stack integration scenarios. Unlike the per-page B1–B8 specs,
// these drive a complete user journey across multiple pages and then
// verify the data plane / API surface behaves the way the dashboard
// promised it would.

const API_BASE = process.env.E2E_API_BASE ?? 'http://127.0.0.1:18080';

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

async function fillCodeMirror(page: Page, locator: string, text: string): Promise<void> {
	const cm = page.locator(locator).first();
	await cm.click();
	await page.keyboard.press('ControlOrMeta+A');
	await page.keyboard.press('Delete');
	await page.keyboard.type(text);
}

test('end-to-end: app + domain + script + route via dashboard → invoke via public URL', async ({
	page,
	uniqueSlug
}) => {
	const slug = uniqueSlug('public');
	const domain = `${slug}.local`;
	const routePath = `/${slug}/hello`;
	const scriptName = `${slug}-hello`;
	const scriptSource = `return #{ statusCode: 200, body: #{ source: "public", slug: "${slug}" } };`;

	// 1. Create the app from the apps list.
	await page.goto('/admin/apps');
	await page.getByRole('button', { name: 'New app' }).click();
	await page.getByLabel('Name').fill(slug);
	const slugInput = page.getByLabel('Slug');
	await slugInput.fill('');
	await slugInput.fill(slug);
	await page.getByRole('button', { name: 'Create app' }).click();
	cleanup.app(slug);
	await expect(page.getByRole('link', { name: new RegExp(slug) })).toBeVisible();

	// 2. Open the app and claim the domain on the Domains tab.
	await page.getByRole('link', { name: new RegExp(slug) }).click();
	await expect(page).toHaveURL(new RegExp(`/admin/apps/${slug}$`));
	await page.getByRole('link', { name: 'Domains' }).click();
	const domainForm = page.locator('form.create-form.inline');
	await domainForm.getByPlaceholder(/app\.example\.com/).fill(domain);
	await domainForm.getByRole('button', { name: /^Add domain$/ }).click();
	await expect(page.locator('.domain-row')).toContainText(domain);

	// 3. Create the script on the Scripts tab.
	await page.getByRole('link', { name: 'Scripts' }).click();
	await page.getByRole('button', { name: /^New script$/ }).click();
	await page.getByLabel('Name').fill(scriptName);
	await fillCodeMirror(page, '.cm-content', scriptSource);
	await page.getByRole('button', { name: /^Create script$/ }).click();

	// 4. Open the script and bind a route on the Routing tab.
	await page.getByRole('link', { name: new RegExp(scriptName) }).click();
	await page.getByRole('button', { name: 'Routing' }).click();
	await page.getByRole('button', { name: '+ Add route' }).click();
	const routeForm = page.locator('form.route-form');
	await routeForm.getByLabel('Path', { exact: true }).fill(routePath);
	await routeForm.getByLabel('Method').selectOption('GET');
	await routeForm.getByLabel(/^Host/).fill(domain);
	await page.getByRole('button', { name: /^Create route$/ }).click();
	await expect(page.locator('.route-list')).toContainText(routePath);

	// 5. Invoke via the public URL, with the Host header pointing at
	//    the claimed domain. The dev backend listens on 127.0.0.1; the
	//    orchestrator resolves the app from Host, then the route.
	const publicCtx = await request.newContext({ baseURL: API_BASE });
	try {
		const res = await publicCtx.get(routePath, { headers: { host: domain } });
		expect(res.status()).toBe(200);
		const body = (await res.json()) as { source: string; slug: string };
		expect(body.source).toBe('public');
		expect(body.slug).toBe(slug);
	} finally {
		await publicCtx.dispose();
	}
});

test('api key minted via dashboard works as a CLI bearer, then revoke disables it', async ({
	page,
	uniqueUsername
}) => {
	// Worker-aware unique helper instead of Date.now() — keeps two
	// workers from minting the same name on the same millisecond.
	const name = uniqueUsername('cli');

	// 1. Mint the key from /profile and capture the revealed token.
	await page.goto('/admin/profile');
	await page.getByRole('button', { name: /\+ Mint API key/ }).click();
	const mintForm = page.locator('form.mint');
	await mintForm.getByPlaceholder('e.g. ci-deploy').fill(name);
	// script:read is enough to read the scripts list — that's our
	// "CLI verb" below.
	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();
	const rawToken = (await reveal.locator('code.token').textContent())?.trim();
	expect(rawToken).toBeTruthy();
	await reveal.getByRole('checkbox', { name: /saved this token/i }).check();
	await reveal.getByRole('button', { name: /^Done$/ }).click();

	// 2. Act like a CLI: call the API directly with Bearer <token>.
	const cli = await request.newContext({
		baseURL: API_BASE,
		extraHTTPHeaders: { authorization: `Bearer ${rawToken}` }
	});
	try {
		const ok = await cli.get('/api/v1/admin/scripts');
		expect(ok.status()).toBe(200);
		const body = (await ok.json()) as unknown;
		expect(Array.isArray(body)).toBe(true);

		// Sanity: a route the scope doesn't cover must reject.
		// `script:read` cannot list instance admins (that's
		// instance:admin territory).
		const denied = await cli.get('/api/v1/admin/admins');
		expect(denied.status()).toBe(403);

		// 3. Revoke via the dashboard.
		await page.reload();
		const revokeBtn = page.getByRole('button', { name: `Revoke ${name}` });
		await expect(revokeBtn).toBeVisible();
		await revokeBtn.click();
		await page.getByRole('dialog').getByRole('button', { name: /^Revoke$/ }).click();
		await expect(revokeBtn).toHaveCount(0);

		// 4. Same CLI call must now fail auth.
		const afterRevoke = await cli.get('/api/v1/admin/scripts');
		expect(afterRevoke.status()).toBe(401);
	} finally {
		await cli.dispose();
	}

	// Belt-and-braces cleanup: if the UI revoke missed, drop via API.
	const api = await adminApi();
	try {
		const list = await api.get('/api/v1/admin/api-keys');
		if (list.ok()) {
			const all = (await list.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();
	}
});
