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

type OperationStatus = 'QUEUED' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED';
type Operation = {
    id: string;
    profileName: string;
    type: 'RESET' | 'DEPLOY' | 'START' | 'STOP' | 'CANCEL_GAME';
    status: OperationStatus;
    sourceMode?: 'BRANCH' | 'COMMIT';
    sourceRef?: string;
    resolvedCommitSha?: string;
    scheduledAt?: string;
    completedAt?: string;
    error?: string;
    payload: Record<string, unknown>;
    requestedBy: string;
    createdAt: string;
    updatedAt: string;
};

type LogEntry = {
    cursor: string;
    operationId: string;
    level: 'INFO' | 'OUTPUT' | 'ERROR';
    phase: string;
    message: string;
    createdAt: string;
};

type FixtureState = {
    operations: Operation[];
    gatewayOperations: Array<{
        id: string;
        type: 'DEPLOY' | 'ROLLBACK';
        status: OperationStatus;
        sourceMode?: 'BRANCH' | 'COMMIT';
        sourceRef?: string;
        resolvedCommitSha?: string;
        completedAt?: string;
        error?: string;
        payload: Record<string, unknown>;
        requestedBy: string;
        createdAt: string;
        updatedAt: string;
    }>;
    runtimeRunning: boolean;
    requestBodies: Array<{ operation: string; body: unknown }>;
    profileLogPollCount?: number;
    profileLogProgress?: boolean;
    profileLogsEmpty?: boolean;
    profileLogBatches?: LogEntry[][];
    profileLogPollGate?: (pollCount: number) => Promise<void>;
    gatewayLogPollCount?: number;
    gatewayLogsEmpty?: boolean;
    gatewayLogBatches?: LogEntry[][];
    gatewayLogPollGate?: (pollCount: number) => Promise<void>;
    gatewayStateFailuresAfterRequest?: number;
    gatewayStateFailuresRemaining?: number;
    gatewayStateFailureCount?: number;
    capabilities?: Array<{ permission: string; scope: 'GLOBAL' | 'PROFILE'; scopes: string[] }>;
    profileListDelayMs?: number;
    profileNavigationDelayMs?: number;
    profileNavigationRequests?: number;
    profileNavigationResolved?: boolean;
    scenarioFailuresRemaining?: number;
    resetDefaults?: Record<string, unknown>;
    updateMetaFails?: boolean;
    bulkBatches?: Array<{
        id: string;
        sourceMode: 'BRANCH' | 'COMMIT';
        sourceRef: string;
        resolvedCommitSha: string;
        requestedBy: string;
        createdAt: string;
        status: OperationStatus;
        targets: Array<{
            kind: 'GATEWAY' | 'PROFILE';
            order: number;
            label: string;
            profileName?: string;
            operationId: string;
            status: OperationStatus;
        }>;
    }>;
};

const profile = (runtimeRunning: boolean, resetDefaults?: Record<string, unknown>) => ({
    profileName: 'che:default',
    profile: 'che',
    instanceKey: 'default',
    displayName: '천하서버',
    currentScenario: '2',
    scenario: '2',
    apiPort: 15003,
    status: runtimeRunning ? 'RUNNING' : 'STOPPED',
    buildStatus: 'SUCCEEDED',
    buildCommitSha: '0123456789abcdef0123456789abcdef01234567',
    buildWorkspace: '/srv/sammo/worktrees/0123456789abcdef0123456789abcdef01234567',
    meta: resetDefaults ? { resetDefaults } : {},
    createdAt: '2026-07-25T00:00:00.000Z',
    updatedAt: '2026-07-25T00:00:00.000Z',
    activeOperation: null,
    runtimeActions: [],
    runtime: {
        profileName: 'che:default',
        frontendRunning: runtimeRunning,
        apiRunning: runtimeRunning,
        daemonRunning: runtimeRunning,
        auctionRunning: runtimeRunning,
        battleSimRunning: runtimeRunning,
        tournamentRunning: runtimeRunning,
    },
});

const scenarios = [
    {
        id: 0,
        title: '【테스트】공백지',
        year: null,
        npcCount: 0,
        npcExCount: 0,
        npcNeutralCount: 0,
        nations: [],
        isCurrent: false,
    },
    {
        id: 2,
        title: '【테스트】황건의 난',
        year: 184,
        npcCount: 42,
        npcExCount: 0,
        npcNeutralCount: 0,
        nations: [],
        isCurrent: true,
    },
    {
        id: 5,
        title: '【테스트】군웅할거',
        year: 190,
        npcCount: 55,
        npcExCount: 0,
        npcNeutralCount: 0,
        nations: [],
        isCurrent: false,
    },
    {
        id: 916,
        title: '【공백지】 여명',
        year: 180,
        npcCount: 0,
        npcExCount: 0,
        npcNeutralCount: 0,
        nations: [],
        isCurrent: false,
    },
];

const response = (data: unknown) => ({ result: { data } });
const operationNames = (route: Route): string[] => {
    const url = new URL(route.request().url());
    return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(',');
};

