import { mkdir, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { expect, test, type Page, type Route } from '@playwright/test';

const response = (data: unknown) => ({ result: { data } });
const artifactRoot = process.env.MAIN_NAVIGATION_ARTIFACT_DIR;
const autoRefreshArtifactRoot = process.env.AUTO_REFRESH_ARTIFACT_DIR;
const productionBundle = process.env.PLAYWRIGHT_FRONTEND_MODE === 'production';
const basePath = `/${(process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'che').replace(/^\/+|\/+$/g, '')}`;
const gameProfile = process.env.PLAYWRIGHT_GAME_PROFILE ?? 'che:default';
const operationNames = (route: Route) =>
    decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');

type NavigationFixture = {
    officerLevel: number;
    permission: number;
    nationLevel: number;
    stage: number;
    npcMode: number;
    generalMeCalls: number;
    operations: string[];
    generalName?: string;
    cityDefence?: number;
    cityState?: number;
    nationRate?: number;
    contextRevision?: string;
    contextOperations?: JsonPatchOperation[];
    commandTableRevision?: string;
    commandTableOperations?: JsonPatchOperation[];
    commandBlockedCount?: number;
    forceSnapshotCalls?: number;
    refreshDelayMs?: number;
    largeCommandTable?: boolean;
    reservedTurns?: Array<{ index: number; action: string; args: Record<string, unknown> }>;
    dashboardResponses?: Array<{
        bytes: number;
        contextKind: string | null;
        commandTableKind: string | null;
        boardAccessKind: string | null;
    }>;
};

type JsonPatchOperation = {
    op: 'add' | 'remove' | 'replace';
    path: string;
    value?: unknown;
};

type DashboardBundleInput = {
    include?: { context?: boolean; commandTable?: boolean; boardAccess?: boolean };
    known?: { context?: string; commandTable?: string; boardAccess?: string };
    forceSnapshot?: boolean;
};

const operationInput = (route: Route, index: number): DashboardBundleInput => {
    const input = new URL(route.request().url()).searchParams.get('input');
    if (!input) return {};
    const parsed = JSON.parse(input) as Record<string, unknown>;
    const entry = (parsed[String(index)] ?? parsed) as { json?: DashboardBundleInput };
    return entry.json ?? (entry as DashboardBundleInput);
};

const readModelChanges = (
    overrides: Partial<{
        generalIds: number[];
        cityIds: number[];
        nationIds: number[];
        mapGeneralIds: number[];
        mapCityIds: number[];
        mapNationIds: number[];
        frontStatusGeneralIds: number[];
        frontStatusNationIds: number[];
        frontStatusActorIds: number[];
        frontStatusChanged: boolean;
        lobbyGeneralIds: number[];
        lobbyChanged: boolean;
        reservedGeneralIds: number[];
        recordGeneralIds: number[];
        worldChanged: boolean;
        globalRecordsChanged: boolean;
        worldHistoryChanged: boolean;
        contactsChanged: boolean;
    }>
) => ({
    generalIds: [],
    cityIds: [],
    nationIds: [],
    mapGeneralIds: [],
    mapCityIds: [],
    mapNationIds: [],
    frontStatusGeneralIds: [],
    frontStatusNationIds: [],
    frontStatusActorIds: [],
    frontStatusChanged: false,
    lobbyGeneralIds: [],
    lobbyChanged: false,
    reservedGeneralIds: [],
    recordGeneralIds: [],
    worldChanged: false,
    globalRecordsChanged: false,
    worldHistoryChanged: false,
    contactsChanged: false,
    ...overrides,
});

const emitReadModelChanges = (page: Page, changes: ReturnType<typeof readModelChanges>) =>
    page.evaluate((payload) => {
        (window as unknown as { __emitMainRealtime: (type: string, value: unknown) => void }).__emitMainRealtime(
            'readModelChanged',
            {
                at: new Date().toISOString(),
                revision: Date.now(),
                changes: payload,
            }
        );
    }, changes);

const commandTableFixture = (large: boolean, blockedCount = 0) => ({
    general: large
        ? ['내정', '군사', '계략'].map((category, categoryIndex) => ({
              category,
              values: Array.from({ length: 16 }, (_, localIndex) => {
                  const index = categoryIndex * 16 + localIndex;
                  return {
                      key: `command-${index}`,
                      name: index === 0 ? '주민 선정과 장기 도시 개발' : `명령 ${index}`,
                      reqArg: index % 2 === 0,
                      possible: index >= blockedCount,
                      status: index >= blockedCount ? 'available' : 'blocked',
                      inputFields: [
                          {
                              key: 'amount',
                              label: '수량',
                              kind: 'number',
                              required: true,
                              min: 1,
                              max: 10_000,
                          },
                      ],
                  };
              }),
          }))
        : [],
    nation: [],
    inputOptions: {
        cities: Array.from({ length: 20 }, (_, index) => ({ value: index + 1, label: `도시 ${index + 1}` })),
        nations: [],
        generals: [],
        crewTypes: [],
        armTypes: [],
        nationTypes: [],
        colors: [],
        items: {},
    },
});

const CONTEXT_INITIAL_REVISION = 'AAAAAAAAAAAAAAAAAAAAAA';
const COMMAND_TABLE_REVISION = 'CCCCCCCCCCCCCCCCCCCCCC';
const BOARD_ACCESS_REVISION = 'DDDDDDDDDDDDDDDDDDDDDD';

const contextRevision = (state: NavigationFixture) => {
    if (state.contextRevision) return state.contextRevision;
    const name = state.generalName ?? '메뉴검증장수';
    if (name === '메뉴검증장수') return CONTEXT_INITIAL_REVISION;
    if (name === '부드럽게갱신된장수') return 'EEEEEEEEEEEEEEEEEEEEEE';
    if (name === '탭공유갱신장수') return 'FFFFFFFFFFFFFFFFFFFFFF';
    if (name === '리더만갱신장수') return 'GGGGGGGGGGGGGGGGGGGGGG';
    return 'HHHHHHHHHHHHHHHHHHHHHH';
};

const deltaSlice = <T>(
    value: T,
    revision: string,
    known: string | undefined,
    forceSnapshot: boolean,
    operations?: JsonPatchOperation[]
) => {
    if (forceSnapshot || !known) return { kind: 'snapshot' as const, revision, data: value };
    if (known === revision) return { kind: 'unchanged' as const, revision };
    return {
        kind: 'patch' as const,
        baseRevision: known,
        revision,
        operations: operations ?? [
            {
                op: 'replace' as const,
                path: '/general/name',
                value: (value as ReturnType<typeof generalContext>).general.name,
            },
        ],
    };
};

const emptyMessages = (permission: number) => ({
    private: [],
    national: [],
    public: [],
    diplomacy: [],
    permission,
    sequence: -1,
    hasMore: { private: false, national: false, public: false, diplomacy: false },
    latestRead: { private: 0, national: 0, public: 0, diplomacy: 0 },
    canRespondDiplomacy: false,
});

const generalContext = (state: NavigationFixture) => ({
    general: {
        id: 7,
        name: state.generalName ?? '메뉴검증장수',
        nationId: 1,
        cityId: 1,
        troopId: 0,
        npcState: 0,
        officerLevel: state.officerLevel,
        picture: null,
        imageServer: 0,
        stats: { leadership: 70, strength: 60, intelligence: 50 },
        gold: 1_000,
        rice: 2_000,
        crew: 300,
        train: 80,
        atmos: 90,
        injury: 0,
        experience: 100,
        dedication: 200,
        progression: {
            experienceLevel: 1,
            dedicationLevel: 2,
            statExperience: { leadership: 5, strength: 10, intelligence: 15 },
            statUpgradeLimit: 20,
            dex: [350, 100_000, 500_000, 1_000_000, 1_275_975],
        },
        items: { horse: null, weapon: null, book: null, item: null },
        turnTime: '0185-01-01T00:00:00.000Z',
    },
    city: {
        id: 1,
        name: '업',
        level: 8,
        nationId: 1,
        region: 1,
        population: 100_000,
        populationMax: 200_000,
        agriculture: 1_000,
        agricultureMax: 2_000,
        commerce: 1_000,
        commerceMax: 2_000,
        security: 1_000,
        securityMax: 2_000,
        trust: 80,
        trade: 100,
        defence: state.cityDefence ?? 1_000,
        defenceMax: 2_000,
        wall: 1_000,
        wallMax: 2_000,
        supplyState: 1,
        frontState: 0,
        state: state.cityState ?? 0,
    },
    nation: {
        id: 1,
        name: '위',
        color: '#008000',
        level: state.nationLevel,
        gold: 10_000,
        rice: 20_000,
        tech: 100,
        rate: state.nationRate ?? 20,
        bill: 100,
        capitalCityId: 1,
        typeCode: 'che_유가',
    },
    settings: {},
    penalties: {},
});

const installFixture = async (page: Page, state: NavigationFixture) => {
    await page.addInitScript(
        ({ profile }) => {
            localStorage.setItem('sammo-game-token', 'ga_navigation');
            localStorage.setItem('sammo-game-profile', profile);
        },
        { profile: gameProfile }
    );

    await page.route('**/image/**', async (route) => {
        await route.fulfill({ status: 200, contentType: 'image/jpeg', body: Buffer.from('') });
    });
    await page.route('**/events**', async (route) => {
        await route.abort();
    });
    await page.route('**/gateway/api/trpc/**', async (route) => {
        const operations = operationNames(route);
        const results = operations.map((operation) =>
            operation === 'me'
                ? response({ id: 'user-7', username: 'menu-user', displayName: '메뉴 사용자' })
                : response({ ok: true })
        );
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(operations.length === 1 ? results[0] : results),
        });
    });
    await page.route(`**${basePath}/api/trpc/**`, async (route) => {
        const operations = operationNames(route);
        state.operations.push(...operations);
        if (
            operations.some((operation) => ['general.me', 'dashboard.getContextBundleDelta'].includes(operation)) &&
            state.generalMeCalls > 0 &&
            state.refreshDelayMs
        ) {
            await new Promise((resolve) => setTimeout(resolve, state.refreshDelayMs));
        }
        const results = operations.map((operation, index) => {
            if (operation === 'auth.status') return response({ ok: true });
            if (operation === 'lobby.info') {
                return response({ myGeneral: { id: 7, name: '메뉴검증장수' }, year: 185, month: 1, turnTerm: 10 });
            }
            if (operation === 'dashboard.getContextBundleDelta') {
                state.generalMeCalls += 1;
                const input = operationInput(route, index);
                const include = input.include ?? {};
                const forceSnapshot = input.forceSnapshot === true;
                if (forceSnapshot) state.forceSnapshotCalls = (state.forceSnapshotCalls ?? 0) + 1;
                const revision = contextRevision(state);
                const context = include.context
                    ? deltaSlice(
                          generalContext(state),
                          revision,
                          input.known?.context,
                          forceSnapshot,
                          state.contextOperations
                      )
                    : undefined;
                const currentCommandTableRevision = state.commandTableRevision ?? COMMAND_TABLE_REVISION;
                const commandTable = include.commandTable
                    ? forceSnapshot || !input.known?.commandTable
                        ? {
                              kind: 'snapshot' as const,
                              revision: currentCommandTableRevision,
                              data: commandTableFixture(state.largeCommandTable === true, state.commandBlockedCount),
                          }
                        : input.known.commandTable === currentCommandTableRevision
                          ? { kind: 'unchanged' as const, revision: currentCommandTableRevision }
                          : {
                                kind: 'patch' as const,
                                baseRevision: input.known.commandTable,
                                revision: currentCommandTableRevision,
                                operations: state.commandTableOperations ?? [],
                            }
                    : undefined;
                const boardAccess = include.boardAccess
                    ? forceSnapshot || !input.known?.boardAccess
                        ? {
                              kind: 'snapshot' as const,
                              revision: BOARD_ACCESS_REVISION,
                              data: {
                                  permission: state.permission,
                                  canMeeting: state.officerLevel >= 1,
                                  canSecret: state.permission >= 2,
                              },
                          }
                        : { kind: 'unchanged' as const, revision: BOARD_ACCESS_REVISION }
                    : undefined;
                return response({ context, commandTable, boardAccess });
            }
            if (operation === 'general.me') {
                state.generalMeCalls += 1;
                return response(generalContext(state));
            }
            if (operation === 'world.getState') {
                return response({
                    currentYear: 185,
                    currentMonth: 1,
                    tickSeconds: 600,
                    config: { npcMode: state.npcMode, const: {}, environment: {} },
                    meta: {},
                });
            }
            if (operation === 'world.getMapLayout') {
                return response({
                    mapName: 'che',
                    cityList: [{ id: 1, name: '업', level: 8, region: 1, x: 200, y: 120, path: [] }],
                    regionMap: { 1: '하북' },
                    levelMap: { 8: '특' },
                });
            }
            if (operation === 'world.getMap') {
                return response({
                    result: true,
                    version: 0,
                    startYear: 180,
                    year: 185,
                    month: 1,
                    cityList: [[1, 8, state.cityState ?? 0, 1, 1, 1]],
                    nationList: [[1, '위', '#008000', 1]],
                    spyList: {},
                    shownByGeneralList: [],
                    myCity: 1,
                    myNation: 1,
                });
            }
            if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
            if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
                return response({ turns: state.reservedTurns ?? [], revision: 0 });
            }
            if (operation === 'messages.getRecent') return response(emptyMessages(state.permission));
            if (operation === 'messages.getContacts') return response({ nation: [] });
            if (operation === 'general.getRecentRecords') {
                return response({
                    global: [{ id: 3, text: '장수 동향 기록' }],
                    general: [{ id: 2, text: '개인 기록' }],
                    history: [{ id: 1, text: '중원 정세 기록' }],
                });
            }
            if (operation === 'general.getFrontStatus') {
                return response({
                    onlineUserCount: 1,
                    onlineNations: '위(1)',
                    onlineGenerals: '메뉴검증장수',
                    nationNotice: '<p>국가 방침</p>',
                    lastExecuted: null,
                    latestVote: { id: 9, title: '메뉴 설문', hasVoted: false },
                });
            }
            if (operation === 'board.getAccess') {
                return response({
                    permission: state.permission,
                    canMeeting: state.officerLevel >= 1,
                    canSecret: state.permission >= 2,
                });
            }
            if (operation === 'tournament.getState') return response({ stage: state.stage });
            return response({ ok: true });
        });
        operations.forEach((operation, index) => {
            if (operation !== 'dashboard.getContextBundleDelta') return;
            const item = results[index];
            if (!item) return;
            const data = item.result.data as {
                context?: { kind: string };
                commandTable?: { kind: string };
                boardAccess?: { kind: string };
            };
            (state.dashboardResponses ??= []).push({
                bytes: Buffer.byteLength(JSON.stringify(item)),
                contextKind: data.context?.kind ?? null,
                commandTableKind: data.commandTable?.kind ?? null,
                boardAccessKind: data.boardAccess?.kind ?? null,
            });
        });
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(operations.length === 1 ? results[0] : results),
        });
    });
};

