/**
 * SV-series — Section visibility / feature_flags (Fase 12).
 *
 *   SV-01 Ana hides "Tasks" in /settings → sidebar + bottom tab bar stop
 *         showing it, and visiting /tasks redirects to /lists.
 *   SV-02 Ana (admin) hides "Notes" in /collective/manage → Borja (member of
 *         the same collective) sees the entry disappear without a reload
 *         (realtime).
 *   SV-03 Ana sets a collective-level Tasks override ON in a fresh extra
 *         collective; Borja (already opted out of Tasks at the user layer)
 *         sees Tasks reappear in that collective but stays hidden in others.
 *         Verifies precedence collective > user resolves per-collective.
 *
 * Each test resets the seed user/collective JSON columns afterwards so the
 * suite is idempotent. Reset uses the live PostgREST endpoint via the JWT
 * already stashed in localStorage by `loginAs`.
 */
import { test, expect, type Page } from '@playwright/test';
import { USERS, COLLECTIVE_NAME } from '../fixtures/users.js';
import { loginAs } from '../fixtures/login.js';

const SEED_COLLECTIVE_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';

/**
 * Patch a row via the Supabase client already loaded in the page (exposed
 * as `window.__sb` in dev — see `+layout.svelte`). Returns the HTTP-ish
 * status (200 on success, 0 if the client is not ready yet).
 */
async function patchRow(
	page: Page,
	table: 'users' | 'collectives',
	id: string,
	body: Record<string, unknown>
): Promise<number> {
	// Wait for the dev hook to land — onMount fires after the first paint, so
	// in fast tests we can race the eval. 5s timeout is plenty.
	await page.waitForFunction(
		() => Boolean((window as unknown as { __sb?: unknown }).__sb),
		null,
		{ timeout: 5_000 }
	);
	return await page.evaluate(
		async ({ table, id, body }) => {
			const supabase = (
				window as unknown as {
					__sb: import('@supabase/supabase-js').SupabaseClient;
				}
			).__sb;
			const { error } = await supabase.from(table).update(body).eq('id', id);
			return error ? 500 : 200;
		},
		{ table, id, body }
	);
}

/**
 * Tab bar entries live inside `[data-testid="bottom-tab-bar"]`. We assert on
 * the *count* of the matching `[data-section="X"]` so the test does not depend
 * on the visible-vs-hidden CSS state (the entry is removed from the DOM when
 * hidden).
 */
async function tabBarHasSection(page: Page, section: string): Promise<number> {
	return await page
		.getByTestId('bottom-tab-bar')
		.locator(`[data-section="${section}"]`)
		.count();
}

async function resetAllFlags(browser: import('@playwright/test').Browser): Promise<void> {
	// Reset the seed rows via Ana (the only user authoritative for both her own
	// row AND the seed collective row). Patching another user's row from Ana
	// would be blocked by RLS, so we open a second context for Borja.
	const anaCtx = await browser.newContext();
	const anaPage = await anaCtx.newPage();
	try {
		await loginAs(anaPage, USERS.ana);
		await patchRow(anaPage, 'users', USERS.ana.id, { feature_flags: {} });
		await patchRow(anaPage, 'collectives', SEED_COLLECTIVE_ID, { feature_flags: {} });
	} catch {
		/* don't mask original failure */
	} finally {
		await anaCtx.close();
	}

	const borjaCtx = await browser.newContext();
	const borjaPage = await borjaCtx.newPage();
	try {
		await loginAs(borjaPage, USERS.borja);
		await patchRow(borjaPage, 'users', USERS.borja.id, { feature_flags: {} });
	} catch {
		/* don't mask original failure */
	} finally {
		await borjaCtx.close();
	}
}