const installFixture = async (page: Page, state: FixtureState) => {
    await page.addInitScript(() => {
        window.localStorage.setItem('sammo-session-token', 'playwright-admin-session');
    });
    await page.route('**/gateway/api/trpc/**', async (route) => {
        const names = operationNames(route);
        const body = route.request().postDataJSON() as unknown;
        if (names.includes('admin.profiles.listNavigation')) {
            expect(names).toEqual(['admin.profiles.listNavigation']);
            state.profileNavigationRequests = (state.profileNavigationRequests ?? 0) + 1;
            if (state.profileNavigationDelayMs) {
                await new Promise((resolve) => setTimeout(resolve, state.profileNavigationDelayMs));
            }
            state.profileNavigationResolved = true;
        }
        if (names.includes('admin.profiles.list') && state.profileListDelayMs) {
            await new Promise((resolve) => setTimeout(resolve, state.profileListDelayMs));
        }
        if (names.includes('admin.profiles.listScenarios') && (state.scenarioFailuresRemaining ?? 0) > 0) {
            state.scenarioFailuresRemaining = (state.scenarioFailuresRemaining ?? 0) - 1;
            await route.abort('failed');
            return;
        }
        if (names.includes('admin.profiles.updateMeta') && state.updateMetaFails) {
            await route.abort('failed');
            return;
        }
        if (names.includes('admin.releases.gatewayState') && (state.gatewayStateFailuresRemaining ?? 0) > 0) {
            state.gatewayStateFailuresRemaining = (state.gatewayStateFailuresRemaining ?? 0) - 1;
            state.gatewayStateFailureCount = (state.gatewayStateFailureCount ?? 0) + 1;
            await route.fulfill({
                status: 502,
                contentType: 'application/json',
                body: '',
            });
            return;
        }
        if (names.includes('admin.operations.logs') && !state.profileLogProgress) {
            await new Promise((resolve) => setTimeout(resolve, 50));
        }
        if (names.includes('admin.operations.logs') && state.profileLogPollGate) {
            await state.profileLogPollGate((state.profileLogPollCount ?? 0) + 1);
        }
        if (names.includes('admin.releases.logs') && state.gatewayLogPollGate) {
            await state.gatewayLogPollGate((state.gatewayLogPollCount ?? 0) + 1);
        }
        const results = names.map((name) => {
            state.requestBodies.push({ operation: name, body });
            if (name === 'admin.profiles.list') {
                return response([profile(state.runtimeRunning, state.resetDefaults)]);
            }
            if (name === 'admin.profiles.listNavigation') {
                return response([
                    {
                        profileName: 'che:default',
                        profile: 'che',
                        instanceKey: 'default',
                        displayName: '천하서버',
                        currentScenario: '2',
                        meta: { korName: '천하서버' },
                    },
                ]);
            }
            if (name === 'admin.capabilities.list') {
                return response(
                    state.capabilities ?? [
                        { permission: 'admin.profiles.runtime', scope: 'PROFILE', scopes: ['*'] },
                        { permission: 'admin.profiles.settings', scope: 'PROFILE', scopes: ['*'] },
                        { permission: 'admin.profiles.deploy', scope: 'PROFILE', scopes: ['*'] },
                        { permission: 'admin.scenarios.reset', scope: 'PROFILE', scopes: ['*'] },
                        { permission: 'admin.reset.schedule', scope: 'PROFILE', scopes: ['*'] },
                        { permission: 'admin.releases.manage', scope: 'GLOBAL', scopes: ['*'] },
                    ]
                );
            }
            if (name === 'admin.bulkReleases.targets') {
                return response({
                    gateway: true,
                    profiles: [
                        {
                            profileName: 'che:default',
                            displayName: '체',
                            status: 'RUNNING',
                            currentScenario: '2',
                            buildCommitSha: '0123456789abcdef0123456789abcdef01234567',
                            activeOperation: null,
                        },
                        {
                            profileName: 'hwe:default',
                            displayName: '환상',
                            status: 'RUNNING',
                            currentScenario: '1010',
                            buildCommitSha: 'fedcba9876543210fedcba9876543210fedcba98',
                            activeOperation: {
                                id: '99999999-9999-4999-8999-999999999999',
                                type: 'DEPLOY',
                                status: 'RUNNING',
                            },
                        },
                    ],
                });
            }
            if (name === 'admin.bulkReleases.list') {
                return response(state.bulkBatches ?? []);
            }
            if (name === 'admin.bulkReleases.request') {
                const batch = {
                    id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
                    sourceMode: 'BRANCH' as const,
                    sourceRef: 'main',
                    resolvedCommitSha: '1234567890abcdef1234567890abcdef12345678',
                    requestedBy: 'admin',
                    createdAt: '2026-08-25T01:00:00.000Z',
                    status: 'QUEUED' as const,
                    targets: [
                        {
                            kind: 'GATEWAY' as const,
                            order: 0,
                            label: 'Gateway',
                            operationId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
                            status: 'QUEUED' as const,
                        },
                        {
                            kind: 'PROFILE' as const,
                            order: 1,
                            label: '체',
                            profileName: 'che:default',
                            operationId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
                            status: 'QUEUED' as const,
                        },
                    ],
                };
                state.bulkBatches = [batch];
                return response({ id: batch.id, resolvedCommitSha: batch.resolvedCommitSha, targetCount: 2 });
            }
            if (name === 'admin.operations.list') {
                return response(state.operations);
            }
            if (name === 'admin.operations.logs') {
                const operation = state.operations[0];
                if (!operation) throw new Error('Profile operation fixture is missing');
                state.profileLogPollCount = (state.profileLogPollCount ?? 0) + 1;
                if (state.profileLogBatches) {
                    const entries = state.profileLogBatches[state.profileLogPollCount - 1] ?? [];
                    const completed = state.profileLogPollCount >= state.profileLogBatches.length;
                    const nextOperation = {
                        ...operation,
                        status: completed ? ('SUCCEEDED' as const) : ('RUNNING' as const),
                    };
                    state.operations[0] = nextOperation;
                    return response({
                        operation: nextOperation,
                        entries,
                        nextCursor: entries.at(-1)?.cursor,
                    });
                }
                if (['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(operation.status)) {
                    return response({ operation, entries: [] });
                }
                if (!state.profileLogProgress) {
                    return response({ operation, entries: [] });
                }
                if (state.profileLogsEmpty) {
                    return response({ operation, entries: [] });
                }
                const completed = state.profileLogPollCount > 1;
                const nextOperation = {
                    ...operation,
                    status: completed ? ('SUCCEEDED' as const) : ('RUNNING' as const),
                };
                if (completed) state.operations[0] = nextOperation;
                return response({
                    operation: nextOperation,
                    entries: completed
                        ? [
                              {
                                  cursor: '2',
                                  operationId: operation.id,
                                  level: 'OUTPUT',
                                  phase: operation.type === 'RESET' ? 'seed' : 'build',
                                  message:
                                      operation.type === 'RESET'
                                          ? '시나리오 초기 데이터 생성을 완료했습니다.'
                                          : 'game-frontend build complete',
                                  createdAt: '2026-08-01T01:00:02.000Z',
                              },
                          ]
                        : [
                              {
                                  cursor: '1',
                                  operationId: operation.id,
                                  level: 'INFO',
                                  phase: 'build',
                                  message: `${operation.profileName} 구성 요소를 빌드합니다.`,
                                  createdAt: '2026-08-01T01:00:01.000Z',
                              },
                          ],
                    nextCursor: completed ? '2' : '1',
                });
            }
            if (name === 'admin.releases.gatewayState') {
                return response({
                    id: 'gateway',
                    activeCommitSha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
                    activeWorkspace: '/srv/sammo/current',
                    previousCommitSha: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
                    previousWorkspace: '/srv/sammo/previous',
                    updatedAt: '2026-08-01T00:00:00.000Z',
                });
            }
            if (name === 'admin.releases.list') {
                return response(state.gatewayOperations);
            }
            if (name === 'admin.releases.logs') {
                const releaseOperation = state.gatewayOperations[0];
                if (!releaseOperation) throw new Error('Release operation fixture is missing');
                state.gatewayLogPollCount = (state.gatewayLogPollCount ?? 0) + 1;
                if (state.gatewayLogBatches) {
                    const entries = state.gatewayLogBatches[state.gatewayLogPollCount - 1] ?? [];
                    const completed = state.gatewayLogPollCount >= state.gatewayLogBatches.length;
                    const nextOperation = {
                        ...releaseOperation,
                        status: completed ? ('SUCCEEDED' as const) : ('RUNNING' as const),
                    };
                    state.gatewayOperations[0] = nextOperation;
                    return response({
                        operation: nextOperation,
                        entries,
                        nextCursor: entries.at(-1)?.cursor,
                    });
                }
                if (state.gatewayLogsEmpty) {
                    return response({ operation: releaseOperation, entries: [] });
                }
                const completed = state.gatewayLogPollCount > 1;
                return response({
                    operation: { ...releaseOperation, status: completed ? 'SUCCEEDED' : 'RUNNING' },
                    entries: completed
                        ? [
                              {
                                  cursor: '2',
                                  operationId: releaseOperation.id,
                                  level: 'OUTPUT',
                                  phase: 'build',
                                  message: 'gateway-frontend build complete',
                                  createdAt: '2026-08-01T02:00:02.000Z',
                              },
                          ]
                        : [
                              {
                                  cursor: '1',
                                  operationId: releaseOperation.id,
                                  level: 'INFO',
                                  phase: 'build',
                                  message: 'Gateway 구성 요소를 빌드합니다.',
                                  createdAt: '2026-08-01T02:00:01.000Z',
                              },
                          ],
                    nextCursor: completed ? '2' : '1',
                });
            }
            if (name === 'admin.profiles.listScenarios') {
                expect(names).toEqual(['admin.profiles.listScenarios']);
                return response(scenarios);
            }
            if (name === 'admin.profiles.getResetDefaults') {
                return response({
                    source: state.resetDefaults ? 'PROFILE' : 'SYSTEM',
                    defaults: state.resetDefaults ?? {
                        turnTermMinutes: 60,
                        sync: true,
                        fiction: 1,
                        extend: true,
                        blockGeneralCreate: 0,
                        npcMode: 0,
                        showImgLevel: 3,
                        tournamentTrig: true,
                        joinMode: 'full',
                        autorunUser: null,
                    },
                });
            }
            if (name === 'admin.profiles.updateMeta') {
                return response(profile(state.runtimeRunning, state.resetDefaults));
            }
            if (name === 'admin.operations.requestReset') {
                const operation: Operation = {
                    id: '11111111-1111-4111-8111-111111111111',
                    profileName: 'che:default',
                    type: 'RESET',
                    status: 'QUEUED',
                    sourceMode: 'COMMIT',
                    sourceRef: '0123456789abcdef0123456789abcdef01234567',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-07-25T02:00:00.000Z',
                    updatedAt: '2026-07-25T02:00:00.000Z',
                };
                state.operations = [operation, ...state.operations];
                return response(operation);
            }
            if (name === 'admin.operations.requestDeploy') {
                const operation: Operation = {
                    id: '66666666-6666-4666-8666-666666666666',
                    profileName: 'che:default',
                    type: 'DEPLOY',
                    status: 'QUEUED',
                    sourceMode: 'BRANCH',
                    sourceRef: 'main',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-08-01T01:00:00.000Z',
                    updatedAt: '2026-08-01T01:00:00.000Z',
                };
                state.operations = [operation, ...state.operations];
                return response(operation);
            }
            if (name === 'admin.operations.requestGameCancellation') {
                const operation: Operation = {
                    id: '88888888-8888-4888-8888-888888888888',
                    profileName: 'che:default',
                    type: 'CANCEL_GAME',
                    status: 'QUEUED',
                    sourceMode: 'COMMIT',
                    sourceRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-08-18T01:00:00.000Z',
                    updatedAt: '2026-08-18T01:00:00.000Z',
                };
                state.operations = [operation];
                return response(operation);
            }
            if (name === 'admin.releases.requestGatewayDeploy' || name === 'admin.releases.requestGatewayRollback') {
                const releaseOperation = {
                    id: '77777777-7777-4777-8777-777777777777',
                    type: name.endsWith('Rollback') ? ('ROLLBACK' as const) : ('DEPLOY' as const),
                    status: 'QUEUED' as const,
                    sourceMode: 'BRANCH' as const,
                    sourceRef: 'main',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-08-01T02:00:00.000Z',
                    updatedAt: '2026-08-01T02:00:00.000Z',
                };
                state.gatewayOperations = [releaseOperation];
                state.gatewayStateFailuresRemaining = state.gatewayStateFailuresAfterRequest;
                return response(releaseOperation);
            }
            if (name === 'admin.operations.requestRuntime') {
                const serialized = JSON.stringify(body);
                const type = serialized.includes('"STOP"') ? 'STOP' : 'START';
                state.runtimeRunning = type === 'START';
                const operation: Operation = {
                    id:
                        type === 'START'
                            ? '22222222-2222-4222-8222-222222222222'
                            : '33333333-3333-4333-8333-333333333333',
                    profileName: 'che:default',
                    type,
                    status: 'SUCCEEDED',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-07-25T03:00:00.000Z',
                    updatedAt: '2026-07-25T03:00:00.000Z',
                };
                state.operations = [operation];
                return response(operation);
            }
            if (name === 'admin.operations.retry') {
                const operation: Operation = {
                    id: '44444444-4444-4444-8444-444444444444',
                    profileName: 'che:default',
                    type: 'RESET',
                    status: 'QUEUED',
                    sourceMode: 'COMMIT',
                    sourceRef: 'fedcba9876543210fedcba9876543210fedcba98',
                    payload: { installOperationId: 'failed-generation' },
                    requestedBy: 'admin',
                    createdAt: '2026-07-25T04:00:00.000Z',
                    updatedAt: '2026-07-25T04:00:00.000Z',
                };
                state.operations = [operation, ...state.operations];
                return response(operation);
            }
            if (name === 'admin.operations.cancel') {
                const id = JSON.stringify(body).match(/[0-9a-f]{8}-[0-9a-f-]{27,}/u)?.[0];
                state.operations = state.operations.map((operation) =>
                    operation.id === id ? { ...operation, status: 'CANCELLED' as const } : operation
                );
                return response({ ok: true });
            }
            if (name === 'admin.releases.cancel') {
                const id = JSON.stringify(body).match(/[0-9a-f]{8}-[0-9a-f-]{27,}/u)?.[0];
                state.gatewayOperations = state.gatewayOperations.map((operation) =>
                    operation.id === id ? { ...operation, status: 'CANCELLED' as const } : operation
                );
                return response({ ok: true });
            }
            if (name === 'admin.releases.retry') {
                const previous = state.gatewayOperations[0];
                if (!previous) throw new Error('Release retry fixture is missing');
                const retried = {
                    ...previous,
                    id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
                    status: 'QUEUED' as const,
                    sourceMode: 'COMMIT' as const,
                    sourceRef: previous.resolvedCommitSha ?? previous.sourceRef,
                };
                state.gatewayOperations = [retried, ...state.gatewayOperations];
                return response(retried);
            }
            throw new Error(`Unhandled tRPC operation: ${name}`);
        });
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(
                new URL(route.request().url()).searchParams.get('batch') === '1' ? results : results[0]
            ),
        });
    });
};