const installRealtimeHarness = async (page: Page) => {
    await page.addInitScript(() => {
        class TestEventSource extends EventTarget {
            static latest: TestEventSource | null = null;
            static created = 0;
            static closed = 0;
            readonly url: string;

            constructor(url: string | URL) {
                super();
                this.url = url.toString();
                TestEventSource.created += 1;
                TestEventSource.latest = this;
                queueMicrotask(() => this.dispatchEvent(new Event('open')));
            }

            close() {
                if (TestEventSource.latest === this) {
                    TestEventSource.latest = null;
                    TestEventSource.closed += 1;
                }
            }
        }

        Object.defineProperty(window, 'EventSource', { configurable: true, value: TestEventSource });
        Object.defineProperty(window, '__emitMainRealtime', {
            configurable: true,
            value: (type: string, payload: unknown) => {
                TestEventSource.latest?.dispatchEvent(
                    new MessageEvent(type, { data: JSON.stringify({ type, ...((payload as object) ?? {}) }) })
                );
            },
        });
        Object.defineProperty(window, '__hasMainRealtime', {
            configurable: true,
            value: () => TestEventSource.latest !== null,
        });
        Object.defineProperty(window, '__mainRealtimeStats', {
            configurable: true,
            value: () => ({
                active: TestEventSource.latest !== null,
                created: TestEventSource.created,
                closed: TestEventSource.closed,
            }),
        });
    });
};