test.describe('Section visibility', () => {
	// Reset BOTH before and after every test. The before-each guards against
	// state leakage from prior suites (the broader `just test-all` run shares
	// the same Docker database with whatever tasks/lists tests ran before).
	// The after-each keeps subsequent suites clean.
	test.beforeEach(async ({ browser }) => {
		await resetAllFlags(browser);
	});

	test.afterEach(async ({ browser }) => {
		await resetAllFlags(browser);
	});

	test('SV-01: Ana hides Tasks in /settings — sidebar + tab bar drop it, /tasks redirects to /lists', async ({
		page
	}) => {
		await loginAs(page, USERS.ana);
		await page.goto('/settings');

		const toggle = page.getByTestId('settings-section-toggle-tasks');
		await expect(toggle).toBeVisible({ timeout: 10_000 });
		await expect(toggle).toBeChecked();
		await toggle.click();
		await expect(toggle).not.toBeChecked();

		const sidebar = page.getByTestId('desktop-sidebar');
		await expect(sidebar.locator('[data-section="tasks"]')).toHaveCount(0, {
			timeout: 5_000
		});
		const tabBar = page.getByTestId('bottom-tab-bar');
		await expect(tabBar.locator('[data-section="tasks"]')).toHaveCount(0);
		await expect(tabBar).toHaveAttribute('data-section-count', '3');

		await page.goto('/tasks');
		await page.waitForURL(/\/lists/, { timeout: 10_000 });
		await expect(page.getByTestId('section-disabled-toast')).toBeVisible({
			timeout: 5_000
		});
	});

	test('SV-02: Ana hides Notes for the collective — Borja sees it disappear in realtime', async ({
		browser
	}) => {
		const anaCtx = await browser.newContext();
		const ana = await anaCtx.newPage();
		const borjaCtx = await browser.newContext();
		const borja = await borjaCtx.newPage();

		try {
			await loginAs(ana, USERS.ana);
			// Make sure both flag rows are clean before Borja loads.
			await patchRow(ana, 'collectives', SEED_COLLECTIVE_ID, { feature_flags: {} });
			await patchRow(ana, 'users', USERS.ana.id, { feature_flags: {} });

			await loginAs(borja, USERS.borja);
			await patchRow(borja, 'users', USERS.borja.id, { feature_flags: {} });
			await borja.goto('/lists');
			// BottomTabBar is `md:hidden` so on Desktop Chrome it lives in the
			// DOM but is CSS-hidden — assert via locator count instead of
			// toBeVisible (which would otherwise fail on the visibility check).
			await expect(borja.getByTestId('bottom-tab-bar')).toHaveCount(1, { timeout: 10_000 });
			expect(await tabBarHasSection(borja, 'notes')).toBe(1);

			// Ana flips the collective-level Notes toggle off.
			await ana.goto('/collective/manage');
			const adminToggle = ana.getByTestId('manage-section-toggle-notes');
			await expect(adminToggle).toBeVisible({ timeout: 10_000 });
			await expect(adminToggle).toBeChecked();
			await adminToggle.click();
			await expect(adminToggle).not.toBeChecked();

			// Borja's nav loses Notes without a reload — realtime carried the change.
			await expect
				.poll(() => tabBarHasSection(borja, 'notes'), { timeout: 15_000 })
				.toBe(0);
		} finally {
			await anaCtx.close();
			await borjaCtx.close();
		}
	});

	test('SV-03: collective ON override wins over user OFF — Tasks reappears per-collective', async ({
		page,
		browser
	}) => {
		// Borja opts out of Tasks at the user layer (in the seed collective).
		await loginAs(page, USERS.borja);
		await patchRow(page, 'users', USERS.borja.id, { feature_flags: { tasks: false } });
		// Force a re-fetch by reloading — the page first loaded with the empty
		// row, the realtime subscription will also fire, but reload is the most
		// deterministic way to start from a known state.
		await page.reload();
		await page.waitForLoadState('networkidle');
		await expect(page.getByTestId('bottom-tab-bar')).toHaveCount(1, { timeout: 10_000 });
		expect(await tabBarHasSection(page, 'tasks')).toBe(0);

		// Ana (admin) explicitly sets Tasks ON at the collective level. Since
		// the collective layer outranks the user layer, Tasks should reappear
		// for Borja in this collective without him touching his user toggle.
		const adminCtx = await browser.newContext();
		const adminPage = await adminCtx.newPage();
		try {
			await loginAs(adminPage, USERS.ana);
			const status = await patchRow(adminPage, 'collectives', SEED_COLLECTIVE_ID, {
				feature_flags: { tasks: true }
			});
			expect(status).toBeGreaterThanOrEqual(200);
			expect(status).toBeLessThan(300);
		} finally {
			await adminCtx.close();
		}

		// Borja's BottomTabBar should pick the change up via realtime within
		// the standard 15s window (matches the Realtime gotcha note).
		await expect
			.poll(() => tabBarHasSection(page, 'tasks'), { timeout: 15_000 })
			.toBe(1);

		// Sanity reference for the COLLECTIVE_NAME import — keeps the
		// fixture link explicit in case the helper file moves.
		expect(COLLECTIVE_NAME.length).toBeGreaterThan(0);
	});
});