const makeLogEntries = (operationId: string, prefix: string, startCursor: number, count: number): LogEntry[] =>
    Array.from({ length: count }, (_, index) => {
        const cursor = startCursor + index;
        return {
            cursor: String(cursor),
            operationId,
            level: 'OUTPUT',
            phase: 'build',
            message: `${prefix} ${cursor}`,
            createdAt: '2026-08-01T01:00:00.000Z',
        };
    });

const deferred = () => {
    let resolve!: () => void;
    const promise = new Promise<void>((done) => {
        resolve = done;
    });
    return { promise, resolve };
};

test('selects authorized Gateway and profile targets and registers one pinned bulk update', async ({
    page,
}, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/releases/batch');
    await expect(page.getByRole('heading', { name: '일괄 업데이트', level: 1 })).toBeVisible();
    await expect(page.getByTestId('bulk-target-gateway')).toBeEnabled();
    await expect(page.getByTestId('bulk-target-che:default')).toBeEnabled();
    await expect(page.getByTestId('bulk-target-hwe:default')).toBeDisabled();
    await page.getByTestId('bulk-target-gateway').check();
    await page.getByTestId('bulk-target-che:default').check();
    await page.getByTestId('submit-bulk-release').click();

    await expect(page.getByText('1234567890ab', { exact: true })).toBeVisible();
    await expect(page.getByText('Gateway', { exact: true }).last()).toBeVisible();
    await expect(page.getByText('체', { exact: true }).last()).toBeVisible();
    const request = state.requestBodies.find((entry) => entry.operation === 'admin.bulkReleases.request');
    expect(JSON.stringify(request?.body)).toContain('che:default');
    expect(JSON.stringify(request?.body)).toContain('includeGateway');

    const formBox = await page.getByTestId('bulk-release-form').boundingBox();
    expect(formBox?.width ?? 0).toBeGreaterThan(700);
    await page.screenshot({ path: testInfo.outputPath('bulk-release-desktop.png'), fullPage: true });
});

test('keeps bulk target selection and progress readable on a 390px mobile viewport', async ({ page }, testInfo) => {
    await page.setViewportSize({ width: 390, height: 844 });
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        bulkBatches: [
            {
                id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
                sourceMode: 'COMMIT',
                sourceRef: '1234567890abcdef1234567890abcdef12345678',
                resolvedCommitSha: '1234567890abcdef1234567890abcdef12345678',
                requestedBy: 'admin',
                createdAt: '2026-08-25T01:00:00.000Z',
                status: 'FAILED',
                targets: [
                    {
                        kind: 'GATEWAY',
                        order: 0,
                        label: 'Gateway',
                        operationId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
                        status: 'SUCCEEDED',
                    },
                    {
                        kind: 'PROFILE',
                        order: 1,
                        label: '체',
                        profileName: 'che:default',
                        operationId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
                        status: 'FAILED',
                    },
                ],
            },
        ],
    };
    await installFixture(page, state);
    await page.goto('admin/releases/batch');
    await page.getByText('1234567890ab', { exact: true }).click();
    await expect(page.getByRole('button', { name: '재시도' })).toBeVisible();

    const metrics = await page.evaluate(() => ({
        scrollWidth: document.documentElement.scrollWidth,
        clientWidth: document.documentElement.clientWidth,
        checkboxSize: getComputedStyle(document.querySelector<HTMLInputElement>('[data-testid="bulk-target-gateway"]')!)
            .width,
    }));
    expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth);
    expect(Number.parseFloat(metrics.checkboxSize)).toBeGreaterThanOrEqual(18);
    await page.screenshot({ path: testInfo.outputPath('bulk-release-mobile.png'), fullPage: true });
});

