import { expect, test, type Page, type Route } from '@playwright/test';
import { mkdir, readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';

type Role = 'leader' | 'head' | 'member';
type FixtureState = {
    role: Role;
    failNextRate?: boolean;
    failPersonnelLoad?: boolean;
    rate: number;
    appointedGeneralId?: number;
    appointedCityId?: number;
    appointedOfficerLevel?: number;
    noticeMutationInput?: string;
    scoutMutationInput?: string;
    uploadDataUrl?: string;
};

type TrpcRequestPayload = {
    json?: Record<string, unknown>;
    input?: { json?: Record<string, unknown> };
};

const purifiedNoticeResponse =
    '<p data-flip="horizontal" style="color:#00ffff">서버 정화 방침</p><img src="/image/icons/default.jpg" alt="default.jpg" />';
const purifiedScoutResponse = '<strong>서버 정화 임관문</strong><a>위험 링크</a>';

const artifactRoot = process.env.OFFICE_PARITY_ARTIFACT_DIR ? resolve(process.env.OFFICE_PARITY_ARTIFACT_DIR) : null;
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const imageRoots = [
    ...(process.env.FRONTEND_PARITY_IMAGE_ROOT ? [resolve(process.env.FRONTEND_PARITY_IMAGE_ROOT)] : []),
    resolve(repositoryRoot, '../image'),
    resolve(repositoryRoot, '../../image'),
];
const referenceAsset = async (relativePath: string): Promise<Buffer> => {
    for (const root of imageRoots) {
        try {
            return await readFile(resolve(root, relativePath));
        } catch {
            // Nested worktrees and the primary checkout have different image parents.
        }
    }
    throw new Error(`Reference image not found: ${relativePath}`);
};
const response = (data: unknown) => ({ result: { data } });
const errorResponse = (path: string, message: string, code = 'BAD_REQUEST') => ({
    error: { message, code: -32000, data: { code, httpStatus: code === 'FORBIDDEN' ? 403 : 400, path } },
});
const operationName = (route: Route): string => {
    const url = new URL(route.request().url());
    return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6));
};
const responseHasOperation = (url: string, operation: string): boolean => {
    const pathname = decodeURIComponent(new URL(url).pathname);
    return pathname
        .slice(pathname.lastIndexOf('/trpc/') + 6)
        .split(',')
        .includes(operation);
};
const fulfillJson = (route: Route, body: unknown) =>
    route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });

const general = (id: number, name: string, officerLevel: number, overrides: Record<string, unknown> = {}) => ({
    id,
    name,
    npcState: 0,
    officerLevel,
    cityId: 1,
    cityName: '허창',
    troopId: 0,
    troopName: null,
    picture: 'default.jpg',
    imageServer: 0,
    officerCity: officerLevel >= 2 && officerLevel <= 4 ? 1 : 0,
    officerCityName: officerLevel >= 2 && officerLevel <= 4 ? '허창' : null,
    stats: { leadership: 70, strength: 70, intelligence: 70 },
    experience: 100,
    dedication: 200,
    injury: 0,
    gold: 1000,
    rice: 1000,
    crew: 100,
    personality: null,
    specialDomestic: null,
    specialWar: null,
    belong: 10,
    permission: 'normal',
    ...overrides,
});

const personnelFixture = (state: FixtureState) => {
    const officerLevel = state.role === 'leader' ? 12 : state.role === 'head' ? 5 : 1;
    const fullGenerals = [
        general(1, '조조', 12),
        general(2, '순욱', 11),
        general(3, '하후돈', 4),
        general(4, '곽가', 3),
        general(5, '정욱', 2),
        general(6, '장료', 1),
        general(7, '허저', 1, { permission: 'ambassador' }),
    ];
    const visibleGenerals =
        state.role === 'member' ? fullGenerals.filter((entry) => entry.officerLevel >= 2) : fullGenerals;
    return {
        me: {
            id: state.role === 'leader' ? 1 : state.role === 'head' ? 2 : 6,
            officerLevel,
            canManage: state.role !== 'member',
            canChangePermissions: state.role === 'leader',
            canKick: state.role !== 'member',
        },
        nation: {
            id: 1,
            name: '작위검증국',
            color: '#777777',
            level: 3,
            typeCode: 'che_법가',
            capitalCityId: 1,
            chiefSet: 0,
        },
        chiefStatMin: 65,
        generals: visibleGenerals,
        chiefAssignments: { 12: fullGenerals[0], 11: fullGenerals[1] },
        cityAssignments: [
            {
                id: 1,
                name: '허창',
                level: 7,
                region: 2,
                officerSet: 1 << 3,
                officers: { 4: fullGenerals[2], 3: fullGenerals[3], 2: fullGenerals[4] },
            },
            { id: 2, name: '낙양', level: 6, region: 2, officerSet: 0, officers: { 4: null, 3: null, 2: null } },
        ],
        awards: {
            tigers: [{ id: 3, name: '하후돈', value: 12 }],
            eagles: [{ id: 4, name: '곽가', value: 9 }],
        },
        permissionCandidates:
            state.role === 'leader'
                ? {
                      ambassadors: [
                          { id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 },
                          { id: 7, name: '허저', npcState: 0, permission: 'ambassador', maxPermission: 4 },
                      ],
                      auditors: [{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 }],
                  }
                : { ambassadors: [], auditors: [] },
    };
};

