import { test, expect, type Page } from './fixtures';

// Offline pass-and-play (hotseat) is offered only in offline mode, which is now implicit (the net-state
// machine detects it — no toggle). The e2e drives the machine through the mock's __net hook. Standalone
// display mode is still forced so the mock's offline dictionary path (served from /e2edict/) matches an
// installed PWA.
async function forceStandalone(page: Page): Promise<void> {
  await page.addInitScript(() => {
    const orig = window.matchMedia.bind(window);
    window.matchMedia = (q: string) =>
      (q.includes('display-mode: standalone')
        ? { matches: true, media: q, onchange: null, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, dispatchEvent: () => false }
        : orig(q)) as MediaQueryList;
  });
}

async function enterLobby(page: Page): Promise<void> {
  await page.getByRole('button', { name: /guest|гост/i }).first().click();
  await expect(page.locator('button.tab').nth(2)).toBeVisible();
}

async function goOffline(page: Page): Promise<void> {
  // The machine auto-detects offline (no toggle, no dialog); the lobby stays mounted and turns blue.
  await page.evaluate(() => (window as unknown as { __net: { offline(): void } }).__net.offline());
  await expect(page.locator('header.nav.offline')).toBeVisible({ timeout: 15000 });
}

// typePin clicks the on-screen keypad digits (the pad has no OK button — the 4th digit is the action).
// It first waits for the dots to be empty, so a call made right after a previous entry does not lose
// digits during the 250 ms verdict pause (the 4th digit → pause → clear/advance).
async function typePin(page: Page, digits: string): Promise<void> {
  await expect(page.locator('.pad .dot.on')).toHaveCount(0);
  for (const d of digits) await page.locator('.pad .key', { hasText: d }).click();
}

test.describe('offline hotseat (pass-and-play)', () => {
  test('create with a locked seat, unlock, host-skip, terminate from the lobby', async ({ page }) => {
    await forceStandalone(page);
    await page.goto('/');
    await enterLobby(page);
    await goOffline(page);

    // A known seed: the bag deals a full rack deterministically, AND the seeded seating shuffle keeps
    // the roster order (Ann first) so the locked-seat assertions below are stable. (Seat order is
    // randomised at start; the shuffle is seed-driven — seed '1' would seat Bob first, '2' Ann first.)
    await page.evaluate(() => (window as unknown as { __mock: { setLocalSeed(s: string): void } }).__mock.setLocalSeed('2'));

    // New game -> "with friends" offers an online (invite) / offline (hotseat) segment. Offline, the
    // online segment is disabled and the offline (pass-and-play) one is forced, so the hotseat form shows.
    await page.locator('button.tab').nth(0).click();
    await page.getByRole('button', { name: /With friends|друзьями/i }).click();
    await expect(page.getByRole('button', { name: /Invite a friend|Пригласить друга/i })).toBeDisabled();
    await expect(page.locator('.hostpin')).toBeVisible();

    // The player rows are disabled until the mandatory host (master) PIN is set.
    await expect(page.locator('.pname').first()).toBeDisabled();

    // Set the host PIN (enter + confirm), then decline taking a seat.
    await page.locator('.hostpin .plink').click();
    await typePin(page, '9999');
    await typePin(page, '9999');
    await page.getByRole('button', { name: /^(No|Нет)$/ }).click();

    // Pick the English variant (the plaques mirror Quick Match; "Scrabble" is the Latin English name).
    await page.locator('.variant', { hasText: 'Scrabble' }).click();

    // Two players: Ann (PIN-locked) and Bob (open).
    await page.locator('.pname').nth(0).fill('Ann');
    await page.locator('.pname').nth(1).fill('Bob');

    // Row-delete: a 3rd player's kebab asks the host PIN, which ARMS a ❌ (not a silent delete);
    // tapping the ❌ removes the row.
    await page.getByRole('button', { name: /Add player|Добавить/i }).click();
    await expect(page.locator('.prow')).toHaveCount(3);
    await page.locator('.prow').nth(2).locator('.pkebab').click();
    await typePin(page, '9999');
    const rowDel = page.locator('.prow').nth(2).locator('.prow-del');
    await expect(rowDel).toBeVisible();
    await rowDel.click();
    await expect(page.locator('.prow')).toHaveCount(2);

    // Lock Ann's seat with a PIN.
    await page.locator('.prow').nth(0).locator('.plink').click();
    await typePin(page, '1234');
    await typePin(page, '1234');

    await page.locator('button.invite').click();
    await expect(page.locator('[data-cell]').first()).toBeVisible();

    // Ann is to move and PIN-locked: the board is visible but her rack is withheld behind Unlock.
    await expect(page.locator('.unlock')).toBeVisible();
    await expect(page.locator('.rack .tile')).toHaveCount(0);

    // A wrong PIN keeps it locked; the right PIN reveals the full rack.
    await page.locator('.unlock').click();
    await typePin(page, '0000');
    await expect(page.locator('.pad')).toBeVisible(); // still open after a wrong PIN
    await typePin(page, '1234');
    await expect(page.locator('.rack .tile')).toHaveCount(7);

    // Opening the history reveals NO social controls on the seat plaques (a local game is
    // account-less), and the Dictionary entry (the comms button) is kept for the active game.
    await page.locator('.scoreboard').click();
    await expect(page.locator('.fico')).toHaveCount(0);
    await expect(page.locator('.chat-ico')).toBeVisible();
    await page.locator('.scoreboard').click();

    // Host override: 🔐 -> master PIN -> skip the current turn -> confirm. The turn passes to Bob,
    // whose seat is open, so his rack shows without a lock.
    await page.locator('button.tab', { hasText: /Host|Ведущий/ }).click();
    await typePin(page, '9999');
    await page.getByRole('button', { name: /Skip|Пропустить/ }).click();
    await page.getByRole('button', { name: /^(OK|ОК)$/ }).click();
    await expect(page.locator('.unlock')).toHaveCount(0);
    await expect(page.locator('.rack .tile')).toHaveCount(7);

    // Back in the lobby the active hotseat game has a kebab; terminating it needs the master PIN.
    await page.getByRole('button', { name: /Back|Назад/i }).click();
    await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible();
    await expect(page.locator('.rowwrap:not(.greyed)')).toHaveCount(1);
    await page.locator('.kebab').first().click();
    const del = page.locator('.rowwrap.revealed .del');
    await expect(del).toBeVisible();
    await del.click();
    await typePin(page, '9999');
    await expect(page.locator('.rowwrap:not(.greyed)')).toHaveCount(0);
  });
});