test('separates branch and commit semantics and submits a reset from the dedicated page', async ({
    page,
}, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: false,
        requestBodies: [],
        profileLogProgress: true,
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/servers/che%3Adefault/scenario');
    await expect(page.getByTestId('server-operations-page')).toBeVisible();
    await expect(page).toHaveURL(/\/gateway\/admin\/servers\/che%3Adefault\/scenario$/);
    await expect(page.getByTestId('source-current')).toBeChecked();
    await expect(page.getByTestId('source-help')).toContainText('브랜치를 추적하면 작업 시작 시 최신 커밋');
    await expect(page.getByTestId('scenario-select')).toHaveValue('2');
    await expect(page.getByTestId('request-reset')).toBeEnabled();
    await expect(page.getByTestId('scenario-select').locator('option:checked')).toContainText('현재 시나리오');
    await expect(page.getByTestId('scenario-select').locator('option[value="916"]')).toContainText('【공백지】 여명');
    const catalogGeometry = await page.getByTestId('scenario-select').evaluate((select) => {
        const scenarioSelect = select as HTMLSelectElement;
        const rect = select.getBoundingClientRect();
        return {
            x: rect.x,
            y: rect.y,
            width: rect.width,
            height: rect.height,
            optionCount: scenarioSelect.options.length,
            value: scenarioSelect.value,
        };
    });
    expect(catalogGeometry.optionCount).toBe(4);
    expect(catalogGeometry.value).toBe('2');
    expect(catalogGeometry.width).toBeGreaterThan(300);
    await page.screenshot({ path: testInfo.outputPath('current-scenario-catalog.png'), fullPage: true });
    await page.getByTestId('scenario-select').selectOption('0');
    await expect(page.getByTestId('request-reset')).toBeEnabled();
    await expect(page.getByTestId('server-profile-tabs')).toBeVisible();
    await expect(page.getByRole('link', { name: '시나리오 초기화', exact: true })).toHaveAttribute(
        'aria-current',
        'page'
    );
    await expect(page.getByText('운영 프로필', { exact: true })).toHaveCount(0);

    const desktopGeometry = await page
        .getByTestId('server-operations-page')
        .locator('section')
        .first()
        .evaluate((section) => {
            const children = Array.from(section.children).map((child) => {
                const rect = child.getBoundingClientRect();
                return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
            });
            return children;
        });
    expect(desktopGeometry).toHaveLength(1);
    expect(desktopGeometry[0]!.width).toBeGreaterThan(800);
    await page.getByTestId('source-commit').check();
    const sourceInput = page.getByTestId('source-ref');
    await sourceInput.focus();
    const focusedInputStyle = await sourceInput.evaluate((element) => {
        const style = getComputedStyle(element);
        return {
            borderColor: style.borderColor,
            backgroundColor: style.backgroundColor,
            color: style.color,
            fontSize: style.fontSize,
            lineHeight: style.lineHeight,
            outlineStyle: style.outlineStyle,
        };
    });
    await writeFile(
        testInfo.outputPath('layout-metrics.json'),
        JSON.stringify({ catalogGeometry, desktopGeometry, focusedInputStyle }, null, 2)
    );
    await page.screenshot({ path: testInfo.outputPath('desktop-operations.png'), fullPage: true });

    await expect(page.getByTestId('source-help')).toContainText('전체 SHA로 고정');
    await page.getByTestId('source-ref').fill('0123456789abcdef0123456789abcdef01234567');
    await page.getByTestId('load-scenarios').click();
    await page.getByTestId('scenario-select').selectOption('5');
    await expect(page.getByText('초기화 시작 → 가오픈 시작 → 정식 오픈 순서입니다.')).toBeVisible();
    await page.getByTestId('reset-scheduled-at').fill('2030-08-13T09:30');
    await page.getByTestId('reset-preopen-at').fill('2030-08-13T10:00');
    await page.getByTestId('reset-open-at').fill('2030-08-13T11:00');
    const publishSchedule = page.getByTestId('reset-publish-schedule');
    await publishSchedule.check();
    await page.getByTestId('reset-open-at').focus();
    await page.keyboard.press('Tab');
    await publishSchedule.focus();
    await expect(publishSchedule).toBeFocused();
    const publishFocusStyle = await publishSchedule.evaluate((element) => {
        const style = getComputedStyle(element);
        return { style: style.outlineStyle, width: Number.parseFloat(style.outlineWidth) };
    });
    expect(publishFocusStyle.style).not.toBe('none');
    expect(publishFocusStyle.width).toBeGreaterThanOrEqual(2);
    await expect(page.getByText('빌드는 초기화 시작 시각까지 대기합니다.')).toBeVisible();
    const scheduledHelp = page.getByTestId('reset-help-scheduled-at');
    await scheduledHelp.hover();
    await expect(page.getByTestId('reset-help-scheduled-at-tooltip')).toContainText(
        '완료되어도 가오픈 전에는 접속을 차단합니다.'
    );
    await page.getByTestId('request-reset').hover();
    await page.getByTestId('request-reset').click();

    await expect(page.getByText('초기화 작업을 등록했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('operations-table')).toContainText('시나리오 초기화');
    await expect(page.getByTestId('profile-operation-log-panel')).toBeVisible();
    await expect(page.getByTestId('profile-operation-log')).toContainText('천하서버 구성 요소를 빌드합니다.');
    await expect(page.getByTestId('profile-operation-log')).toContainText('시나리오 초기 데이터 생성을 완료했습니다.');
    await expect(page.getByTestId('profile-operation-log-status')).toContainText('SUCCEEDED');
    const operationTable = page.getByTestId('operations-table');
    await expect(operationTable.getByRole('columnheader')).toHaveText(['요청 · 작업', '상태', '보기']);
    await expect(operationTable.getByText('시나리오 초기화', { exact: true })).toBeVisible();
    await expect(operationTable.getByText('완료', { exact: true })).toBeVisible();
    await expect(operationTable.getByText('che:default', { exact: true })).toBeHidden();
    const detailsToggle = operationTable.getByTestId('operation-details-toggle');
    await expect(detailsToggle).toHaveAttribute('aria-expanded', 'false');
    await detailsToggle.click();
    await expect(detailsToggle).toHaveAttribute('aria-expanded', 'true');
    const operationDetail = operationTable.getByTestId('operation-detail');
    await expect(operationDetail).toContainText('천하서버');
    await expect(operationDetail).not.toContainText('che:default');
    await expect(operationDetail).toContainText('0123456789abcdef0123456789abcdef01234567');
    await expect(operationDetail).toContainText('admin');
    const operationTableGeometry = await page.getByTestId('operations-table').evaluate((table) => {
        const columnWidths = Array.from(table.querySelectorAll('thead th')).map(
            (heading) => heading.getBoundingClientRect().width
        );
        const summaryRowHeight = table
            .querySelector('[data-testid="operation-summary-row"]')
            ?.getBoundingClientRect().height;
        return {
            columnWidths,
            summaryRowHeight,
            tableWidth: table.getBoundingClientRect().width,
            scrollerWidth: table.parentElement?.getBoundingClientRect().width ?? 0,
            scrollerScrollWidth: table.parentElement?.scrollWidth ?? 0,
            tableLayout: getComputedStyle(table).tableLayout,
        };
    });
    const sourceRefGeometry = await page.getByTestId('operation-source-ref').evaluate((element) => {
        const style = getComputedStyle(element);
        return {
            text: element.textContent?.trim(),
            overflow: style.overflow,
            whiteSpace: style.whiteSpace,
        };
    });
    expect(operationTableGeometry.tableLayout).toBe('fixed');
    expect(operationTableGeometry.columnWidths).toHaveLength(3);
    expect(operationTableGeometry.tableWidth).toBeLessThanOrEqual(operationTableGeometry.scrollerWidth + 1);
    expect(operationTableGeometry.scrollerScrollWidth).toBeLessThanOrEqual(operationTableGeometry.scrollerWidth + 1);
    expect(operationTableGeometry.columnWidths[0]).toBeGreaterThan(operationTableGeometry.columnWidths[1]!);
    expect(operationTableGeometry.columnWidths[2]).toBeGreaterThan(operationTableGeometry.columnWidths[1]!);
    expect(operationTableGeometry.summaryRowHeight).toBeLessThanOrEqual(90);
    expect(sourceRefGeometry).toEqual({
        text: 'COMMIT 0123456789abcdef0123456789abcdef01234567',
        overflow: 'visible',
        whiteSpace: 'normal',
    });
    await writeFile(
        testInfo.outputPath('operation-table-metrics.json'),
        JSON.stringify({ operationTableGeometry, sourceRefGeometry }, null, 2)
    );
    const resetRequest = state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestReset');
    expect(JSON.stringify(resetRequest?.body)).toContain('"sourceMode":"COMMIT"');
    expect(JSON.stringify(resetRequest?.body)).toContain('0123456789abcdef0123456789abcdef01234567');
    expect(JSON.stringify(resetRequest?.body)).toContain('"scenarioId":5');
    expect(JSON.stringify(resetRequest?.body)).toContain('"scheduledAt":"2030-08-13T00:30:00.000Z"');
    expect(JSON.stringify(resetRequest?.body)).toContain('"publishSchedule":true');
    expect(JSON.stringify(resetRequest?.body)).toContain('"preopenAt":"2030-08-13T01:00:00.000Z"');
    expect(JSON.stringify(resetRequest?.body)).toContain('"openAt":"2030-08-13T02:00:00.000Z"');
    await page.screenshot({ path: testInfo.outputPath('reset-operation-log-desktop.png'), fullPage: true });

    await page.setViewportSize({ width: 500, height: 844 });
    const mobileGeometry = await page
        .getByTestId('server-operations-page')
        .locator('section')
        .first()
        .evaluate((section) => {
            const children = Array.from(section.children).map((child) => {
                const rect = child.getBoundingClientRect();
                return { x: rect.x, y: rect.y, width: rect.width };
            });
            return children;
        });
    expect(mobileGeometry[0]!.width).toBeLessThanOrEqual(500);
    const mobilePublishGeometry = await publishSchedule.evaluate((element) => {
        const label = element.closest('label');
        if (!label) throw new Error('expected publish schedule label');
        const rect = label.getBoundingClientRect();
        return {
            left: rect.left,
            right: rect.right,
            documentWidth: document.documentElement.scrollWidth,
            viewportWidth: window.innerWidth,
        };
    });
    expect(mobilePublishGeometry.left).toBeGreaterThanOrEqual(0);
    expect(mobilePublishGeometry.right).toBeLessThanOrEqual(mobilePublishGeometry.viewportWidth);
    expect(mobilePublishGeometry.documentWidth).toBe(mobilePublishGeometry.viewportWidth);
    const mobileTabs = await page
        .getByTestId('server-profile-tabs')
        .locator('a')
        .evaluateAll((links) =>
            links.map((link) => {
                const rect = link.getBoundingClientRect();
                return { top: rect.top, width: rect.width, height: rect.height };
            })
        );
    expect(mobileTabs).toHaveLength(3);
    expect(mobileTabs[1]!.top).toBeGreaterThan(mobileTabs[0]!.top);
    expect(mobileTabs.every((tab) => tab.height >= 44)).toBe(true);
    const mobileOperationTableGeometry = await page.getByTestId('operations-table').evaluate((table) => {
        const scroller = table.parentElement!;
        const scrollerRect = scroller.getBoundingClientRect();
        const detailRect = table.querySelector('[data-testid="operation-detail"]')?.getBoundingClientRect();
        return {
            tableWidth: table.getBoundingClientRect().width,
            scrollerX: scrollerRect.x,
            scrollerWidth: scrollerRect.width,
            scrollerScrollWidth: scroller.scrollWidth,
            detailX: detailRect?.x,
            detailRight: detailRect?.right,
            viewportWidth: document.documentElement.clientWidth,
            documentScrollWidth: document.documentElement.scrollWidth,
        };
    });
    expect(mobileOperationTableGeometry.tableWidth).toBeLessThanOrEqual(mobileOperationTableGeometry.scrollerWidth + 1);
    expect(mobileOperationTableGeometry.scrollerScrollWidth).toBeLessThanOrEqual(
        mobileOperationTableGeometry.scrollerWidth + 1
    );
    expect(mobileOperationTableGeometry.scrollerX).toBeGreaterThanOrEqual(0);
    expect(mobileOperationTableGeometry.scrollerX + mobileOperationTableGeometry.scrollerWidth).toBeLessThanOrEqual(
        mobileOperationTableGeometry.viewportWidth
    );
    expect(mobileOperationTableGeometry.documentScrollWidth).toBeLessThanOrEqual(
        mobileOperationTableGeometry.viewportWidth
    );
    expect(mobileOperationTableGeometry.detailX).toBeGreaterThanOrEqual(mobileOperationTableGeometry.scrollerX);
    expect(mobileOperationTableGeometry.detailRight).toBeLessThanOrEqual(
        mobileOperationTableGeometry.scrollerX + mobileOperationTableGeometry.scrollerWidth
    );
    await page.screenshot({ path: testInfo.outputPath('mobile-operation-detail.png'), fullPage: true });
    await detailsToggle.click();
    await expect(operationDetail).toBeHidden();
    await writeFile(
        testInfo.outputPath('operation-table-mobile-metrics.json'),
        JSON.stringify(mobileOperationTableGeometry, null, 2)
    );
    await page.screenshot({ path: testInfo.outputPath('mobile-operations.png'), fullPage: true });
});

test('separates DB-preserving profile deployment from DB reset', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        profileLogProgress: true,
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/servers/che%3Adefault/version');
    await expect(page.getByRole('heading', { name: 'DB 보존 버전 업데이트' })).toBeVisible();
    await expect(page.getByText('운영 프로필', { exact: true })).toHaveCount(0);
    await expect(page.getByRole('link', { name: '버전 업데이트', exact: true })).toHaveAttribute(
        'aria-current',
        'page'
    );
    await page.getByTestId('request-deploy').click();

    await expect(page.getByText('DB 보존 배포 작업을 등록했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('operations-table')).toContainText('버전 업데이트');
    await expect(page.getByTestId('profile-operation-log-panel')).toBeVisible();
    await expect(page.getByTestId('profile-operation-log')).toContainText('천하서버 구성 요소를 빌드합니다.');
    await expect(page.getByTestId('profile-operation-log')).toContainText('game-frontend build complete');
    await expect(page.getByTestId('profile-operation-log-status')).toContainText('SUCCEEDED');
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestDeploy')).toBe(true);
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestReset')).toBe(false);
});

test('keeps a future scenario reset reserved while submitting one interim DB-preserving deploy', async ({
    page,
}, testInfo) => {
    const scheduledReset: Operation = {
        id: '99999999-9999-4999-8999-999999999999',
        profileName: 'che:default',
        type: 'RESET',
        status: 'QUEUED',
        sourceMode: 'BRANCH',
        sourceRef: 'main',
        scheduledAt: '2099-08-27T05:00:00.000Z',
        payload: {},
        requestedBy: 'admin',
        createdAt: '2026-08-24T00:00:00.000Z',
        updatedAt: '2026-08-24T00:00:00.000Z',
    };
    const state: FixtureState = {
        operations: [scheduledReset],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
    };
    await installFixture(page, state);
    const confirmations: string[] = [];
    page.on('dialog', async (dialog) => {
        confirmations.push(dialog.message());
        await dialog.accept();
    });

    await page.goto('admin/servers/che%3Adefault/version');
    const notice = page.getByTestId('interim-deploy-notice');
    await expect(notice).toContainText('예약 초기화 유지');
    await expect(notice).toContainText('시나리오 초기화 예약은 취소되지 않습니다.');
    await expect(page.getByTestId('request-deploy')).toBeEnabled();
    await page.getByTestId('request-deploy').click();

    await expect(page.getByText('DB 보존 배포 작업을 등록했습니다.').first()).toBeVisible();
    expect(confirmations).toHaveLength(1);
    expect(confirmations[0]).toContain('예약된 시나리오 초기화');
    expect(confirmations[0]).toContain('자동 취소됩니다.');
    expect(state.operations).toHaveLength(2);
    expect(state.operations).toContainEqual(scheduledReset);
    expect(state.requestBodies.filter((entry) => entry.operation === 'admin.operations.requestDeploy')).toHaveLength(1);
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestReset')).toBe(false);

    await page.setViewportSize({ width: 390, height: 844 });
    const mobileGeometry = await notice.evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return {
            left: rect.left,
            right: rect.right,
            viewportWidth: document.documentElement.clientWidth,
            documentScrollWidth: document.documentElement.scrollWidth,
        };
    });
    expect(mobileGeometry.left).toBeGreaterThanOrEqual(0);
    expect(mobileGeometry.right).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
    expect(mobileGeometry.documentScrollWidth).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
    await page.screenshot({ path: testInfo.outputPath('interim-deploy-mobile.png'), fullPage: true });
});

test('submits a separately authorized destructive game cancellation on desktop and mobile', async ({
    page,
}, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        profileLogProgress: true,
        capabilities: [{ permission: 'admin.games.cancel', scope: 'PROFILE', scopes: ['che:default'] }],
    };
    await installFixture(page, state);
    const confirmations: string[] = [];
    page.on('dialog', async (dialog) => {
        confirmations.push(dialog.message());
        await dialog.accept();
    });

    await page.goto('admin/servers/che%3Adefault/cancel');
    await expect(page).toHaveURL(/\/gateway\/admin\/servers\/che%3Adefault\/cancel$/);
    await expect(page.getByRole('heading', { name: '천하서버 게임 취소' })).toBeVisible();
    await expect(page.getByRole('link', { name: '게임 취소', exact: true })).toHaveAttribute('aria-current', 'page');
    await expect(page.getByRole('link', { name: '시나리오 초기화', exact: true })).toHaveCount(0);
    await expect(page.getByTestId('request-game-cancellation')).toBeDisabled();

    await page.getByTestId('cancellation-history-mode').selectOption('DELETE');
    await page.getByTestId('cancellation-general-mode').selectOption('RETAIN');
    await page.getByTestId('cancellation-retention-percent').fill('35');
    await page.getByTestId('cancellation-reason').fill('잘못된 시나리오로 개장함');
    await page.getByTestId('cancellation-confirmation').fill('천하서버 게임 취소');
    const cancelButton = page.getByTestId('request-game-cancellation');
    await expect(cancelButton).toBeEnabled();
    await cancelButton.hover();
    const desktopMetrics = await page.getByTestId('game-cancellation-form').evaluate((form) => {
        const rect = form.getBoundingClientRect();
        const style = getComputedStyle(form);
        const controls = Array.from(form.querySelectorAll('select, input:not([type="range"]), textarea, button')).map(
            (control) => {
                const controlRect = control.getBoundingClientRect();
                return { width: controlRect.width, height: controlRect.height };
            }
        );
        return {
            x: rect.x,
            width: rect.width,
            borderColor: style.borderColor,
            backgroundColor: style.backgroundColor,
            minimumControlHeight: Math.min(...controls.map((control) => control.height)),
        };
    });
    expect(desktopMetrics.width).toBeGreaterThan(800);
    expect(desktopMetrics.minimumControlHeight).toBeGreaterThanOrEqual(21);
    await page.screenshot({ path: testInfo.outputPath('game-cancellation-desktop.png'), fullPage: true });

    await cancelButton.click();
    await expect(page.getByText('게임 취소 작업을 등록했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('operations-table')).toContainText('게임 취소');
    expect(confirmations).toHaveLength(1);
    expect(confirmations[0]).toContain('기수 행 물리 삭제');
    expect(confirmations[0]).toContain('장수 기록 보존');
    expect(confirmations[0]).toContain('유산 획득분 35% 보전');
    const request = state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestGameCancellation');
    expect(JSON.stringify(request?.body)).toContain('"historyMode":"DELETE"');
    expect(JSON.stringify(request?.body)).toContain('"generalMode":"RETAIN"');
    expect(JSON.stringify(request?.body)).toContain('"earnedPointRetentionPercent":35');
    expect(JSON.stringify(request?.body)).toContain('잘못된 시나리오로 개장함');

    await page.setViewportSize({ width: 390, height: 844 });
    const mobileMetrics = await page.getByTestId('game-cancellation-form').evaluate((form) => {
        const rect = form.getBoundingClientRect();
        return {
            x: rect.x,
            width: rect.width,
            viewportWidth: document.documentElement.clientWidth,
            documentScrollWidth: document.documentElement.scrollWidth,
        };
    });
    expect(mobileMetrics.x).toBeGreaterThanOrEqual(0);
    expect(mobileMetrics.x + mobileMetrics.width).toBeLessThanOrEqual(mobileMetrics.viewportWidth);
    expect(mobileMetrics.documentScrollWidth).toBeLessThanOrEqual(mobileMetrics.viewportWidth);
    await writeFile(
        testInfo.outputPath('game-cancellation-metrics.json'),
        JSON.stringify({ desktopMetrics, mobileMetrics }, null, 2)
    );
    await page.screenshot({ path: testInfo.outputPath('game-cancellation-mobile.png'), fullPage: true });
});

for (const viewportSize of [
    { name: 'desktop', width: 1280, height: 720 },
    { name: 'mobile', width: 390, height: 844 },
]) {
    test(`follows new profile and Gateway logs only while each viewport is near the end on ${viewportSize.name}`, async ({
        page,
    }, testInfo) => {
        await page.setViewportSize(viewportSize);
        const profileOperationId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
        const gatewayOperationId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
        const profileSecondPoll = deferred();
        const profileThirdPoll = deferred();
        const gatewaySecondPoll = deferred();
        const gatewayThirdPoll = deferred();
        const state: FixtureState = {
            operations: [
                {
                    id: profileOperationId,
                    profileName: 'che:default',
                    type: 'DEPLOY',
                    status: 'RUNNING',
                    sourceMode: 'BRANCH',
                    sourceRef: 'main',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-08-01T01:00:00.000Z',
                    updatedAt: '2026-08-01T01:00:00.000Z',
                },
            ],
            gatewayOperations: [
                {
                    id: gatewayOperationId,
                    type: 'DEPLOY',
                    status: 'RUNNING',
                    sourceMode: 'BRANCH',
                    sourceRef: 'main',
                    payload: {},
                    requestedBy: 'admin',
                    createdAt: '2026-08-01T02:00:00.000Z',
                    updatedAt: '2026-08-01T02:00:00.000Z',
                },
            ],
            runtimeRunning: true,
            requestBodies: [],
            profileLogProgress: true,
            profileLogBatches: [
                makeLogEntries(profileOperationId, 'profile history', 1, 80),
                makeLogEntries(profileOperationId, 'profile while reading', 81, 1),
                makeLogEntries(profileOperationId, 'profile near end', 82, 1),
            ],
            profileLogPollGate: async (pollCount) => {
                if (pollCount === 2) await profileSecondPoll.promise;
                if (pollCount === 3) await profileThirdPoll.promise;
            },
            gatewayLogBatches: [
                makeLogEntries(gatewayOperationId, 'gateway history', 1, 80),
                makeLogEntries(gatewayOperationId, 'gateway while reading', 81, 1),
                makeLogEntries(gatewayOperationId, 'gateway near end', 82, 1),
            ],
            gatewayLogPollGate: async (pollCount) => {
                if (pollCount === 2) await gatewaySecondPoll.promise;
                if (pollCount === 3) await gatewayThirdPoll.promise;
            },
        };
        await installFixture(page, state);

        const verifyViewport = async (
            url: string,
            testId: 'profile-operation-log' | 'gateway-release-log',
            historyText: string,
            readingText: string,
            nearEndText: string,
            releaseSecondPoll: () => void,
            releaseThirdPoll: () => void,
            screenshotName: string
        ) => {
            await page.goto(url);
            const viewport = page.getByTestId(testId);
            await expect(viewport).toContainText(historyText);
            await expect
                .poll(() =>
                    viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)
                )
                .toBeLessThanOrEqual(1);

            const readingPosition = await viewport.evaluate((element) => {
                element.scrollTop = 200;
                return element.scrollTop;
            });
            expect(readingPosition).toBe(200);
            releaseSecondPoll();
            await expect(viewport).toContainText(readingText);
            await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(readingPosition);

            const nearEndGap = await viewport.evaluate((element) => {
                element.scrollTop = element.scrollHeight - element.clientHeight - 20;
                return element.scrollHeight - element.clientHeight - element.scrollTop;
            });
            expect(nearEndGap).toBeGreaterThan(0);
            expect(nearEndGap).toBeLessThanOrEqual(40);
            releaseThirdPoll();
            await expect(viewport).toContainText(nearEndText);
            await expect
                .poll(() =>
                    viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)
                )
                .toBeLessThanOrEqual(1);
            await page.screenshot({
                path: testInfo.outputPath(`${viewportSize.name}-${screenshotName}`),
                fullPage: true,
            });
        };

        await verifyViewport(
            'admin/servers/che%3Adefault/version',
            'profile-operation-log',
            'profile history 80',
            'profile while reading 81',
            'profile near end 82',
            profileSecondPoll.resolve,
            profileThirdPoll.resolve,
            'profile-log-scroll-follow.png'
        );
        await verifyViewport(
            'admin/releases',
            'gateway-release-log',
            'gateway history 80',
            'gateway while reading 81',
            'gateway near end 82',
            gatewaySecondPoll.resolve,
            gatewayThirdPoll.resolve,
            'gateway-log-scroll-follow.png'
        );
    });
}