const waitForMain = async (page: Page) => {
    await page.goto('./');
    await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible();
    await expect(page.locator('.main-global-menu').first()).toBeVisible();
    await expect(page.locator('.main-nation-menu')).toBeVisible();
    await expect(page.locator('[data-navigation-id="npc-list"]')).toHaveCount(3);
};

const gridColumnCount = async (page: Page, selector: string) =>
    page
        .locator(selector)
        .first()
        .evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);

const persistArtifact = async (page: Page, name: string) => {
    if (!artifactRoot) return;
    const target = resolve(artifactRoot);
    await mkdir(target, { recursive: true });
    const geometry = await page.evaluate(() => {
        const describe = (selector: string) => {
            const element = document.querySelector<HTMLElement>(selector);
            if (!element) return null;
            const rect = element.getBoundingClientRect();
            const style = getComputedStyle(element);
            return {
                rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
                display: style.display,
                position: style.position,
                zIndex: style.zIndex,
                gridTemplateColumns: style.gridTemplateColumns,
                columnCount: style.columnCount,
                gap: style.gap,
                fontSize: style.fontSize,
                lineHeight: style.lineHeight,
                color: style.color,
                backgroundColor: style.backgroundColor,
                backgroundImage: style.backgroundImage,
                borderColor: style.borderColor,
                padding: style.padding,
                boxShadow: style.boxShadow,
                overflowY: style.overflowY,
                maxHeight: style.maxHeight,
                cursor: style.cursor,
                opacity: style.opacity,
                visibility: style.visibility,
            };
        };
        return {
            viewport: { width: innerWidth, height: innerHeight },
            global: describe('.main-global-menu'),
            nation: describe('.main-nation-menu'),
            bottom: describe('.main-mobile-bottom'),
            globalPopup: describe('#mobile-global-menu'),
            nationPopup: describe('#mobile-nation-menu'),
            quickPopup: describe('#mobile-quick-menu'),
        };
    });
    await Promise.all([
        page.screenshot({ path: resolve(target, `${name}.png`), fullPage: true }),
        writeFile(resolve(target, `${name}.json`), `${JSON.stringify(geometry, null, 2)}\n`),
    ]);
};

test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({ page }) => {
    const state: NavigationFixture = {
        officerLevel: 5,
        permission: 2,
        nationLevel: 3,
        stage: 1,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
    };
    await installFixture(page, state);
    await page.setViewportSize({ width: 1200, height: 900 });
    await waitForMain(page);

    await expect(page.locator('.main-global-menu')).toHaveCount(3);
    expect(await gridColumnCount(page, '.main-global-menu')).toBe(8);
    expect(await gridColumnCount(page, '.main-nation-menu')).toBe(10);
    await expect(page.locator('.main-mobile-bottom')).toBeHidden();
    await expect(page.locator('.layout-desktop')).toBeVisible();
    await expect(page.locator('.layout-mobile')).toHaveCount(0);
    const contentOrder = await page
        .locator('.record-zone, [data-menu-position="middle"], .desktop-message-panel, [data-menu-position="bottom"]')
        .evaluateAll((elements) =>
            elements.map((element) => {
                if (element.classList.contains('record-zone')) return 'records';
                if (element.getAttribute('data-menu-position') === 'middle') return 'middle-menu';
                if (element.classList.contains('desktop-message-panel')) return 'messages';
                return 'bottom-menu';
            })
        );
    expect(contentOrder).toEqual(['records', 'middle-menu', 'messages', 'bottom-menu']);

    const global = page.locator('.main-global-menu').first();
    await expect(global.locator('[data-navigation-id="nation-betting"]')).toHaveAttribute(
        'href',
        `${basePath}/nation-betting`
    );
    await expect(global.locator('[data-navigation-id="nation-list"]')).toHaveAttribute(
        'href',
        `${basePath}/nation-list`
    );
    await expect(global.locator('[data-navigation-id="nation-list"]')).toHaveAttribute('target', '_blank');
    await expect(global.locator('[data-navigation-id="board-community"]')).toHaveAttribute('href', '/xe/community');
    await expect(global.locator('[data-navigation-id="official-chat"]')).toHaveAttribute('aria-disabled', 'true');
    await expect(global.locator('[data-navigation-id="survey"]')).toHaveClass(/highlight/);
    await expect(page.locator('.main-nation-menu [data-navigation-id="tournament"]')).toHaveClass(/highlight/);

    const gameInfoButton = global.locator('[data-menu-id="game-info"]');
    await gameInfoButton.focus();
    await gameInfoButton.press('Enter');
    await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true');
    await expect(global.locator('#global-menu-game-info')).toBeVisible();
    await page.keyboard.press('Escape');
    await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'false');
    await expect(gameInfoButton).toBeFocused();

    await gameInfoButton.click();
    await page.getByRole('heading', { name: '전장 현황' }).click();
    await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'false');
    await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
});

