import { devices, expect, test, type Browser, type Page } from '@playwright/test';
import { createGameFixture, type GameFixture } from './fixtures';

/**
 * The whole product loop through the real UI: a host signs in and runs a championship, two guests
 * join from phones and play, and the bar TV shows it. Needs the game server started with
 * GAME_TIMINGS_SCALE (e.g. 0.25) so a round takes about a minute.
 */
test.describe.configure({ mode: 'serial' });

let fx: GameFixture;
test.beforeAll(async () => {
  fx = await createGameFixture();
});
test.afterAll(async () => {
  await fx?.cleanup();
});

function trackErrors(page: Page, label: string, sink: string[]) {
  page.on('pageerror', (e) => sink.push(`${label}: ${e.message}`));
  page.on('console', (m) => {
    // Socket reconnect noise is expected while servers restart; anything else is a bug.
    if (m.type() === 'error' && !/WebSocket|ERR_CONNECTION|Failed to load resource/.test(m.text())) sink.push(`${label}: ${m.text()}`);
  });
}

async function phone(browser: Browser, nickname: string, errors: string[]) {
  const ctx = await browser.newContext({ ...devices['Pixel 7'] });
  const page = await ctx.newPage();
  trackErrors(page, nickname, errors);
  await page.goto(`/join/${fx.joinCode}`);
  await page.getByLabel('Your nickname').fill(nickname);
  await page.getByRole('button', { name: /join the game/i }).click();
  await expect(page).toHaveURL(/\/play\//);
  return page;
}

const screen = (page: Page) => page.locator('[data-screen]').first();

/** Keeps answering whatever question is open with the given letter until stopped. */
function autoAnswer(page: Page, letter: 'A' | 'B' | 'C' | 'D') {
  let stopped = false;
  let answered = 0;
  const loop = (async () => {
    while (!stopped) {
      try {
        if ((await screen(page).getAttribute('data-screen')) === 'open') {
          const btn = page.getByRole('button', { name: new RegExp(`^${letter}: `) });
          if (await btn.isEnabled({ timeout: 200 })) {
            await btn.click({ timeout: 1_000 });
            answered++;
          }
        }
      } catch {
        // the screen moved on mid-click; try again on the next pass
      }
      await page.waitForTimeout(250);
    }
  })();
  return {
    get answered() {
      return answered;
    },
    async stop() {
      stopped = true;
      await loop;
    },
  };
}

test('a host runs a championship that phones play and the TV shows', async ({ browser, baseURL }) => {
  test.setTimeout(240_000);
  const errors: string[] = [];

  // Host signs in and opens the lobby.
  const hostCtx = await browser.newContext({ viewport: { width: 1280, height: 800 }, baseURL });
  const host = await hostCtx.newPage();
  trackErrors(host, 'host', errors);
  await host.goto('/host/login');
  await host.getByLabel('Email').fill(fx.hostEmail);
  await host.getByLabel('Password').fill(fx.hostPassword);
  await host.getByRole('button', { name: 'Sign in' }).click();
  await expect(host).toHaveURL(/\/host$/);
  await host.goto(`/host/events/${fx.eventId}`);
  await host.getByRole('button', { name: 'Open lobby' }).click();

  // The bar TV shows the lobby with a join QR.
  const tvCtx = await browser.newContext({ viewport: { width: 1280, height: 720 }, baseURL });
  const tv = await tvCtx.newPage();
  trackErrors(tv, 'tv', errors);
  await tv.goto(fx.tvPath);
  await expect(tv.getByText(fx.joinCode).first()).toBeVisible();

  // Two guests join from their phones and land in the lobby.
  const ava = await phone(browser, 'Ava E2E', errors);
  const ben = await phone(browser, 'Ben E2E', errors);
  await expect(screen(ava)).toHaveAttribute('data-screen', 'lobby');
  await expect(screen(ben)).toHaveAttribute('data-screen', 'lobby');
  await expect(tv.getByText('Ava E2E').first()).toBeVisible();

  // Start the show; both phones follow the round into the first question.
  await host.getByRole('button', { name: 'Start show' }).click();
  await expect(screen(ava)).toHaveAttribute('data-screen', /intro|pending|open/, { timeout: 20_000 });
  const a = autoAnswer(ava, 'A');
  const b = autoAnswer(ben, 'B');

  // The first reveal: phones show their result, the TV marks the correct answer.
  await expect(screen(ava)).toHaveAttribute('data-screen', 'revealed', { timeout: 30_000 });
  await expect(tv.getByText(/correct/i).first()).toBeVisible();

  // Pause and resume from the host panel.
  await host.getByRole('button', { name: 'Pause' }).click();
  await expect(screen(ava)).toHaveAttribute('data-paused', 'true', { timeout: 10_000 });
  await expect(tv.getByText(/paused/i).first()).toBeVisible();
  await host.getByRole('button', { name: 'Resume' }).click();
  await expect(screen(ava)).not.toHaveAttribute('data-paused', 'true', { timeout: 10_000 });

  // Play the rest of round 1 through to its results.
  await expect(screen(ava)).toHaveAttribute('data-screen', 'round-over', { timeout: 150_000 });
  expect(a.answered).toBeGreaterThanOrEqual(5);
  expect(b.answered).toBeGreaterThanOrEqual(5);

  // End the show: everyone sees the final standings.
  await host.getByRole('button', { name: 'End event' }).click();
  await host.getByRole('dialog').getByRole('button', { name: 'End event' }).click();
  await expect(screen(ava)).toHaveAttribute('data-screen', 'complete', { timeout: 15_000 });
  await expect(screen(ben)).toHaveAttribute('data-screen', 'complete', { timeout: 15_000 });
  await expect(tv.getByText(/final standings/i).first()).toBeVisible({ timeout: 15_000 });
  await a.stop();
  await b.stop();

  expect(errors).toEqual([]);
});