test('loads server metadata defaults into the reset form and submits them', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        resetDefaults: {
            turnTermMinutes: 3,
            sync: false,
            fiction: 0,
            extend: false,
            blockGeneralCreate: 2,
            npcMode: 1,
            showImgLevel: 1,
            tournamentTrig: false,
            joinMode: 'onlyRandom',
            autorunUser: { limitMinutes: 720, options: ['develop', 'recruit_high', 'chief'] },
        },
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/servers/che%3Adefault/scenario');
    await expect(page.getByTestId('reset-turn-term')).toHaveValue('3');
    await page.getByText('고급 시나리오 옵션').click();
    await expect(page.getByTestId('reset-defaults-source')).toContainText('서버의 메타');
    await expect(page.getByTestId('reset-npc-mode')).toHaveValue('1');
    await expect(page.getByTestId('reset-autorun-recruit-high')).toBeChecked();
    await expect(page.getByTestId('reset-autorun-chief')).toBeChecked();
    await page.getByTestId('request-reset').click();

    await expect
        .poll(() => state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestReset'))
        .toBeTruthy();
    const request = JSON.stringify(
        state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestReset')?.body
    );
    expect(request).toContain('"turnTermMinutes":3');
    expect(request).toContain('"npcMode":1');
    expect(request).toContain('"joinMode":"onlyRandom"');
    expect(request).toContain('"limitMinutes":720');
    expect(request).toContain('"options":["develop","recruit_high","chief"]');
});