const financeFixture = (state: FixtureState) => ({
    editable: state.role === 'leader' || state.role === 'head',
    nationMsg: '<p>백성을 편안하게 한다.</p>',
    scoutMsg: '<p>천하의 인재를 구합니다.</p>',
    nationId: 1,
    officerLevel: state.role === 'leader' ? 12 : state.role === 'head' ? 5 : 1,
    year: 185,
    month: 1,
    nationsList: [
        {
            id: 1,
            name: '작위검증국',
            color: '#777777',
            level: 3,
            power: 1234,
            generalCount: 7,
            cityCount: 2,
            diplomacy: { state: 7, term: null },
        },
        {
            id: 2,
            name: '촉',
            color: '#008000',
            level: 3,
            power: 987,
            generalCount: 5,
            cityCount: 1,
            diplomacy: { state: 2, term: 6 },
        },
    ],
    gold: 10000,
    rice: 20000,
    income: { gold: { city: 2000, war: 300 }, rice: { city: 1800, wall: 200 } },
    outcome: 1000,
    policy: { rate: state.rate, bill: 100, secretLimit: 3, blockScout: false, blockWar: false },
    warSettingCnt: { remain: 5, inc: 2, max: 10 },
});

const installFixture = async (page: Page, state: FixtureState) => {
    await page.addInitScript((profile) => {
        localStorage.setItem('sammo-game-token', 'ga_office_playwright');
        localStorage.setItem('sammo-game-profile', profile);
    }, gameProfile);
    for (const filename of ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg']) {
        await page.route(`**/image/game/${filename}`, async (route) =>
            route.fulfill({
                status: 200,
                contentType: 'image/jpeg',
                body: await referenceAsset(`game/${filename}`),
            })
        );
    }
    await page.route('**/image/icons/**', async (route) =>
        route.fulfill({
            status: 200,
            contentType: 'image/jpeg',
            body: await referenceAsset('icons/default.jpg'),
        })
    );
    await page.route(gameTrpcRoute, async (route) => {
        const operations = operationName(route).split(',');
        const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
        const requestBody =
            rawRequestBody && typeof rawRequestBody === 'object' ? (rawRequestBody as Record<string, unknown>) : {};
        const results = operations.map((operation, operationIndex) => {
            const rawPayload =
                requestBody[String(operationIndex)] ?? (operations.length === 1 ? requestBody : undefined);
            const payload =
                rawPayload && typeof rawPayload === 'object' ? (rawPayload as TrpcRequestPayload) : undefined;
            const jsonInput =
                payload?.json ?? payload?.input?.json ?? (payload as Record<string, unknown> | undefined) ?? {};
            if (operation === 'auth.status') return response({ ok: true });
            if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '조조' } });
            if (operation === 'join.getConfig') return response({});
            if (operation === 'nation.getPersonnelInfo') {
                return state.failPersonnelLoad
                    ? errorResponse(operation, '권한이 부족합니다.', 'FORBIDDEN')
                    : response(personnelFixture(state));
            }
            if (operation === 'nation.getStratFinan') return response(financeFixture(state));
            if (operation === 'nation.appoint') {
                state.appointedGeneralId = Number(jsonInput.destGeneralId ?? 0);
                state.appointedCityId = Number(jsonInput.destCityId ?? 0);
                state.appointedOfficerLevel = Number(jsonInput.officerLevel ?? 0);
                return response({ ok: true });
            }
            if (operation === 'nation.kick' || operation === 'nation.changePermission') return response({ ok: true });
            if (operation === 'nation.setRate') {
                if (state.failNextRate) {
                    state.failNextRate = false;
                    return errorResponse(operation, '세율을 변경할 수 없습니다.');
                }
                state.rate = 25;
                return response({ ok: true });
            }
            if (operation === 'nation.setNotice') {
                state.noticeMutationInput = typeof jsonInput.msg === 'string' ? jsonInput.msg : undefined;
                return response({ ok: true, msg: purifiedNoticeResponse });
            }
            if (operation === 'nation.setScoutMsg') {
                state.scoutMutationInput = typeof jsonInput.msg === 'string' ? jsonInput.msg : undefined;
                return response({ ok: true, msg: purifiedScoutResponse });
            }
            if (operation === 'board.uploadImage') {
                state.uploadDataUrl = typeof jsonInput.dataUrl === 'string' ? jsonInput.dataUrl : undefined;
                return response({
                    url: 'https://sam-image.hided.net/uploads/core2026/0123456789abcdef0123456789abcdef.webp',
                    width: 1,
                    height: 1,
                    format: 'webp',
                    animated: false,
                    size: 68,
                });
            }
            if (['nation.setBill', 'nation.setSecretLimit', 'nation.setBlockScout'].includes(operation))
                return response({ ok: true });
            if (operation === 'nation.setBlockWar') return response({ availableCnt: 4 });
            return errorResponse(operation, `Unhandled fixture operation: ${operation}`);
        });
        await fulfillJson(route, results);
    });
};