test('main cards and command input stay inside their Ref-sized grid slots', async ({ page }) => {
    const state: NavigationFixture = {
        officerLevel: 1,
        permission: 0,
        nationLevel: 1,
        stage: 0,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
        largeCommandTable: true,
        reservedTurns: Array.from({ length: 14 }, (_, index) => ({
            index,
            action: `command-${index}`,
            args: {},
        })),
    };
    await installFixture(page, state);
    await page.setViewportSize({ width: 1200, height: 900 });
    await waitForMain(page);

    const cityBars = page.locator('[data-main-target="city"] [role="progressbar"]');
    const statBars = page.locator('[data-stat-progress] [role="progressbar"]');
    const experienceBar = page.locator('[data-experience-progress] [role="progressbar"]');
    await expect(cityBars).toHaveCount(8);
    await expect(statBars).toHaveCount(3);
    await expect(experienceBar).toHaveCount(1);
    await expect(page.locator('[data-main-target="general"] [data-dex-progress]')).toHaveCount(0);
    await expect(page.locator('[data-main-target="general"] [role="progressbar"]')).toHaveCount(4);

    expect(await cityBars.first().evaluate((element) => element.getBoundingClientRect().height)).toBe(9);
    expect(await statBars.first().evaluate((element) => element.getBoundingClientRect().height)).toBe(12);
    expect(await experienceBar.evaluate((element) => element.getBoundingClientRect().height)).toBe(12);
    const texture = await cityBars.first().evaluate((element) => getComputedStyle(element).backgroundImage);
    const fillTexture = await cityBars
        .first()
        .locator('.legacy-progress__fill')
        .evaluate((element) => getComputedStyle(element).backgroundImage);
    expect(texture).toContain('/game/pr5.gif');
    expect(fillTexture).toContain('/game/pb5.gif');

    const desktopGeometry = await page.evaluate(() => {
        const box = (element: Element) => {
            const rect = element.getBoundingClientRect();
            return {
                x: rect.x,
                y: rect.y,
                width: rect.width,
                height: rect.height,
                right: rect.right,
                bottom: rect.bottom,
            };
        };
        const target = (name: string) => {
            const panel = document.querySelector<HTMLElement>(`[data-main-target="${name}"]`);
            if (!panel) throw new Error(`${name} panel missing`);
            return panel;
        };
        const general = target('general');
        const nation = target('nation');
        const city = target('city');
        const commands = target('commands');
        const generalBody = general.querySelector<HTMLElement>('.general-body');
        const editor = commands.querySelector<HTMLElement>('.reserved-command-editor');
        const controlPad = commands.querySelector<HTMLElement>('.control-pad');
        const bottomActions = commands.querySelector<HTMLElement>('.bottom-actions');
        if (!generalBody || !editor || !controlPad || !bottomActions) throw new Error('main layout probe missing');
        const visibleControlBoxes = [...controlPad.children]
            .filter((element) => getComputedStyle(element).display !== 'none')
            .map(box);
        const bottomActionBoxes = [...bottomActions.children].map(box);
        return {
            general: box(general),
            generalBody: box(generalBody),
            nation: box(nation),
            city: box(city),
            commands: box(commands),
            editor: box(editor),
            controlPad: box(controlPad),
            controlColumns: getComputedStyle(controlPad).gridTemplateColumns,
            visibleControlBoxes,
            bottomActionBoxes,
            commandHorizontalOverflow: commands.scrollWidth - commands.clientWidth,
            commandVerticalOverflow: commands.scrollHeight - commands.clientHeight,
        };
    });
    expect(desktopGeometry.general.y).toBe(desktopGeometry.nation.y);
    expect(desktopGeometry.general.bottom).toBe(desktopGeometry.nation.bottom);
    expect(desktopGeometry.commands.bottom).toBe(desktopGeometry.city.bottom);
    expect(desktopGeometry.generalBody.bottom).toBeLessThanOrEqual(desktopGeometry.general.bottom);
    expect(desktopGeometry.editor.right).toBeLessThanOrEqual(desktopGeometry.commands.right);
    expect(desktopGeometry.commandHorizontalOverflow).toBeLessThanOrEqual(0);
    expect(desktopGeometry.commandVerticalOverflow).toBeLessThanOrEqual(0);
    expect(desktopGeometry.controlColumns.split(' ')).toHaveLength(3);
    expect(desktopGeometry.visibleControlBoxes).toHaveLength(3);
    expect(new Set(desktopGeometry.visibleControlBoxes.map(({ y }) => y)).size).toBe(1);
    expect(desktopGeometry.bottomActionBoxes).toHaveLength(3);
    expect(new Set(desktopGeometry.bottomActionBoxes.map(({ y }) => y)).size).toBe(1);

    const modeButton = page.locator('[data-main-target="commands"] .control-pad').getByRole('button', {
        name: '고급 모드',
    });
    await modeButton.hover();
    await modeButton.focus();
    await expect(modeButton).toBeFocused();
    await modeButton.click();
    await page.locator('[data-main-target="commands"] .select-command').click();
    const picker = page.getByTestId('command-picker');
    await expect(picker).toBeVisible();
    const pickerGeometry = await picker.evaluate((element) => {
        const rect = element.getBoundingClientRect();
        const editor = element.closest('.reserved-command-editor')?.getBoundingClientRect();
        const categories = element.querySelector<HTMLElement>('.category-list');
        if (!editor || !categories) throw new Error('command picker geometry missing');
        return {
            left: rect.left,
            right: rect.right,
            editorLeft: editor.left,
            editorRight: editor.right,
            categoryColumns: getComputedStyle(categories).gridTemplateColumns,
            categoryBoxes: [...categories.children].map((child) => {
                const childRect = child.getBoundingClientRect();
                return { x: childRect.x, y: childRect.y, width: childRect.width, height: childRect.height };
            }),
            horizontalOverflow: element.scrollWidth - element.clientWidth,
        };
    });
    expect(pickerGeometry.left).toBeGreaterThanOrEqual(pickerGeometry.editorLeft);
    expect(pickerGeometry.right).toBeLessThanOrEqual(pickerGeometry.editorRight);
    expect(pickerGeometry.categoryColumns.split(' ')).toHaveLength(3);
    expect(pickerGeometry.categoryBoxes).toHaveLength(3);
    expect(new Set(pickerGeometry.categoryBoxes.map(({ y }) => y)).size).toBe(1);
    expect(pickerGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
    await picker.getByRole('button', { name: '명령 입력 닫기' }).click();

    const captureProgress = async (name: string) => {
        if (!artifactRoot) return;
        await mkdir(artifactRoot, { recursive: true });
        const measurement = await page.locator('.legacy-progress').evaluateAll((elements) =>
            elements.map((element) => {
                const rect = element.getBoundingClientRect();
                const style = getComputedStyle(element);
                const fill = element.querySelector<HTMLElement>('.legacy-progress__fill');
                return {
                    label: element.getAttribute('aria-label'),
                    rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
                    borderTop: style.borderTop,
                    borderBottom: style.borderBottom,
                    backgroundImage: style.backgroundImage,
                    fillWidth: fill?.getBoundingClientRect().width ?? 0,
                    fillBackgroundImage: fill ? getComputedStyle(fill).backgroundImage : '',
                };
            })
        );
        const layout = await page.evaluate(() => {
            const describe = (selector: string) => {
                const element = document.querySelector<HTMLElement>(selector);
                if (!element) return null;
                const rect = element.getBoundingClientRect();
                return {
                    rect: {
                        x: rect.x,
                        y: rect.y,
                        width: rect.width,
                        height: rect.height,
                        right: rect.right,
                        bottom: rect.bottom,
                    },
                    clientWidth: element.clientWidth,
                    clientHeight: element.clientHeight,
                    scrollWidth: element.scrollWidth,
                    scrollHeight: element.scrollHeight,
                    overflowX: getComputedStyle(element).overflowX,
                    overflowY: getComputedStyle(element).overflowY,
                    boxSizing: getComputedStyle(element).boxSizing,
                    cssWidth: getComputedStyle(element).width,
                    minWidth: getComputedStyle(element).minWidth,
                    paddingInline: getComputedStyle(element).paddingInline,
                    borderInline: `${getComputedStyle(element).borderLeftWidth} ${getComputedStyle(element).borderRightWidth}`,
                };
            };
            return {
                viewport: { width: innerWidth, height: innerHeight },
                documentWidth: document.documentElement.scrollWidth,
                roots: {
                    html: describe('html'),
                    body: describe('body'),
                    app: describe('#app'),
                    main: describe('main.main-page'),
                },
                overflowingElements: [...document.body.querySelectorAll<HTMLElement>('*')]
                    .map((element) => {
                        const rect = element.getBoundingClientRect();
                        return {
                            tag: element.tagName.toLowerCase(),
                            className: element.className,
                            testId: element.dataset.testid ?? null,
                            left: rect.left,
                            right: rect.right,
                            width: rect.width,
                        };
                    })
                    .filter(({ left, right }) => left < 0 || right > innerWidth)
                    .slice(0, 20),
                city: describe('[data-main-target="city"]'),
                nation: describe('[data-main-target="nation"]'),
                general: describe('[data-main-target="general"]'),
                commands: describe('[data-main-target="commands"]'),
                commandEditor: describe('[data-main-target="commands"] .reserved-command-editor'),
                commandPicker: describe('[data-testid="command-picker"]'),
            };
        });
        await Promise.all([
            page.screenshot({ path: resolve(artifactRoot, `progress-bars-${name}.png`), fullPage: true }),
            writeFile(
                resolve(artifactRoot, `progress-bars-${name}.json`),
                `${JSON.stringify({ layout, progress: measurement }, null, 2)}\n`
            ),
        ]);
    };
    await captureProgress('desktop-1200');

    await page.locator('[data-main-target="commands"] .control-pad').getByRole('button', { name: '일반 모드' }).click();
    await page.setViewportSize({ width: 500, height: 900 });
    await expect(page.locator('.layout-mobile')).toBeVisible();
    expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(500);
    await expect(page.locator('[data-main-target="city"] [role="progressbar"]')).toHaveCount(8);
    await expect(page.locator('[data-main-target="general"] [role="progressbar"]')).toHaveCount(4);
    expect(
        await page
            .locator('[data-main-target="city"] [role="progressbar"]')
            .first()
            .evaluate((element) => element.getBoundingClientRect().height)
    ).toBe(9);
    const mobileGeometry = await page.locator('[data-main-target="commands"]').evaluate((commands) => {
        const editor = commands.querySelector<HTMLElement>('.reserved-command-editor');
        const controls = commands.querySelector<HTMLElement>('.control-pad');
        if (!editor || !controls) throw new Error('mobile command layout probe missing');
        const panelRect = commands.getBoundingClientRect();
        const editorRect = editor.getBoundingClientRect();
        const controlBoxes = [...controls.children]
            .filter((element) => getComputedStyle(element).display !== 'none')
            .map((element) => {
                const rect = element.getBoundingClientRect();
                return { y: rect.y, width: rect.width };
            });
        return {
            panelWidth: panelRect.width,
            panelHeight: panelRect.height,
            editorLeft: editorRect.left,
            editorRight: editorRect.right,
            panelLeft: panelRect.left,
            panelRight: panelRect.right,
            horizontalOverflow: commands.scrollWidth - commands.clientWidth,
            verticalOverflow: commands.scrollHeight - commands.clientHeight,
            controlColumns: getComputedStyle(controls).gridTemplateColumns,
            controlBoxes,
        };
    });
    expect(mobileGeometry.panelHeight).toBe(645);
    expect(mobileGeometry.editorLeft).toBeGreaterThanOrEqual(mobileGeometry.panelLeft);
    expect(mobileGeometry.editorRight).toBeLessThanOrEqual(mobileGeometry.panelRight);
    expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
    expect(mobileGeometry.verticalOverflow).toBeLessThanOrEqual(0);
    expect(mobileGeometry.controlColumns.split(' ')).toHaveLength(3);
    expect(mobileGeometry.controlBoxes).toHaveLength(3);
    expect(new Set(mobileGeometry.controlBoxes.map(({ y }) => y)).size).toBe(1);
    await captureProgress('mobile-500');
});

test('the 939/940 boundary switches to the Ref-style 500px single document', async ({ page }) => {
    const state: NavigationFixture = {
        officerLevel: 5,
        permission: 2,
        nationLevel: 3,
        stage: 6,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
    };
    await installFixture(page, state);
    await page.setViewportSize({ width: 940, height: 900 });
    await waitForMain(page);
    expect(await gridColumnCount(page, '.main-global-menu')).toBe(8);
    expect(await gridColumnCount(page, '.main-nation-menu')).toBe(10);
    await expect(page.locator('.main-mobile-bottom')).toBeHidden();
    await expect(page.locator('.layout-desktop')).toBeVisible();

    await page.setViewportSize({ width: 939, height: 900 });
    await expect(page.locator('.layout-mobile')).toBeVisible();
    expect(await gridColumnCount(page, '.main-global-menu')).toBe(4);
    expect(await gridColumnCount(page, '.main-nation-menu')).toBe(5);
    await expect(page.locator('.main-mobile-bottom')).toBeVisible();

    await page.setViewportSize({ width: 500, height: 900 });
    const documentGeometry = await page.locator('.main-page').evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return {
            x: rect.x,
            width: rect.width,
            height: rect.height,
            scrollWidth: document.documentElement.scrollWidth,
        };
    });
    expect(documentGeometry).toMatchObject({
        x: 0,
        width: 500,
        scrollWidth: 500,
    });
    expect(documentGeometry.height).toBeGreaterThan(900);
    for (const selector of [
        '[data-main-target="commands"]',
        '[data-main-target="general"]',
        '[data-main-target="map"]',
        '[data-main-target="world-history"]',
        '.mobile-message-panel',
    ]) {
        await expect(page.locator(selector)).toBeVisible();
    }
    await persistArtifact(page, `${basePath.slice(1)}-mobile-500`);
});

