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

// Phase B4 — Routing tab in the script editor. Add / remove / match
// preview + validation paths (host check, path-kind mismatch, reserved
// prefix, duplicate conflict, adversarial paths).

const HELLO_RHAI = `return #{ statusCode: 200, body: #{ ok: true } };`;

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

async function makeAppWithScript(slug: string): Promise<{ appId: string; scriptId: string }> {
	const api = await adminApi();
	try {
		const appRes = await api.post('/api/v1/admin/apps', {
			data: { slug, name: slug }
		});
		expect(appRes.ok()).toBe(true);
		const appBody = (await appRes.json()) as { id: string };

		const scriptRes = await api.post('/api/v1/admin/scripts', {
			data: { app_id: appBody.id, name: 'route-target', source: HELLO_RHAI }
		});
		expect(scriptRes.ok()).toBe(true);
		const scriptBody = (await scriptRes.json()) as { id: string };

		return { appId: appBody.id, scriptId: scriptBody.id };
	} finally {
		await api.dispose();
	}
}

async function gotoRoutingTab(page: Page, scriptId: string): Promise<void> {
	await page.goto(`/admin/scripts/${scriptId}`);
	await page.getByRole('button', { name: 'Routing' }).click();
}

async function addRoute(
	page: Page,
	opts: { path: string; pathKind?: 'exact' | 'param' | 'prefix'; method?: string; host?: string }
): Promise<void> {
	await page.getByRole('button', { name: '+ Add route' }).click();
	const form = page.locator('form.route-form');
	await form.getByLabel('Path', { exact: true }).fill(opts.path);
	if (opts.pathKind) {
		await form.getByLabel('Path kind').selectOption(opts.pathKind);
	}
	if (opts.method !== undefined) {
		await form.getByLabel('Method').selectOption(opts.method);
	}
	if (opts.host !== undefined) {
		await form.getByLabel(/^Host/).fill(opts.host);
	}
}

test.describe('B4 routing', () => {
	test('add route appears in list and matches in the preview', async ({ page, uniqueSlug }) => {
		const slug = uniqueSlug('addr');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await addRoute(page, { path: '/greet', method: 'GET' });
		await page.getByRole('button', { name: /^Create route$/ }).click();

		await expect(page.locator('.route-list')).toContainText('/greet');

		// Match preview confirms the route resolves.
		await page.getByLabel('URL').fill('http://localhost/greet');
		await page.locator('.actions').getByRole('button', { name: 'Match' }).click();
		await expect(page.locator('pre.preview')).toContainText('script_id');
	});

	test('remove route updates the list', async ({ page, uniqueSlug }) => {
		const slug = uniqueSlug('remr');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await addRoute(page, { path: '/transient', method: 'GET' });
		await page.getByRole('button', { name: /^Create route$/ }).click();
		await expect(page.locator('.route-list')).toContainText('/transient');

		// removeRoute() uses window.confirm — accept it.
		page.once('dialog', (d) => void d.accept());
		await page.locator('.route-list').getByRole('button', { name: 'remove' }).click();
		await expect(page.locator('.route-list')).toHaveCount(0);
		await expect(page.getByText(/no routes yet/i)).toBeVisible();
	});

	test('duplicate route surfaces a 409 conflict error inline', async ({ page, uniqueSlug }) => {
		const slug = uniqueSlug('dupr');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await addRoute(page, { path: '/twice', method: 'GET' });
		await page.getByRole('button', { name: /^Create route$/ }).click();
		await expect(page.locator('.route-list')).toContainText('/twice');

		// Same path + method again — must conflict.
		await addRoute(page, { path: '/twice', method: 'GET' });
		await page.getByRole('button', { name: /^Create route$/ }).click();
		await expect(page.locator('.route-form .error.inline')).toBeVisible();
	});

	test('path-kind mismatch warns inline when /:name is set to exact', async ({
		page,
		uniqueSlug
	}) => {
		const slug = uniqueSlug('mism');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await page.getByRole('button', { name: '+ Add route' }).click();
		await page.getByLabel('Path', { exact: true }).fill('/users/:id');
		// Override to a wrong kind — auto-detect would have picked
		// `param`; selecting `exact` should fire the warning.
		await page.getByLabel('Path kind').selectOption('exact');
		await expect(page.locator('.route-form .warning.inline')).toBeVisible();
	});

	test('host validation warns when the host is not a claimed domain', async ({
		page,
		uniqueSlug
	}) => {
		const slug = uniqueSlug('unclaim');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await page.getByRole('button', { name: '+ Add route' }).click();
		await page.getByLabel('Path', { exact: true }).fill('/x');
		await page.getByLabel(/^Host/).fill('example.test-not-claimed.local');
		// One of the inline warnings is the unclaimed-host explainer.
		await expect(page.locator('.route-form .warning.inline').first()).toBeVisible();
	});
});

test.describe('B4 routing adversarial', () => {
	test('reserved prefix /api/ is rejected with a visible error', async ({ page, uniqueSlug }) => {
		const slug = uniqueSlug('reserv');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await addRoute(page, { path: '/api/v9/oops', method: 'GET' });
		await page.getByRole('button', { name: /^Create route$/ }).click();
		await expect(page.locator('.route-form .error.inline')).toBeVisible();
		await expect(page.locator('.route-form .error.inline')).toContainText(
			/reserved|api|prefix/i
		);
		// Empty-state copy renders when no routes exist; the path
		// itself must not appear anywhere on the routing tab.
		await expect(page.getByText(/no routes yet/i)).toBeVisible();
	});

	test('xss payload in path stored or rejected — never executes on render', async ({
		page,
		uniqueSlug
	}) => {
		page.on('dialog', async (d) => {
			await d.dismiss();
			throw new Error(`Unexpected dialog: ${d.message()}`);
		});
		const slug = uniqueSlug('pxss');
		const { scriptId } = await makeAppWithScript(slug);
		cleanup.app(slug);

		await gotoRoutingTab(page, scriptId);
		await addRoute(page, {
			path: '/<script>alert(1)</script>',
			method: 'GET'
		});
		await page.getByRole('button', { name: /^Create route$/ }).click();

		// Either accepted (rendered as text in the list) or rejected
		// (error inline). Both fine — what's NOT fine is an alert
		// dialog or an injected <script> tag in the list.
		const xssScripts = await page.locator('.route-list script:has-text("alert")').count();
		expect(xssScripts).toBe(0);
	});
});