const gotoOffice = async (page: Page, path: 'nation/personnel' | 'nation/finance') => {
    const lobbyResponse = page.waitForResponse((response) => responseHasOperation(response.url(), 'lobby.info'));
    await page.goto(path);
    await lobbyResponse;
};
const screenshot = async (page: Page, name: string) => {
    if (!artifactRoot) return;
    await mkdir(artifactRoot, { recursive: true });
    await page.screenshot({ path: resolve(artifactRoot, name), fullPage: true });
};

test('personnel keeps the desktop frame while exposing row-level appointment controls', async ({ page }) => {
    await installFixture(page, { role: 'leader', rate: 20 });
    await page.setViewportSize({ width: 1000, height: 900 });
    await gotoOffice(page, 'nation/personnel');
    await expect(page.getByText('작위검증국')).toBeVisible();

    const computed = await page.locator('#personnel-container').evaluate((container) => {
        const box = (selector?: string) => {
            const element = selector ? container.querySelector<HTMLElement>(selector)! : container;
            const rect = element.getBoundingClientRect();
            const style = getComputedStyle(element);
            return {
                width: rect.width,
                height: rect.height,
                fontFamily: style.fontFamily,
                fontSize: style.fontSize,
                lineHeight: style.lineHeight,
                backgroundImage: style.backgroundImage,
                color: style.color,
            };
        };
        return {
            container: box(),
            heading: box('.heading-table'),
            status: box('.chief-status'),
            icon: box('.general-icon'),
            chiefEntry: box('.chief-entry-cell'),
            changeButton: box('.personnel-change-button'),
            cityOfficer: box('.city-officer-cell'),
            documentWidth: document.documentElement.scrollWidth,
        };
    });
    expect(computed.container.width).toBe(1000);
    expect(computed.heading.width).toBe(1000);
    expect(computed.heading.height).toBeCloseTo(56, 0);
    expect(computed.status.width).toBe(1000);
    expect(computed.icon.width).toBe(64);
    expect(computed.icon.height).toBeCloseTo(64, 0);
    expect(computed.chiefEntry.width).toBeGreaterThan(499);
    expect(computed.chiefEntry.width).toBeLessThan(501);
    expect(computed.chiefEntry.height).toBeGreaterThanOrEqual(76);
    expect(computed.changeButton.height).toBeGreaterThanOrEqual(34);
    expect(computed.cityOfficer.width).toBeCloseTo(280, 0);
    expect(computed.container.fontFamily).toContain('Pretendard');
    expect(computed.container.fontSize).toBe('14px');
    expect(computed.container.lineHeight).toBe('18.2px');
    expect(computed.status.backgroundImage).toContain('back_walnut.jpg');
    expect(computed.documentWidth).toBe(1000);

    const changeButton = page.getByRole('button', { name: '주부 변경하기', exact: true });
    expect(await changeButton.evaluate((button) => getComputedStyle(button).backgroundColor)).toBe('rgb(49, 91, 61)');
    await changeButton.hover();
    expect(await changeButton.evaluate((button) => getComputedStyle(button).cursor)).toBe('pointer');
    await changeButton.focus();
    expect(await changeButton.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none');
    await expect(page.getByRole('button', { name: '허창 태수 변경하기', exact: true })).toBeVisible();
    await expect(page.getByRole('button', { name: '허창 군사 변경하기', exact: true })).toHaveCount(0);
    await screenshot(page, 'core-personnel-desktop-leader.png');
});

test('personnel selects an informed general and reports the JosaUtil-composed result in a toast', async ({ page }) => {
    const state: FixtureState = { role: 'head', rate: 20 };
    await installFixture(page, state);
    await page.setViewportSize({ width: 1000, height: 900 });
    await gotoOffice(page, 'nation/personnel');

    await page.getByRole('button', { name: '주부 변경하기', exact: true }).click();
    const picker = page.getByTestId('personnel-selection-dialog');
    await expect(picker).toBeVisible();
    await expect(picker.getByRole('heading', { name: '주부 임명 대상 선택' })).toBeVisible();
    await picker.getByPlaceholder('장수명·도시·관직·특성 검색').fill('장료');
    const candidate = picker.getByRole('button', { name: /장료/ });
    await expect(candidate).toContainText('허창 · 일반 장수');
    await expect(candidate).toContainText('통솔70');
    await expect(candidate).toContainText('소속10년');
    await expect(candidate).toContainText('병력100');
    await candidate.hover();
    expect(await candidate.evaluate((button) => getComputedStyle(button).cursor)).toBe('pointer');
    await candidate.focus();
    expect(await candidate.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none');
    await screenshot(page, 'core-personnel-desktop-general-picker.png');
    page.once('dialog', async (dialog) => {
        expect(dialog.message()).toBe('장료를 주부직에 임명하시겠습니까?');
        await dialog.accept();
    });
    await candidate.click();

    await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
    expect(state.appointedGeneralId).toBe(6);
    expect(state.appointedCityId).toBe(0);
    expect(state.appointedOfficerLevel).toBe(11);
    await expect(page.locator('.feedback.status')).toHaveCount(0);
    await screenshot(page, 'core-personnel-appointment-toast.png');
});

test('personnel reflows row-level appointments at 500px and 390px without gradients or overflow', async ({ page }) => {
    const state: FixtureState = { role: 'head', rate: 20 };
    await installFixture(page, state);
    await page.setViewportSize({ width: 500, height: 900 });
    await gotoOffice(page, 'nation/personnel');
    await expect(page.getByText('작위검증국')).toBeVisible();
    expect(
        await page.locator('#personnel-container').evaluate((element) => element.getBoundingClientRect().width)
    ).toBe(500);
    expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(500);
    const rowGeometry = await page.locator('#personnel-container').evaluate((container) => {
        const rect = (selector: string) => container.querySelector<HTMLElement>(selector)!.getBoundingClientRect();
        return {
            chiefWidth: rect('.chief-entry-cell').width,
            cityWidth: rect('.city-identity').width,
            officerWidth: rect('.city-officer-cell').width,
            gradientCount: [...container.querySelectorAll<HTMLElement>('*')].filter((element) =>
                getComputedStyle(element).backgroundImage.includes('gradient')
            ).length,
        };
    });
    expect(rowGeometry.chiefWidth).toBeGreaterThan(249);
    expect(rowGeometry.chiefWidth).toBeLessThan(251);
    expect(rowGeometry.cityWidth).toBeGreaterThan(79);
    expect(rowGeometry.cityWidth).toBeLessThan(81);
    expect(rowGeometry.officerWidth).toBeGreaterThan(139);
    expect(rowGeometry.officerWidth).toBeLessThan(141);
    expect(rowGeometry.gradientCount).toBe(0);
    await expect(page.getByRole('combobox', { name: '외교권자' })).toHaveCount(0);
    await expect(page.getByRole('combobox', { name: '추방 대상 장수' })).toBeVisible();

    await page.getByRole('button', { name: '허창 태수 변경하기', exact: true }).click();
    const picker = page.getByTestId('personnel-selection-dialog');
    await expect(picker).toBeVisible();
    await expect(picker.getByRole('heading', { name: '허창 태수 변경' })).toBeVisible();
    expect(await picker.evaluate((element) => getComputedStyle(element).transitionDuration)).toContain('0.15s');
    await expect(picker).toHaveCSS('transform', 'none');
    const pickerGeometry = await picker.evaluate((element) => {
        const rect = element.getBoundingClientRect();
        const style = getComputedStyle(element);
        return {
            left: rect.left,
            right: rect.right,
            bottom: rect.bottom,
            width: rect.width,
            maxHeight: style.maxHeight,
            borderTopLeftRadius: style.borderTopLeftRadius,
        };
    });
    expect(pickerGeometry.left).toBeGreaterThanOrEqual(0);
    expect(pickerGeometry.right).toBeLessThanOrEqual(500);
    expect(pickerGeometry.bottom).toBe(900);
    expect(pickerGeometry.width).toBeGreaterThan(480);
    expect(pickerGeometry.borderTopLeftRadius).toBe('16px');
    expect(
        await picker.evaluate(
            (element) =>
                [...element.querySelectorAll<HTMLElement>('*')].filter((child) =>
                    getComputedStyle(child).backgroundImage.includes('gradient')
                ).length
        )
    ).toBe(0);
    await expect(picker.getByRole('button', { name: /장료/ })).toContainText('허창 · 일반 장수');
    await expect(picker.getByRole('button', { name: /하후돈/ })).toContainText('현재 임명 중');
    await picker.getByRole('button', { name: /장료/ }).focus();
    expect(
        await picker.getByRole('button', { name: /장료/ }).evaluate((button) => getComputedStyle(button).outlineStyle)
    ).not.toBe('none');
    await screenshot(page, 'core-personnel-mobile-city-picker.png');
    page.once('dialog', async (dialog) => {
        expect(dialog.message()).toBe('장료를 허창 태수직에 임명하시겠습니까?');
        await dialog.accept();
    });
    await picker.getByRole('button', { name: /장료/ }).click();
    await expect(page.getByTestId('game-toast')).toContainText('장료를 임명했습니다.');
    expect(state.appointedGeneralId).toBe(6);
    expect(state.appointedCityId).toBe(1);
    expect(state.appointedOfficerLevel).toBe(4);

    await page.setViewportSize({ width: 390, height: 844 });
    await page.waitForTimeout(250);
    expect(
        await page.locator('#personnel-container').evaluate((element) => element.getBoundingClientRect().width)
    ).toBe(390);
    const overflowContributors = await page.evaluate(() =>
        [...document.querySelectorAll<HTMLElement>('body *')]
            .map((element) => {
                const rect = element.getBoundingClientRect();
                return {
                    element: `${element.tagName.toLowerCase()}#${element.id}.${element.className}`,
                    parent: `${element.parentElement?.tagName.toLowerCase() ?? ''}#${element.parentElement?.id ?? ''}.${element.parentElement?.className ?? ''}`,
                    left: rect.left,
                    right: rect.right,
                    width: rect.width,
                };
            })
            .filter((entry) => entry.right > window.innerWidth + 0.5 || entry.left < -0.5)
            .slice(0, 12)
    );
    expect(overflowContributors).toEqual([]);
    expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
    const narrowChiefWidth = await page
        .locator('.chief-entry-cell')
        .first()
        .evaluate((element) => element.getBoundingClientRect().width);
    expect(narrowChiefWidth).toBeGreaterThan(194);
    expect(narrowChiefWidth).toBeLessThan(196);
    await screenshot(page, 'core-personnel-mobile-rows.png');
});

test('personnel hides every mutation control for an ordinary member and exposes load errors', async ({ page }) => {
    await installFixture(page, { role: 'member', rate: 20 });
    await gotoOffice(page, 'nation/personnel');
    await expect(page.getByRole('button', { name: /변경하기/ })).toHaveCount(0);
    await expect(page.getByText('외 교 권 자 임 명')).toHaveCount(0);
    await expect(page.getByRole('combobox', { name: '추방 대상 장수' })).toHaveCount(0);
    await expect(page.getByRole('button', { name: '추방', exact: true })).toHaveCount(0);
    const auditorCell = page.locator('.city-officer-cell').filter({ hasText: '곽가' });
    await expect(auditorCell).toContainText('10년 · 허창');

    const failed = await page.context().newPage();
    await installFixture(failed, { role: 'member', rate: 20, failPersonnelLoad: true });
    await gotoOffice(failed, 'nation/personnel');
    await expect(failed.getByRole('alert')).toContainText('권한이 부족합니다.');
});

test('finance matches the reference 1000px and 500px computed DOM contracts', async ({ page }) => {
    await installFixture(page, { role: 'head', rate: 20 });
    await page.setViewportSize({ width: 1000, height: 900 });
    await gotoOffice(page, 'nation/finance');
    await expect(page.getByText('외교관계')).toBeVisible();
    const desktop = await page.locator('#finance-container').evaluate((container) => {
        const geometry = (selector?: string) => {
            const element = selector ? container.querySelector<HTMLElement>(selector)! : container;
            const rect = element.getBoundingClientRect();
            const style = getComputedStyle(element);
            return {
                width: rect.width,
                height: rect.height,
                gridTemplateColumns: style.gridTemplateColumns,
                fontFamily: style.fontFamily,
                fontSize: style.fontSize,
                lineHeight: style.lineHeight,
                backgroundColor: style.backgroundColor,
                backgroundImage: style.backgroundImage,
            };
        };
        return {
            container: geometry(),
            top: geometry('.top-back-bar'),
            title: geometry('.diplomacy-title'),
            row: geometry('.diplomacy-row'),
            notice: geometry('.notice-title'),
            finance: geometry('.finance-title'),
            input: geometry('input[type="number"]'),
        };
    });
    expect(desktop.container.width).toBe(1000);
    expect(desktop.container.fontFamily).toContain('Pretendard');
    expect(desktop.container.fontSize).toBe('14px');
    expect(desktop.container.backgroundImage).toContain('back_walnut.jpg');
    expect(desktop.top.height).toBe(32);
    expect(desktop.top.gridTemplateColumns).toBe('90px 90px 640px 90px 90px');
    expect(desktop.title.height).toBeCloseTo(25.47, 1);
    expect(desktop.title.backgroundColor).toBe('rgb(55, 90, 127)');
    expect(desktop.row.gridTemplateColumns).toBe(
        '260.859px 130.438px 86.9531px 86.9531px 173.922px 86.9531px 173.922px'
    );
    expect(desktop.notice.backgroundColor).toBe('rgb(255, 255, 255)');
    expect(desktop.finance.backgroundColor).toBe('rgb(0, 188, 140)');
    expect(desktop.input.width).toBeCloseTo(58.66, 1);
    expect(desktop.input.height).toBe(30);
    expect(
        await page
            .locator('.policy-submit')
            .first()
            .evaluate((element) => getComputedStyle(element).backgroundColor)
    ).toBe('rgb(55, 90, 127)');
    expect(
        await page
            .locator('.policy-cancel')
            .first()
            .evaluate((element) => getComputedStyle(element).backgroundColor)
    ).toBe('rgb(108, 117, 125)');
    expect(
        await page
            .getByRole('checkbox', { name: '전쟁 금지' })
            .evaluate((element) => getComputedStyle(element).borderRadius)
    ).toBe('16px');
    await screenshot(page, 'core-finance-desktop-head.png');

    await page.setViewportSize({ width: 500, height: 900 });
    await page.reload();
    await expect(page.getByText('외교관계')).toBeVisible();
    expect(await page.locator('#finance-container').evaluate((element) => element.getBoundingClientRect().width)).toBe(
        500
    );
    expect(
        await page.locator('.top-back-bar').evaluate((element) => getComputedStyle(element).gridTemplateColumns)
    ).toBe('90px 90px 140px 90px 90px');
    expect(
        await page
            .locator('.diplomacy-row')
            .first()
            .evaluate((element) => getComputedStyle(element).gridTemplateColumns)
    ).toBe('130.422px 65.2188px 43.4844px 43.4688px 86.9688px 43.4688px 86.9688px');
    await screenshot(page, 'core-finance-mobile-head.png');
});

test('finance enforces edit permissions and preserves the old value across an API failure', async ({ page }) => {
    const state: FixtureState = { role: 'head', rate: 20, failNextRate: true };
    await installFixture(page, state);
    await gotoOffice(page, 'nation/finance');
    const input = page.getByRole('spinbutton', { name: '세율' });
    await input.fill('25');
    await page.locator('.policy-cell').filter({ hasText: '세율' }).getByRole('button', { name: '변경' }).click();
    await expect(page.getByRole('alert')).toContainText('세율을 변경할 수 없습니다.');
    await expect(input).toHaveValue('20');
    await input.fill('25');
    await page.locator('.policy-cell').filter({ hasText: '세율' }).getByRole('button', { name: '변경' }).click();
    await expect(page.getByRole('status')).toContainText('세율을 변경했습니다.');
    expect(state.rate).toBe(25);

    const readOnly = await page.context().newPage();
    await installFixture(readOnly, { role: 'member', rate: 20 });
    await gotoOffice(readOnly, 'nation/finance');
    await expect(readOnly.getByRole('button', { name: '국가방침 수정' })).toHaveCount(0);
    await expect(readOnly.locator('.policy-cell').getByRole('button', { name: '변경' })).toHaveCount(0);
    await expect(readOnly.getByRole('checkbox', { name: '전쟁 금지' })).toBeDisabled();
});

test('finance editor preserves Ref formatting controls and uploads images through sam-image', async ({ page }) => {
    const state: FixtureState = { role: 'head', rate: 20 };
    await installFixture(page, state);
    await page.setViewportSize({ width: 1000, height: 900 });
    await gotoOffice(page, 'nation/finance');
    await page.getByRole('button', { name: '국가방침 수정' }).click();

    const editor = page.getByRole('textbox', { name: '국가 방침' });
    const editorFrame = page.locator('#notice-form .legacy-html-editor');
    await expect(editor).toBeVisible();
    expect(await editorFrame.evaluate((element) => getComputedStyle(element).backgroundColor)).toBe('rgba(0, 0, 0, 0)');
    expect(await editor.evaluate((element) => getComputedStyle(element).backgroundColor)).toBe('rgba(0, 0, 0, 0)');

    await editor.fill('서식 검증');
    await editor.press('Control+A');
    await page.getByRole('combobox', { name: '글꼴', exact: true }).selectOption('Gungsuh, serif');
    await page.getByRole('combobox', { name: '글꼴 크기' }).selectOption('22px');
    await page.getByLabel('글자색').evaluate((element) => {
        const input = element as HTMLInputElement;
        input.value = '#123456';
        input.dispatchEvent(new Event('input', { bubbles: true }));
    });
    await page.getByLabel('배경색').evaluate((element) => {
        const input = element as HTMLInputElement;
        input.value = '#fedcba';
        input.dispatchEvent(new Event('input', { bubbles: true }));
    });
    await page.getByRole('button', { name: '가운데 정렬' }).click();
    await expect(page.getByRole('button', { name: '가운데 정렬' })).toHaveClass(/active/);

    const formattedText = editor.locator('span', { hasText: '서식 검증' });
    await expect(formattedText).toHaveCSS('font-family', /Gungsuh/);
    await expect(formattedText).toHaveCSS('font-size', '22px');
    await expect(formattedText).toHaveCSS('color', 'rgb(18, 52, 86)');
    await expect(formattedText).toHaveCSS('background-color', 'rgb(254, 220, 186)');
    await expect(editor.locator('p')).toHaveCSS('text-align', 'center');

    await editor.press('End');
    await page.getByRole('button', { name: '구분선' }).click();
    await page.getByLabel('업로드할 이미지').setInputFiles({
        name: '방침.png',
        mimeType: 'image/png',
        buffer: Buffer.from(
            'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
            'base64'
        ),
    });

    await expect.poll(() => state.uploadDataUrl).toMatch(/^data:image\/png;base64,/);
    await expect(editor.locator('hr')).toHaveCount(1);
    await expect(editor.locator('img')).toHaveAttribute(
        'src',
        'https://sam-image.hided.net/uploads/core2026/0123456789abcdef0123456789abcdef.webp'
    );

    const imageButton = page.getByRole('button', { name: '이미지', exact: true });
    await imageButton.hover();
    await expect(imageButton).toHaveCSS('border-color', 'rgb(157, 200, 240)');
    await page.getByRole('combobox', { name: '글꼴', exact: true }).focus();
    await expect(page.getByRole('combobox', { name: '글꼴', exact: true })).toHaveCSS('outline-style', 'solid');
    await screenshot(page, 'core-finance-editor-desktop.png');

    await page.setViewportSize({ width: 500, height: 900 });
    const mobileGeometry = await editorFrame.evaluate((element) => ({
        width: element.getBoundingClientRect().width,
        scrollWidth: element.scrollWidth,
        toolbarHeight: element.querySelector('[role="toolbar"]')?.getBoundingClientRect().height ?? 0,
    }));
    expect(mobileGeometry.width).toBe(500);
    expect(mobileGeometry.scrollWidth).toBeLessThanOrEqual(500);
    expect(mobileGeometry.toolbarHeight).toBeGreaterThan(24);
    await screenshot(page, 'core-finance-editor-mobile.png');

    await page.locator('#notice-form').getByRole('button', { name: '저장' }).click();
    await expect.poll(() => state.noticeMutationInput).toContain('font-family: Gungsuh, serif');
    expect(state.noticeMutationInput).toContain('font-size: 22px');
    expect(state.noticeMutationInput).toContain('color: rgb(18, 52, 86)');
    expect(state.noticeMutationInput).toContain('background-color: rgb(254, 220, 186)');
    expect(state.noticeMutationInput).toContain('text-align: center');
    expect(state.noticeMutationInput).toContain('<hr>');
    expect(state.noticeMutationInput).toContain(
        'https://sam-image.hided.net/uploads/core2026/0123456789abcdef0123456789abcdef.webp'
    );
});

test('finance adopts the server-purified notice and scout message before rendering the saved preview', async ({
    page,
}) => {
    const state: FixtureState = { role: 'head', rate: 20 };
    await installFixture(page, state);
    await gotoOffice(page, 'nation/finance');

    const dirtyNotice =
        '<script>globalThis.__nationNoticeXss=1</script><img src=x onerror="globalThis.__nationNoticeXss=2"><p data-flip="horizontal" style="color:#00ffff">원문</p>';
    await page.getByRole('button', { name: '국가방침 수정' }).click();
    await page.getByRole('textbox', { name: '국가 방침' }).fill(dirtyNotice);
    await page.locator('#notice-form').getByRole('button', { name: '저장' }).click();

    const noticePreview = page.locator('#notice-form .message-preview');
    await expect(noticePreview).toContainText('서버 정화 방침');
    expect(state.noticeMutationInput).not.toContain('<script>');
    expect(state.noticeMutationInput).toContain('&lt;script&gt;');
    expect(state.noticeMutationInput).toContain('&lt;img src=x onerror=');
    await expect(noticePreview.locator('[data-flip="horizontal"]')).toHaveCSS('color', 'rgb(0, 255, 255)');
    await expect(noticePreview.locator('script, svg, [onerror], [onload], [onclick]')).toHaveCount(0);
    await expect
        .poll(() =>
            page.evaluate(() => (globalThis as typeof globalThis & { __nationNoticeXss?: number }).__nationNoticeXss)
        )
        .toBeUndefined();

    const dirtyScout =
        '<svg onload="globalThis.__nationScoutXss=1"></svg><a href="javascript:alert(1)" onclick="globalThis.__nationScoutXss=2">원문</a>';
    await page.getByRole('button', { name: '임관 권유문 수정' }).click();
    await page.getByRole('textbox', { name: '임관 권유' }).fill(dirtyScout);
    await page.locator('#scout-message-form').getByRole('button', { name: '저장' }).click();

    const scoutPreview = page.locator('#scout-message-form .message-preview');
    await expect(scoutPreview).toContainText('서버 정화 임관문');
    expect(state.scoutMutationInput).not.toContain('<svg');
    expect(state.scoutMutationInput).toContain('&lt;svg onload=');
    expect(state.scoutMutationInput).toContain('&lt;a href="javascript:alert(1)"');
    await expect(scoutPreview.locator('a', { hasText: '위험 링크' })).not.toHaveAttribute('href');
    await expect(scoutPreview.locator('script, svg, [onerror], [onload], [onclick]')).toHaveCount(0);
    await expect
        .poll(() =>
            page.evaluate(() => (globalThis as typeof globalThis & { __nationScoutXss?: number }).__nationScoutXss)
        )
        .toBeUndefined();
});