test('nation menu presentation follows the server-derived permission matrix', async ({ page }) => {
    const state: NavigationFixture = {
        officerLevel: 1,
        permission: 0,
        nationLevel: 1,
        stage: 0,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
    };
    await installFixture(page, state);
    await page.setViewportSize({ width: 1200, height: 900 });
    await waitForMain(page);
    const nationMenu = page.locator('.main-nation-menu');

    await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', `${basePath}/board`);
    await expect(nationMenu.locator('[data-navigation-id="secret-board"]')).toHaveAttribute('aria-disabled', 'true');
    await expect(nationMenu.locator('[data-navigation-id="diplomacy"]')).toHaveAttribute('aria-disabled', 'true');
    await expect(nationMenu.locator('[data-navigation-id="nation-info"]')).toHaveAttribute(
        'href',
        `${basePath}/nation/info`
    );

    state.officerLevel = 2;
    const callsBeforeRefresh = state.generalMeCalls;
    await page.getByRole('button', { name: '갱 신' }).click();
    await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeRefresh);
    await expect(nationMenu.locator('[data-navigation-id="diplomacy"]')).toHaveAttribute(
        'href',
        `${basePath}/diplomacy`
    );
    await expect(nationMenu.locator('[data-navigation-id="nation-secret"]')).toHaveAttribute(
        'href',
        `${basePath}/nation/secret`
    );
});