test('uses ref reset terms with compact hover, focus, and mobile help', async ({ page }, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
    };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault/scenario');
    await page.getByText('고급 시나리오 옵션').click();

    await expect(page.getByText('NPC 상성', { exact: true })).toBeVisible();
    await expect(page.getByText('확장 NPC', { exact: true })).toBeVisible();
    await expect(page.getByText('장수 임의 생성', { exact: true })).toBeVisible();
    await expect(page.getByText('NPC 빙의', { exact: true })).toBeVisible();
    await expect(page.getByText('자율행동', { exact: true })).toBeVisible();
    await expect(page.getByText('임관 모드', { exact: true })).toBeVisible();
    await expect(page.getByText('이미지 표기', { exact: true })).toBeVisible();
    await expect(page.getByText('토너먼트 자동 시작', { exact: true })).toBeVisible();
    await expect(page.getByTestId('reset-fiction').locator('option')).toHaveText(['연의', '가상']);
    await expect(page.getByTestId('reset-extend').locator('option')).toHaveText(['포함', '미포함']);
    await expect(page.getByTestId('reset-block-general-create').locator('option')).toHaveText([
        '가능',
        '장수명 무작위',
        '불가',
    ]);
    await expect(page.getByTestId('reset-npc-mode').locator('option')).toHaveText(['불가', '가능', '선택 생성 가능']);
    await expect(page.getByTestId('reset-show-img-level').locator('option')).toHaveText([
        '안함',
        '전콘',
        '전콘, 병종',
        '전콘, 병종, NPC',
    ]);

    const helpButtons = page.getByRole('button', { name: /도움말$/ });
    await expect(helpButtons).toHaveCount(13);
    const fictionHelp = page.getByTestId('reset-help-fiction');
    const fictionTooltip = page.getByTestId('reset-help-fiction-tooltip');
    await expect(fictionTooltip).toBeHidden();
    await fictionHelp.hover();
    await expect(fictionTooltip).toBeVisible();
    await expect(fictionTooltip).toContainText('연의는 서버 정보에서 사실 모드로도 표시');
    await expect.poll(() => fictionTooltip.evaluate((element) => getComputedStyle(element).opacity)).toBe('1');
    const hoverMetrics = await fictionHelp.evaluate((element) => {
        const buttonRect = element.getBoundingClientRect();
        const tooltip = document.querySelector<HTMLElement>('[data-testid="reset-help-fiction-tooltip"]');
        const tooltipRect = tooltip?.getBoundingClientRect();
        const buttonStyle = getComputedStyle(element);
        const tooltipStyle = tooltip ? getComputedStyle(tooltip) : null;
        return {
            button: { width: buttonRect.width, height: buttonRect.height },
            tooltip: tooltipRect
                ? { x: tooltipRect.x, y: tooltipRect.y, width: tooltipRect.width, height: tooltipRect.height }
                : null,
            buttonBorderRadius: buttonStyle.borderRadius,
            tooltipVisibility: tooltipStyle?.visibility,
            tooltipOpacity: tooltipStyle?.opacity,
        };
    });
    expect(hoverMetrics.button).toEqual({ width: 18, height: 18 });
    expect(hoverMetrics.tooltipVisibility).toBe('visible');
    expect(hoverMetrics.tooltipOpacity).toBe('1');

    await page.mouse.move(0, 0);
    const npcHelp = page.getByTestId('reset-help-npc-mode');
    await npcHelp.focus();
    await expect(page.getByTestId('reset-help-npc-mode-tooltip')).toBeVisible();
    await expect(npcHelp).toBeFocused();

    await page.getByTestId('reset-autorun-enabled').selectOption({ label: '사용' });
    await expect(page.getByText('자율행동 종류', { exact: true })).toBeVisible();
    for (const action of ['내정', '순간이동', '징병', '모병', '훈사', '출병', '기본 사령턴']) {
        await expect(page.getByText(action, { exact: true })).toBeVisible();
    }

    await page.setViewportSize({ width: 390, height: 844 });
    const mobileHelp = page.getByTestId('reset-help-npc-mode');
    await mobileHelp.focus();
    const mobileMetrics = await page.getByTestId('reset-help-npc-mode-tooltip').evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return {
            x: rect.x,
            right: rect.right,
            width: rect.width,
            viewportWidth: document.documentElement.clientWidth,
            documentScrollWidth: document.documentElement.scrollWidth,
        };
    });
    expect(mobileMetrics.x).toBeGreaterThanOrEqual(0);
    expect(mobileMetrics.right).toBeLessThanOrEqual(mobileMetrics.viewportWidth);
    expect(mobileMetrics.documentScrollWidth).toBeLessThanOrEqual(mobileMetrics.viewportWidth);
    await page.screenshot({ path: testInfo.outputPath('mobile-reset-option-help.png'), fullPage: true });
    await writeFile(
        testInfo.outputPath('reset-option-help-metrics.json'),
        JSON.stringify({ hoverMetrics, mobileMetrics }, null, 2)
    );
});

test('edits server reset defaults through profile metadata settings', async ({ page }, testInfo) => {
    const state: FixtureState = { operations: [], gatewayOperations: [], runtimeRunning: true, requestBodies: [] };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault');
    await page.getByText('서버 리셋 기본 옵션').click();
    await page.getByTestId('meta-reset-turn-term').selectOption('3');
    await page.getByTestId('meta-reset-npc-mode').selectOption('2');

    await page.getByRole('button', { name: '메타 저장' }).click();
    const validationToast = page.getByTestId('action-toast').filter({ hasText: '변경 사유를 입력하세요.' });
    await expect(validationToast).toHaveAttribute('data-toast-kind', 'error');
    await expect(validationToast).toHaveAttribute('role', 'alert');

    await page.getByPlaceholder('변경 사유 (필수)').fill('set reset defaults');
    await page.getByRole('button', { name: '메타 저장' }).click();

    await expect(page.getByText('메타 저장 완료').first()).toBeVisible();
    const successToast = page.getByTestId('action-toast').filter({ hasText: '메타 저장 완료' });
    await expect(successToast).toHaveAttribute('data-toast-kind', 'success');
    await expect(successToast).toHaveAttribute('role', 'status');
    const toastGeometry = await successToast.evaluate((element) => {
        const rect = element.getBoundingClientRect();
        const viewport = element.parentElement?.parentElement;
        return {
            right: Math.round(window.innerWidth - rect.right),
            width: Math.round(rect.width),
            viewportPosition: viewport ? getComputedStyle(viewport).position : '',
        };
    });
    expect(toastGeometry.right).toBeGreaterThanOrEqual(0);
    expect(toastGeometry.width).toBeGreaterThan(250);
    expect(toastGeometry.viewportPosition).toBe('fixed');
    await page.screenshot({ path: testInfo.outputPath('meta-save-toast-desktop.png'), fullPage: true });

    await page.setViewportSize({ width: 390, height: 844 });
    const mobileToastGeometry = await successToast.evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return {
            left: Math.round(rect.left),
            right: Math.round(window.innerWidth - rect.right),
            bottom: Math.round(window.innerHeight - rect.bottom),
        };
    });
    expect(mobileToastGeometry.left).toBeGreaterThanOrEqual(0);
    expect(mobileToastGeometry.right).toBeGreaterThanOrEqual(0);
    expect(mobileToastGeometry.bottom).toBeGreaterThanOrEqual(0);
    expect(mobileToastGeometry.bottom).toBeLessThanOrEqual(20);
    await page.screenshot({ path: testInfo.outputPath('meta-save-toast-mobile.png'), fullPage: true });
    const request = JSON.stringify(
        state.requestBodies.find((entry) => entry.operation === 'admin.profiles.updateMeta')?.body
    );
    expect(request).toContain('"resetDefaults"');
    expect(request).toContain('"turnTermMinutes":3');
    expect(request).toContain('"npcMode":2');
});

test('stores event season zero and first game zero from server metadata settings', async ({ page }) => {
    const state: FixtureState = { operations: [], gatewayOperations: [], runtimeRunning: true, requestBodies: [] };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault');
    const nextSeasonInput = page.getByTestId('next-season-idx');
    const firstGameInput = page.getByTestId('first-game-idx');
    await nextSeasonInput.fill('0');
    await firstGameInput.fill('0');
    await expect(nextSeasonInput).toHaveValue('0');
    await expect(firstGameInput).toHaveValue('0');
    expect(
        await nextSeasonInput.evaluate((element: HTMLInputElement) => ({
            valid: element.validity.valid,
            valueAsNumber: element.valueAsNumber,
        }))
    ).toEqual({ valid: true, valueAsNumber: 0 });
    expect(
        await firstGameInput.evaluate((element: HTMLInputElement) => ({
            valid: element.validity.valid,
            valueAsNumber: element.valueAsNumber,
        }))
    ).toEqual({ valid: true, valueAsNumber: 0 });
    await page.getByPlaceholder('변경 사유 (필수)').fill('prepare event season');
    await page.getByRole('button', { name: '메타 저장' }).click();

    await expect
        .poll(() => state.requestBodies.find((entry) => entry.operation === 'admin.profiles.updateMeta'))
        .toBeTruthy();
    const request = JSON.stringify(
        state.requestBodies.find((entry) => entry.operation === 'admin.profiles.updateMeta')?.body
    );
    expect(request).toContain('"nextSeasonIdx":0');
    expect(request).toContain('"firstGameIdx":0');
    await expect(page.getByTestId('action-toast').filter({ hasText: '메타 저장 완료' })).toBeVisible();
});