test('mobile single document refreshes once and preserves tokens on lobby return', async ({ page }) => {
    const state: NavigationFixture = {
        officerLevel: 5,
        permission: 2,
        nationLevel: 3,
        stage: 0,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
    };
    await installFixture(page, state);
    await page.setViewportSize({ width: 500, height: 900 });
    await waitForMain(page);

    for (const selector of [
        '[data-main-target="policy"]',
        '[data-main-target="commands"]',
        '[data-main-target="nation"]',
        '[data-main-target="general"]',
        '[data-main-target="city"]',
        '[data-main-target="map"]',
        '[data-main-target="global-records"]',
        '[data-main-target="general-records"]',
        '[data-main-target="world-history"]',
        '[data-message-type="public"]',
        '[data-message-type="national"]',
        '[data-message-type="private"]',
        '[data-message-type="diplomacy"]',
    ]) {
        await expect(page.locator(selector)).toBeVisible();
    }

    const callsBeforeRefresh = state.generalMeCalls;
    await page.getByRole('button', { name: '갱 신' }).click();
    await expect.poll(() => state.generalMeCalls).toBeGreaterThan(callsBeforeRefresh);

    await page.evaluate(() => {
        localStorage.setItem('sammo-session-token', 'session_navigation');
    });
    await page.route('**/gateway/', async (route) => {
        await route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>gateway</title>' });
    });
    await Promise.all([page.waitForURL('**/gateway/'), page.getByRole('button', { name: '로비로' }).click()]);
    expect(
        await page.evaluate(() => ({
            session: localStorage.getItem('sammo-session-token'),
            game: localStorage.getItem('sammo-game-token'),
            profile: localStorage.getItem('sammo-game-profile'),
        }))
    ).toEqual({
        session: 'session_navigation',
        game: 'ga_navigation',
        profile: gameProfile,
    });
    expect(state.operations).not.toContain('auth.logout');
});