test('shows a dismissible error toast when profile metadata persistence fails', async ({ page }, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        updateMetaFails: true,
    };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault');
    await page.getByPlaceholder('변경 사유 (필수)').fill('exercise persistence error');
    await page.getByRole('button', { name: '메타 저장' }).click();

    const errorToast = page.getByTestId('action-toast').filter({ hasText: '메타 저장 실패' });
    await expect(errorToast).toBeVisible();
    await expect(errorToast).toHaveAttribute('data-toast-kind', 'error');
    await page.screenshot({ path: testInfo.outputPath('meta-save-toast-error.png'), fullPage: true });
    await errorToast.getByRole('button', { name: '알림 닫기' }).click();
    await expect(errorToast).toHaveCount(0);
});

test('renders the fixed-profile version form without waiting for the server list', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        profileNavigationDelayMs: 3000,
        profileNavigationResolved: false,
    };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault/version');
    await expect(page.getByTestId('request-deploy')).toBeVisible({ timeout: 900 });
    expect(state.profileNavigationResolved).toBe(false);
    await expect.poll(() => state.profileNavigationResolved).toBe(true);
    expect(state.profileNavigationRequests).toBe(1);
});

test('recovers the current-version scenario catalog after the initial request fails', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        scenarioFailuresRemaining: 1,
    };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault/scenario');
    await expect(page.getByTestId('scenario-select')).toContainText('선택할 수 있는 시나리오가 없습니다.');
    await expect(page.getByTestId('request-reset')).toBeDisabled();
    await page.getByTestId('load-scenarios').click();
    await expect(page.getByTestId('scenario-select')).toHaveValue('2');
    await expect(page.getByTestId('request-reset')).toBeEnabled();
});

test('renders the stable server identity without exposing the default suffix as the display name', async ({
    page,
}, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        profileListDelayMs: 1500,
    };
    await installFixture(page, state);

    await page.goto('admin/servers/che%3Adefault');
    const navigation = page.getByRole('navigation', { name: '관리자 메뉴' });
    const profileLink = navigation.getByRole('link', { name: '천하서버' });
    await expect(profileLink).toBeVisible({ timeout: 900 });
    await expect(profileLink).toHaveAttribute('title', '천하서버 서버 관리');
    await expect(navigation).not.toContainText('천하서버 (che:default)');
    await expect(navigation.getByRole('link', { name: 'Gateway 릴리스' })).toBeVisible({ timeout: 900 });
    await expect(page.getByText('서버 ID: che:default · 인스턴스: default')).toHaveCount(0);
    await expect(page.getByText('천하서버', { exact: true })).toBeVisible();
    await expect(page.getByText('현재 시나리오: 2')).toBeVisible();
    await profileLink.focus();
    const desktop = await profileLink.evaluate((element) => {
        const rect = element.getBoundingClientRect();
        const style = getComputedStyle(element);
        return {
            x: rect.x,
            width: rect.width,
            height: rect.height,
            overflow: element.scrollWidth - element.clientWidth,
            backgroundColor: style.backgroundColor,
            color: style.color,
        };
    });
    expect(desktop.width).toBeGreaterThan(100);
    expect(desktop.height).toBeGreaterThan(30);
    expect(desktop.overflow).toBeLessThanOrEqual(0);
    expect(desktop.backgroundColor).toBe('rgb(45, 27, 8)');
    expect(desktop.color).toBe('rgb(253, 230, 138)');
    await page.screenshot({ path: testInfo.outputPath('profile-identity-desktop.png'), fullPage: true });

    await page.setViewportSize({ width: 390, height: 844 });
    await page.getByRole('button', { name: '관리자 메뉴' }).click();
    await expect(profileLink).toBeVisible();
    expect(
        await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
    ).toBeLessThanOrEqual(0);
    await page.screenshot({ path: testInfo.outputPath('profile-identity-mobile.png'), fullPage: true });
    expect(state.profileNavigationRequests).toBe(1);
});

test('scenario-only operator resets the server-selected version without Git or Gateway controls', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        capabilities: [{ permission: 'admin.scenarios.reset', scope: 'PROFILE', scopes: ['che:default'] }],
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/servers/che%3Adefault/scenario');
    await expect(page.getByTestId('source-current')).toBeChecked();
    await expect(page.getByTestId('source-branch')).toHaveCount(0);
    await expect(page.getByTestId('source-commit')).toHaveCount(0);
    await expect(page.getByRole('link', { name: 'Gateway 릴리스' })).toHaveCount(0);
    await page.getByTestId('request-reset').click();
    await expect(page.getByText('초기화 작업을 등록했습니다.').first()).toBeVisible();
    await expect
        .poll(() => state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestReset'))
        .toBe(true);

    const request = state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestReset');
    expect(JSON.stringify(request?.body)).toContain('"sourceMode":"CURRENT"');
    expect(JSON.stringify(request?.body)).not.toContain('"sourceRef"');
});