test('realtime read-model events skip clock-only work, merge bursts, patch in place, and stop off-route', async ({
    page,
}) => {
    const state: NavigationFixture = {
        officerLevel: 5,
        permission: 2,
        nationLevel: 3,
        stage: 0,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
        refreshDelayMs: 300,
        largeCommandTable: true,
    };
    await installRealtimeHarness(page);
    await installFixture(page, state);
    await page.setViewportSize({ width: 1200, height: 900 });
    await waitForMain(page);
    await expect(page.locator('.general-title')).toContainText('메뉴검증장수');

    await page.evaluate(() => {
        const general = document.querySelector('[data-main-target="general"]');
        const city = document.querySelector('[data-main-target="city"]');
        if (!general || !city) throw new Error('refresh probe targets missing');
        const probe = {
            general,
            city,
            generalMutations: 0,
            cityMutations: 0,
            vueMeasures: [] as string[],
        };
        new MutationObserver((records) => (probe.generalMutations += records.length)).observe(general, {
            childList: true,
            subtree: true,
            characterData: true,
        });
        new MutationObserver((records) => (probe.cityMutations += records.length)).observe(city, {
            childList: true,
            subtree: true,
            characterData: true,
        });
        Object.defineProperty(window, '__mainRefreshProbe', { configurable: true, value: probe });
        performance.clearMarks();
        performance.clearMeasures();
        new PerformanceObserver((entries) => {
            probe.vueMeasures.push(...entries.getEntries().map((entry) => entry.name));
        }).observe({ entryTypes: ['measure'] });
    });

    const callsBeforeRefresh = state.generalMeCalls;
    const operationsBeforeClockOnly = state.operations.length;
    await page.evaluate(() => {
        (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }).__emitMainRealtime(
            'turnCompleted',
            {
                at: new Date().toISOString(),
                lastTurnTime: '0185-02-01T00:00:00.000Z',
                changes: {
                    generalIds: [],
                    cityIds: [],
                    nationIds: [],
                    reservedGeneralIds: [],
                    recordGeneralIds: [],
                    worldChanged: false,
                    globalRecordsChanged: false,
                    worldHistoryChanged: false,
                    contactsChanged: false,
                },
            }
        );
    });
    await new Promise((resolve) => setTimeout(resolve, 300));
    expect(state.operations.slice(operationsBeforeClockOnly)).toEqual([]);

    const operationsBeforeChangedBurst = state.operations.length;
    state.generalName = '부드럽게갱신된장수';
    await page.evaluate(() => {
        const emit = (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void })
            .__emitMainRealtime;
        for (let index = 0; index < 100; index += 1) {
            emit('turnCompleted', {
                at: new Date().toISOString(),
                lastTurnTime: '0185-02-01T00:00:00.000Z',
                changes: {
                    generalIds: [7],
                    cityIds: [],
                    nationIds: [],
                    mapGeneralIds: [],
                    mapCityIds: [],
                    mapNationIds: [],
                    frontStatusGeneralIds: [],
                    frontStatusNationIds: [],
                    frontStatusActorIds: [],
                    frontStatusChanged: false,
                    lobbyGeneralIds: [],
                    lobbyChanged: false,
                    reservedGeneralIds: [],
                    recordGeneralIds: [],
                    worldChanged: false,
                    globalRecordsChanged: false,
                    worldHistoryChanged: false,
                    contactsChanged: false,
                },
            });
        }
    });

    await expect.poll(() => state.generalMeCalls, { timeout: 3_000 }).toBe(callsBeforeRefresh + 1);
    await expect(page.locator('[data-main-target="general"] .skeleton-line')).toHaveCount(0);
    await expect(page.locator('[data-main-target="city"] .skeleton-line')).toHaveCount(0);
    await expect(page.getByRole('button', { name: '갱 신' })).toHaveAttribute('aria-busy', 'false');
    expect(state.generalMeCalls).toBe(callsBeforeRefresh + 1);
    await expect(page.locator('.general-title')).toContainText('부드럽게갱신된장수');
    const changedOperations = state.operations.slice(operationsBeforeChangedBurst);
    expect(changedOperations).toEqual(['dashboard.getContextBundleDelta']);
    expect(changedOperations).not.toEqual(
        expect.arrayContaining([
            'general.me',
            'turns.getCommandTable',
            'board.getAccess',
            'lobby.info',
            'world.getMap',
            'messages.getRecent',
            'messages.getContacts',
            'general.getRecentRecords',
            'general.getFrontStatus',
            'turns.reserved.getGeneral',
        ])
    );
    const initialBundle = state.dashboardResponses?.find(
        (entry) => entry.contextKind === 'snapshot' && entry.commandTableKind === 'snapshot'
    );
    const realtimeBundle = state.dashboardResponses?.find(
        (entry) => entry.contextKind === 'patch' && entry.commandTableKind === 'unchanged'
    );
    expect(initialBundle?.bytes).toBeGreaterThan(5_000);
    expect(realtimeBundle).toMatchObject({
        contextKind: 'patch',
        commandTableKind: 'unchanged',
        boardAccessKind: 'unchanged',
    });
    expect(realtimeBundle?.bytes).toBeLessThan(1_000);

    const operationsBeforeSurvey = state.operations.length;
    await page.evaluate(() => {
        (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }).__emitMainRealtime(
            'readModelChanged',
            {
                at: new Date().toISOString(),
                revision: 42,
                changes: {
                    generalIds: [],
                    cityIds: [],
                    nationIds: [],
                    mapGeneralIds: [],
                    mapCityIds: [],
                    mapNationIds: [],
                    frontStatusGeneralIds: [],
                    frontStatusNationIds: [],
                    frontStatusActorIds: [],
                    frontStatusChanged: true,
                    lobbyGeneralIds: [],
                    lobbyChanged: false,
                    reservedGeneralIds: [],
                    recordGeneralIds: [],
                    worldChanged: false,
                    globalRecordsChanged: false,
                    worldHistoryChanged: false,
                    contactsChanged: false,
                },
            }
        );
    });
    await expect
        .poll(() => state.operations.slice(operationsBeforeSurvey), { timeout: 3_000 })
        .toEqual(['general.getFrontStatus']);

    const profile = await page.evaluate(() => {
        const probe = (
            window as unknown as {
                __mainRefreshProbe: {
                    general: Element;
                    city: Element;
                    generalMutations: number;
                    cityMutations: number;
                    vueMeasures: string[];
                };
            }
        ).__mainRefreshProbe;
        return {
            generalMounted: probe.general === document.querySelector('[data-main-target="general"]'),
            cityMounted: probe.city === document.querySelector('[data-main-target="city"]'),
            generalMutations: probe.generalMutations,
            cityMutations: probe.cityMutations,
            vueMeasures: probe.vueMeasures.filter((name) => /render|patch/u.test(name)),
        };
    });
    expect(profile.generalMounted).toBe(true);
    expect(profile.cityMounted).toBe(true);
    expect(profile.generalMutations).toBeGreaterThan(0);
    expect(profile.cityMutations).toBe(0);
    if (!productionBundle) {
        expect(profile.vueMeasures.some((name) => name.includes('GeneralBasicCard'))).toBe(true);
        expect(profile.vueMeasures.some((name) => name.includes('CityBasicCard'))).toBe(false);
    }
    if (autoRefreshArtifactRoot) {
        await Promise.all([
            page.screenshot({ path: resolve(autoRefreshArtifactRoot, 'auto-refresh-complete.png'), fullPage: true }),
            writeFile(
                resolve(autoRefreshArtifactRoot, 'profile.json'),
                `${JSON.stringify(
                    {
                        emittedTurnEvents: 100,
                        selectiveGeneralRefreshes: state.generalMeCalls - callsBeforeRefresh,
                        responseBytes: {
                            initialSnapshot: initialBundle?.bytes ?? null,
                            realtimeDelta: realtimeBundle?.bytes ?? null,
                            reductionPercent:
                                initialBundle && realtimeBundle
                                    ? Number(((1 - realtimeBundle.bytes / initialBundle.bytes) * 100).toFixed(2))
                                    : null,
                        },
                        responseKinds: realtimeBundle ?? null,
                        inFlightSkeletons: { general: 0, city: 0 },
                        ...profile,
                    },
                    null,
                    2
                )}\n`
            ),
        ]);
    }

    const callsBeforeDefence = state.generalMeCalls;
    state.cityDefence = 900;
    state.contextRevision = 'I'.repeat(22);
    state.contextOperations = [{ op: 'replace', path: '/city/defence', value: 900 }];
    state.commandTableRevision = 'J'.repeat(22);
    state.commandBlockedCount = 1;
    state.commandTableOperations = [
        { op: 'replace', path: '/general/0/values/0/possible', value: false },
        { op: 'replace', path: '/general/0/values/0/status', value: 'blocked' },
    ];
    await emitReadModelChanges(page, readModelChanges({ cityIds: [1], mapCityIds: [] }));
    await expect.poll(() => state.generalMeCalls, { timeout: 4_000 }).toBe(callsBeforeDefence + 1);
    await expect(page.locator('[data-city-progress="수비"] .city-progress__text')).toHaveText('900 / 2,000');

    const callsBeforeTax = state.generalMeCalls;
    state.nationRate = 25;
    state.contextRevision = 'K'.repeat(22);
    state.contextOperations = [{ op: 'replace', path: '/nation/rate', value: 25 }];
    state.commandTableRevision = 'L'.repeat(22);
    state.commandBlockedCount = 2;
    state.commandTableOperations = [
        { op: 'replace', path: '/general/0/values/1/possible', value: false },
        { op: 'replace', path: '/general/0/values/1/status', value: 'blocked' },
    ];
    await emitReadModelChanges(page, readModelChanges({ nationIds: [1], mapNationIds: [], frontStatusNationIds: [] }));
    await expect.poll(() => state.generalMeCalls, { timeout: 4_000 }).toBe(callsBeforeTax + 1);

    const callsBeforeCityState = state.generalMeCalls;
    const operationsBeforeCityState = state.operations.length;
    state.cityState = 5;
    state.contextRevision = 'M'.repeat(22);
    state.contextOperations = [{ op: 'replace', path: '/city/state', value: 5 }];
    state.commandTableRevision = 'N'.repeat(22);
    state.commandBlockedCount = 3;
    state.commandTableOperations = [
        { op: 'replace', path: '/general/0/values/2/possible', value: false },
        { op: 'replace', path: '/general/0/values/2/status', value: 'blocked' },
    ];
    await emitReadModelChanges(page, readModelChanges({ cityIds: [1], mapCityIds: [1] }));
    await expect.poll(() => state.generalMeCalls, { timeout: 4_000 }).toBe(callsBeforeCityState + 1);
    await expect(page.locator('.city-base .city-state img')).toHaveAttribute('src', /event5\.gif$/u);
    expect(state.operations.slice(operationsBeforeCityState).sort()).toEqual(
        ['dashboard.getContextBundleDelta', 'world.getMap'].sort()
    );

    const callsBeforeFallback = state.generalMeCalls;
    const forcedBeforeFallback = state.forceSnapshotCalls ?? 0;
    state.generalName = 'snapshot복구장수';
    state.contextRevision = 'O'.repeat(22);
    state.contextOperations = [{ op: 'replace', path: '/missing/value', value: 'invalid-delta' }];
    state.commandTableOperations = [];
    await emitReadModelChanges(page, readModelChanges({ generalIds: [7] }));
    await expect.poll(() => state.generalMeCalls, { timeout: 4_000 }).toBe(callsBeforeFallback + 2);
    expect(state.forceSnapshotCalls).toBe(forcedBeforeFallback + 1);
    await expect(page.locator('.general-title')).toContainText('snapshot복구장수');
    await expect(page.locator('.game-feedback')).toHaveCount(0);
    await expect(page.locator('[data-main-target="general"] .skeleton-line')).toHaveCount(0);
    await expect(page.locator('[data-main-target="city"] .skeleton-line')).toHaveCount(0);
    expect(
        await page.evaluate(() => {
            const probe = (
                window as unknown as {
                    __mainRefreshProbe: { general: Element; city: Element };
                }
            ).__mainRefreshProbe;
            return {
                generalMounted: probe.general === document.querySelector('[data-main-target="general"]'),
                cityMounted: probe.city === document.querySelector('[data-main-target="city"]'),
            };
        })
    ).toEqual({ generalMounted: true, cityMounted: true });

    await page.locator(`a[href="${basePath}/board"]`).first().click();
    await page.waitForURL(`**${basePath}/board`);
    expect(
        await page.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime())
    ).toBe(false);
    const callsAfterLeavingMain = state.generalMeCalls;
    await page.evaluate(() => {
        (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }).__emitMainRealtime(
            'turnCompleted',
            {
                at: new Date().toISOString(),
                lastTurnTime: '0185-02-01T00:00:00.000Z',
                changes: {
                    generalIds: [7],
                    cityIds: [],
                    nationIds: [],
                    mapGeneralIds: [],
                    mapCityIds: [],
                    mapNationIds: [],
                    frontStatusGeneralIds: [],
                    frontStatusNationIds: [],
                    frontStatusActorIds: [],
                    frontStatusChanged: false,
                    lobbyGeneralIds: [],
                    lobbyChanged: false,
                    reservedGeneralIds: [],
                    recordGeneralIds: [],
                    worldChanged: false,
                    globalRecordsChanged: false,
                    worldHistoryChanged: false,
                    contactsChanged: false,
                },
            }
        );
    });
    await new Promise((resolve) => setTimeout(resolve, 300));
    expect(state.generalMeCalls).toBe(callsAfterLeavingMain);
});

test('same-account main tabs share one realtime diff and exclude a tab while sync is off', async ({
    context,
    page,
}) => {
    const state: NavigationFixture = {
        officerLevel: 5,
        permission: 2,
        nationLevel: 3,
        stage: 0,
        npcMode: 1,
        generalMeCalls: 0,
        operations: [],
    };
    const secondPage = await context.newPage();
    const pages = [page, secondPage];
    for (const currentPage of pages) {
        await currentPage.addInitScript(() => {
            Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'visible' });
        });
        await installRealtimeHarness(currentPage);
        await installFixture(currentPage, state);
        await currentPage.setViewportSize({ width: 1200, height: 900 });
    }

    await Promise.all(pages.map((currentPage) => waitForMain(currentPage)));
    await expect
        .poll(async () => {
            const stats = await Promise.all(
                pages.map((currentPage) =>
                    currentPage.evaluate(() =>
                        (
                            window as unknown as {
                                __mainRealtimeStats: () => { active: boolean; created: number; closed: number };
                            }
                        ).__mainRealtimeStats()
                    )
                )
            );
            return stats.filter((entry) => entry.active).length;
        })
        .toBe(1);

    const activeFlags = await Promise.all(
        pages.map((currentPage) =>
            currentPage.evaluate(() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime())
        )
    );
    const leaderIndex = activeFlags.findIndex(Boolean);
    const followerIndex = leaderIndex === 0 ? 1 : 0;
    const leaderPage = pages[leaderIndex];
    const followerPage = pages[followerIndex];
    if (!leaderPage || !followerPage) throw new Error('realtime leader election failed');

    const callsBeforeSharedRefresh = state.generalMeCalls;
    state.generalName = '탭공유갱신장수';
    await leaderPage.evaluate(() => {
        (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }).__emitMainRealtime(
            'readModelChanged',
            {
                at: new Date().toISOString(),
                revision: 100,
                changes: {
                    generalIds: [7],
                    cityIds: [],
                    nationIds: [],
                    mapGeneralIds: [],
                    mapCityIds: [],
                    mapNationIds: [],
                    frontStatusGeneralIds: [],
                    frontStatusNationIds: [],
                    frontStatusActorIds: [],
                    frontStatusChanged: false,
                    lobbyGeneralIds: [],
                    lobbyChanged: false,
                    reservedGeneralIds: [],
                    recordGeneralIds: [],
                    worldChanged: false,
                    globalRecordsChanged: false,
                    worldHistoryChanged: false,
                    contactsChanged: false,
                },
            }
        );
    });
    await expect.poll(() => state.generalMeCalls).toBe(callsBeforeSharedRefresh + 1);
    await Promise.all(
        pages.map((currentPage) => expect(currentPage.locator('.general-title')).toContainText('탭공유갱신장수'))
    );

    await followerPage.getByRole('button', { name: /실시간 동기화/u }).click();
    await expect(followerPage.getByRole('button', { name: /실시간 동기화: 끔/u })).toBeVisible();
    const callsBeforeExcludedRefresh = state.generalMeCalls;
    state.generalName = '리더만갱신장수';
    await leaderPage.evaluate(() => {
        (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }).__emitMainRealtime(
            'readModelChanged',
            {
                at: new Date().toISOString(),
                revision: 101,
                changes: {
                    generalIds: [7],
                    cityIds: [],
                    nationIds: [],
                    mapGeneralIds: [],
                    mapCityIds: [],
                    mapNationIds: [],
                    frontStatusGeneralIds: [],
                    frontStatusNationIds: [],
                    frontStatusActorIds: [],
                    frontStatusChanged: false,
                    lobbyGeneralIds: [],
                    lobbyChanged: false,
                    reservedGeneralIds: [],
                    recordGeneralIds: [],
                    worldChanged: false,
                    globalRecordsChanged: false,
                    worldHistoryChanged: false,
                    contactsChanged: false,
                },
            }
        );
    });
    await expect.poll(() => state.generalMeCalls).toBe(callsBeforeExcludedRefresh + 1);
    await expect(leaderPage.locator('.general-title')).toContainText('리더만갱신장수');
    await expect(followerPage.locator('.general-title')).toContainText('탭공유갱신장수');

    await followerPage.getByRole('button', { name: /실시간 동기화: 끔/u }).click();
    await expect(followerPage.locator('.general-title')).toContainText('리더만갱신장수');
    await expect
        .poll(async () => {
            const flags = await Promise.all(
                pages.map((currentPage) =>
                    currentPage.evaluate(() =>
                        (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
                    )
                )
            );
            return flags.filter(Boolean).length;
        })
        .toBe(1);
});