test('stops a running profile build while keeping the existing runtime available', async ({ page }) => {
    const operation: Operation = {
        id: '12121212-1212-4212-8212-121212121212',
        profileName: 'che:default',
        type: 'DEPLOY',
        status: 'RUNNING',
        sourceMode: 'COMMIT',
        sourceRef: '0123456789abcdef0123456789abcdef01234567',
        payload: {},
        requestedBy: 'admin',
        createdAt: '2026-08-20T01:00:00.000Z',
        updatedAt: '2026-08-20T01:00:01.000Z',
    };
    const state: FixtureState = {
        operations: [operation],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        profileLogsEmpty: true,
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/servers/che%3Adefault/version');
    await expect(page.getByTestId('profile-build-recovery-guide')).toContainText('기존 profile runtime과 게임 DB는');
    await page.getByRole('button', { name: '빌드 중단' }).click();

    await expect(page.getByText('프로필 빌드를 중단했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('operations-table').getByText('중단됨', { exact: true })).toBeVisible();
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.cancel')).toBe(true);
});

test('stops and retries a running Gateway build from the release GUI', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [
            {
                id: '34343434-3434-4434-8434-343434343434',
                type: 'DEPLOY',
                status: 'RUNNING',
                sourceMode: 'COMMIT',
                sourceRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
                resolvedCommitSha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
                payload: {},
                requestedBy: 'admin',
                createdAt: '2026-08-20T01:00:00.000Z',
                updatedAt: '2026-08-20T01:00:01.000Z',
            },
        ],
        runtimeRunning: true,
        requestBodies: [],
        gatewayLogsEmpty: true,
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/releases');
    await expect(page.getByTestId('gateway-build-recovery-guide')).toContainText(
        'migration 또는 process 전환이 시작된 뒤에는'
    );
    await page.getByRole('button', { name: '빌드 중단' }).click();
    await expect(page.getByText('Gateway 빌드를 중단했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('gateway-release-table').getByText('CANCELLED', { exact: true })).toBeVisible();

    await page.getByRole('button', { name: '재시도' }).click();
    await expect(page.getByText('Gateway 릴리스 재시도 작업을 등록했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('gateway-release-table').getByText('QUEUED', { exact: true })).toBeVisible();
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.cancel')).toBe(true);
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.retry')).toBe(true);
});

test('controls gateway deployment and rollback through the external controller queue', async ({ page }, testInfo) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [],
        runtimeRunning: true,
        requestBodies: [],
        gatewayStateFailuresAfterRequest: 1,
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/releases');
    const panel = page.getByTestId('gateway-release-panel');
    await expect(panel).toBeVisible();
    await expect(panel).toContainText('aaaaaaaaaaaa');
    await expect(panel).toContainText('bbbbbbbbbbbb');
    await page.getByTestId('gateway-source-ref').fill('release/2026-08');
    await page.getByTestId('request-gateway-deploy').click();

    await expect(page.getByText(/Gateway 배포 작업을 등록했습니다/).first()).toBeVisible();
    expect(state.gatewayStateFailureCount).toBe(1);
    await expect(page.getByTestId('server-operations-page')).not.toContainText('Unexpected end of JSON input');
    await expect(page.getByTestId('action-toast').filter({ hasText: 'Unexpected end of JSON input' })).toHaveCount(0);
    await expect(page.getByTestId('gateway-release-table')).toContainText('DEPLOY');
    await expect(page.getByTestId('gateway-release-log-panel')).toBeVisible();
    await expect(page.getByTestId('gateway-release-log')).toContainText('Gateway 구성 요소를 빌드합니다.');
    await expect(page.getByTestId('gateway-release-log')).toContainText('gateway-frontend build complete');
    await expect(page.getByTestId('gateway-release-log-status')).toContainText('SUCCEEDED');
    expect(state.gatewayLogPollCount).toBeGreaterThanOrEqual(2);
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.requestGatewayDeploy')).toBe(true);
    await page.screenshot({ path: testInfo.outputPath('gateway-release-desktop.png'), fullPage: true });

    await page.setViewportSize({ width: 390, height: 844 });
    const mobileLogGeometry = await page.getByTestId('gateway-release-log-panel').evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return { x: rect.x, width: rect.width, viewportWidth: document.documentElement.clientWidth };
    });
    expect(mobileLogGeometry.x).toBeGreaterThanOrEqual(0);
    expect(mobileLogGeometry.x + mobileLogGeometry.width).toBeLessThanOrEqual(mobileLogGeometry.viewportWidth);
    await page.screenshot({ path: testInfo.outputPath('gateway-release-mobile.png'), fullPage: true });

    state.gatewayOperations = [];
    await page.getByTestId('refresh-operations').click();
    await page.getByTestId('request-gateway-rollback').click();
    await expect(page.getByText('Gateway rollback 작업을 등록했습니다.').first()).toBeVisible();
    expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.requestGatewayRollback')).toBe(true);
});

test('moves long Gateway release errors out of the table column into an expandable detail row', async ({
    page,
}, testInfo) => {
    const longError = [
        'Gateway release did not become ready before the timeout.',
        'Error: gateway-frontend readiness check failed after 30 attempts',
        '    at waitForGatewayReadiness (/srv/core/release-controller/dist/releaseController.js:842:19)',
        'controller-output-without-breaks-'.repeat(12),
    ].join('\n');
    const operationId = '88888888-8888-4888-8888-888888888888';
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [
            {
                id: operationId,
                type: 'DEPLOY',
                status: 'FAILED',
                sourceMode: 'COMMIT',
                sourceRef: 'cccccccccccccccccccccccccccccccccccccccc',
                resolvedCommitSha: 'cccccccccccccccccccccccccccccccccccccccc',
                completedAt: '2026-08-01T02:03:00.000Z',
                error: longError,
                payload: {},
                requestedBy: 'admin',
                createdAt: '2026-08-01T02:00:00.000Z',
                updatedAt: '2026-08-01T02:03:00.000Z',
            },
        ],
        gatewayLogsEmpty: true,
        runtimeRunning: true,
        requestBodies: [],
    };
    await installFixture(page, state);

    await page.goto('admin/releases');
    const table = page.getByTestId('gateway-release-table');
    await expect(table.getByRole('columnheader')).toHaveCount(6);
    await expect(table.getByRole('columnheader', { name: '오류', exact: true })).toHaveCount(0);
    await expect(table.getByRole('columnheader', { name: '상세', exact: true })).toBeVisible();

    const errorToggle = page.getByTestId('gateway-release-error-toggle');
    await expect(errorToggle).toHaveText('오류 보기');
    await expect(errorToggle).toHaveAttribute('aria-expanded', 'false');
    await expect(page.getByTestId('gateway-release-error-detail')).toBeHidden();

    await errorToggle.focus();
    const focusedToggleStyle = await errorToggle.evaluate((element) => {
        const style = getComputedStyle(element);
        return {
            outlineStyle: style.outlineStyle,
            outlineWidth: style.outlineWidth,
            color: style.color,
        };
    });
    expect(focusedToggleStyle.outlineStyle).not.toBe('none');
    expect(parseFloat(focusedToggleStyle.outlineWidth)).toBeGreaterThanOrEqual(2);

    await errorToggle.hover();
    await errorToggle.click();
    await expect(errorToggle).toHaveText('오류 닫기');
    await expect(errorToggle).toHaveAttribute('aria-expanded', 'true');
    const errorDetail = page.getByTestId('gateway-release-error-detail');
    await expect(errorDetail).toContainText('Gateway release did not become ready');
    await expect(errorDetail).toContainText('controller-output-without-breaks');
    const desktopGeometry = await table.evaluate((element) => {
        const headings = Array.from(element.querySelectorAll('thead th'));
        const detail = element.querySelector('[data-testid="gateway-release-error-detail"]');
        const detailCell = detail?.querySelector('td');
        const errorText = detail?.querySelector('pre');
        return {
            tableWidth: element.getBoundingClientRect().width,
            scrollerWidth: element.parentElement?.getBoundingClientRect().width ?? 0,
            tableLayout: getComputedStyle(element).tableLayout,
            columnCount: headings.length,
            detailColSpan: detailCell?.getAttribute('colspan'),
            detailWidth: detailCell?.getBoundingClientRect().width ?? 0,
            errorWhiteSpace: errorText ? getComputedStyle(errorText).whiteSpace : '',
            errorOverflowWrap: errorText ? getComputedStyle(errorText).overflowWrap : '',
        };
    });
    expect(desktopGeometry).toMatchObject({
        tableLayout: 'fixed',
        columnCount: 6,
        detailColSpan: '6',
        errorWhiteSpace: 'pre-wrap',
    });
    expect(desktopGeometry.tableWidth).toBeGreaterThanOrEqual(680);
    expect(desktopGeometry.detailWidth).toBeGreaterThanOrEqual(desktopGeometry.tableWidth - 1);
    await page.screenshot({ path: testInfo.outputPath('gateway-release-error-expanded-desktop.png'), fullPage: true });

    await page.setViewportSize({ width: 390, height: 844 });
    await expect(table.getByRole('columnheader')).toHaveCount(4);
    await expect(table.getByRole('columnheader', { name: '소스', exact: true })).toHaveCount(0);
    await expect(table.getByRole('columnheader', { name: '해석 커밋', exact: true })).toHaveCount(0);
    const mobileGeometry = await table.evaluate((element) => {
        const scroller = element.parentElement!;
        const scrollerRect = scroller.getBoundingClientRect();
        const detailRect = element
            .querySelector('[data-testid="gateway-release-error-detail"]')!
            .getBoundingClientRect();
        return {
            tableWidth: element.getBoundingClientRect().width,
            scrollerX: scrollerRect.x,
            scrollerWidth: scrollerRect.width,
            scrollerScrollWidth: scroller.scrollWidth,
            detailWidth: detailRect.width,
            viewportWidth: document.documentElement.clientWidth,
            documentScrollWidth: document.documentElement.scrollWidth,
        };
    });
    expect(mobileGeometry.tableWidth).toBeLessThanOrEqual(mobileGeometry.scrollerWidth + 1);
    expect(mobileGeometry.detailWidth).toBeGreaterThanOrEqual(mobileGeometry.tableWidth - 1);
    expect(mobileGeometry.scrollerScrollWidth).toBeLessThanOrEqual(mobileGeometry.scrollerWidth + 1);
    expect(mobileGeometry.scrollerX).toBeGreaterThanOrEqual(0);
    expect(mobileGeometry.scrollerX + mobileGeometry.scrollerWidth).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
    expect(mobileGeometry.documentScrollWidth).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
    await page.screenshot({ path: testInfo.outputPath('gateway-release-error-expanded-mobile.png'), fullPage: true });

    await errorToggle.click();
    await expect(errorToggle).toHaveAttribute('aria-expanded', 'false');
    await expect(page.getByTestId('gateway-release-error-detail')).toBeHidden();
});

test('explains terminal releases created before controller progress logging', async ({ page }) => {
    const state: FixtureState = {
        operations: [],
        gatewayOperations: [
            {
                id: '99999999-9999-4999-8999-999999999999',
                type: 'DEPLOY',
                status: 'SUCCEEDED',
                sourceMode: 'COMMIT',
                sourceRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
                payload: {},
                requestedBy: 'admin',
                createdAt: '2026-08-01T02:00:00.000Z',
                updatedAt: '2026-08-01T02:02:00.000Z',
            },
        ],
        gatewayLogsEmpty: true,
        runtimeRunning: true,
        requestBodies: [],
    };
    await installFixture(page, state);

    await page.goto('admin/releases');
    await expect(page.getByTestId('gateway-release-log')).toContainText(
        '로그 지원 controller 적용 전 작업일 수 있습니다.'
    );
    await expect(page.getByTestId('gateway-release-log')).not.toContainText('controller 로그를 기다리고 있습니다');
});

test('renders a failed reset, retries it as a new operation, and reaches success', async ({ page }, testInfo) => {
    const longError =
        '선택한 커밋의 프로필 프로세스를 시작하지 못했습니다. 실패 원인을 확인한 뒤 동일 generation으로 재시도해 주세요.';
    const state: FixtureState = {
        operations: [
            {
                id: '55555555-5555-4555-8555-555555555555',
                profileName: 'che:default',
                type: 'RESET',
                status: 'FAILED',
                sourceMode: 'COMMIT',
                sourceRef: 'fedcba9876543210fedcba9876543210fedcba98',
                resolvedCommitSha: 'fedcba9876543210fedcba9876543210fedcba98',
                payload: { installOperationId: 'failed-generation' },
                requestedBy: 'admin',
                completedAt: '2026-07-25T03:30:00.000Z',
                error: longError,
                createdAt: '2026-07-25T03:00:00.000Z',
                updatedAt: '2026-07-25T03:30:00.000Z',
            },
        ],
        gatewayOperations: [],
        runtimeRunning: false,
        requestBodies: [],
    };
    await installFixture(page, state);
    page.on('dialog', (dialog) => dialog.accept());

    await page.goto('admin/servers/che%3Adefault/scenario');
    const operationTable = page.getByTestId('operations-table');
    await expect(operationTable.getByText('실패', { exact: true })).toBeVisible();
    await expect(operationTable.getByText(longError)).toBeHidden();
    await operationTable.getByRole('button', { name: '오류 상세' }).click();
    await expect(operationTable.getByText('fedcba9876543210fedcba9876543210fedcba98', { exact: true })).toBeVisible();
    const failure = page.getByTestId('operations-table').getByText(longError);
    await expect(failure).toBeVisible();
    expect(await failure.evaluate((element) => getComputedStyle(element).color)).toBe('oklch(0.808 0.114 19.571)');

    await page.getByRole('button', { name: '재시도' }).click();
    await expect(page.getByText('재시도 작업을 등록했습니다.').first()).toBeVisible();
    await expect(page.getByTestId('operations-table').getByText('실패', { exact: true })).toBeVisible();
    await expect(page.getByTestId('operations-table').getByText('대기 중', { exact: true })).toBeVisible();
    await expect(page.getByTestId('operation-summary-row')).toHaveCount(2);

    state.operations[0] = {
        ...state.operations[0]!,
        status: 'SUCCEEDED',
        resolvedCommitSha: 'fedcba9876543210fedcba9876543210fedcba98',
        completedAt: '2026-07-25T04:05:00.000Z',
        updatedAt: '2026-07-25T04:05:00.000Z',
    };
    state.runtimeRunning = true;
    await page.getByTestId('refresh-operations').click();
    await expect(page.getByTestId('operations-table').getByText('완료', { exact: true })).toBeVisible();
    await expect(page.getByText('운영 프로필', { exact: true })).toHaveCount(0);

    await page.screenshot({ path: testInfo.outputPath('failed-retry-succeeded-desktop.png'), fullPage: true });
    await page.setViewportSize({ width: 390, height: 844 });
    const tableGeometry = await page.getByTestId('operations-table').evaluate((table) => {
        const tableRect = table.getBoundingClientRect();
        const scrollerRect = table.parentElement!.getBoundingClientRect();
        return {
            tableWidth: tableRect.width,
            scrollerWidth: scrollerRect.width,
            documentScrollWidth: document.documentElement.scrollWidth,
            viewportWidth: document.documentElement.clientWidth,
        };
    });
    expect(tableGeometry.tableWidth).toBeLessThanOrEqual(tableGeometry.scrollerWidth + 1);
    expect(tableGeometry.documentScrollWidth).toBeLessThanOrEqual(tableGeometry.viewportWidth);
    await page.screenshot({ path: testInfo.outputPath('failed-retry-succeeded-mobile.png'), fullPage: true });
});
