import { expect, test, type Page, type Route } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { canonicalFrontendFixture as fixture } from './fixtures/canonical.js';
import { gameUrl, gatewayUrl } from './testUrls.js';

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const imageRoots = [resolve(repositoryRoot, '../image'), resolve(repositoryRoot, '../../image')];
const artifactRoot = process.env.FRONTEND_PARITY_ARTIFACT_DIR;

const response = (data: unknown) => ({ result: { data } });
const errorResponse = (path: string, message: string) => ({
    error: {
        message,
        code: -32000,
        data: { code: 'INTERNAL_SERVER_ERROR', httpStatus: 500, path },
    },
});

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

const fulfill = async (route: Route, results: unknown[]) => {
    await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify(results),
    });
};

const installImages = async (page: Page) => {
    await page.route('**/image/**', async (route) => {
        const pathname = decodeURIComponent(new URL(route.request().url()).pathname);
        const relative = pathname.replace(/^\/image\//, '');
        const candidates = imageRoots.flatMap((root) => [resolve(root, relative), resolve(root, 'game', relative)]);
        for (const candidate of candidates) {
            try {
                const body = await readFile(candidate);
                await route.fulfill({
                    status: 200,
                    contentType: extname(candidate).toLowerCase() === '.png' ? 'image/png' : 'image/jpeg',
                    body,
                });
                return;
            } catch {
                // Worktree 위치에 따라 다음 image root를 확인한다.
            }
        }
        await route.abort('failed');
    });
};

const cachedHistory = [
    { id: 31, text: '<Y>유비</>가 <C>촉</>을 건국하였습니다.' },
    { id: 30, text: '<S>낙양</>에 새로운 소식이 있습니다.' },
];

const frontRecords = {
    global: [
        { id: 51, text: '<Y>관우</>가 장수 동향을 남겼습니다.' },
        { id: 50, text: '<C>조조</>가 이동하였습니다.' },
    ],
    general: [{ id: 41, text: '<L>유비</>의 개인 기록입니다.' }],
    history: cachedHistory,
};

const generalContext = {
    general: {
        id: 1,
        name: '유비',
        npcState: 0,
        nationId: 1,
        cityId: 1,
        troopId: 0,
        picture: null,
        imageServer: 0,
        officerLevel: 1,
        stats: { leadership: 80, strength: 70, intelligence: 75 },
        gold: 1000,
        rice: 1200,
        crew: 500,
        train: 80,
        atmos: 90,
        injury: 0,
        experience: 100,
        dedication: 200,
        items: { horse: null, weapon: null, book: null, item: null },
    },
    city: {
        id: 1,
        name: '낙양',
        level: 7,
        nationId: 1,
        population: 10000,
        agriculture: 100,
        commerce: 100,
        security: 100,
        defence: 100,
        wall: 100,
        supplyState: 1,
        frontState: 0,
    },
    nation: {
        id: 1,
        name: '촉',
        color: '#d32f2f',
        level: 3,
        gold: 10000,
        rice: 12000,
        tech: 500,
        typeCode: 'che_법가',
        capitalCityId: 1,
    },
    settings: {},
    penalties: {},
};

const emptyMessages = {
    private: [],
    public: [],
    national: [],
    diplomacy: [],
    permission: 0,
    latestRead: { private: 0, diplomacy: 0 },
    canRespondDiplomacy: false,
};

const installGatewayMapFixture = async (page: Page) => {
    await installImages(page);
    await page.route('**/gateway/api/trpc/**', async (route) => {
        const results = operationNames(route).map((operation) => {
            if (operation === 'me') return response(null);
            if (operation === 'lobby.profiles') return response([fixture.gateway.profile]);
            return response(null);
        });
        await fulfill(route, results);
    });
    await page.route('**/che/api/trpc/**', async (route) => {
        const results = operationNames(route).map((operation) => {
            if (operation === 'auth.status') return response({ userId: 'frontend-parity-user' });
            if (operation === 'lobby.info') return response(fixture.game.lobby);
            if (operation === 'public.getMapLayout') return response(fixture.game.mapLayout);
            if (operation === 'public.getCachedMap') {
                return response({ ...fixture.game.map, history: cachedHistory });
            }
            return errorResponse(operation, `Unhandled gateway map operation: ${operation}`);
        });
        await fulfill(route, results);
    });
};

const installMainFixture = async (page: Page, failRecords = false) => {
    await installImages(page);
    await page.addInitScript(
        ({ token, profile }) => {
            window.localStorage.setItem('sammo-game-token', token);
            window.localStorage.setItem('sammo-game-profile', profile);
        },
        { token: fixture.game.session.gameToken, profile: fixture.game.session.profile }
    );
    await page.route('**/che/api/trpc/**', async (route) => {
        const results = operationNames(route).map((operation) => {
            if (operation === 'auth.status') return response({ userId: 'frontend-parity-user' });
            if (operation === 'lobby.info') {
                return response({ ...fixture.game.lobby, myGeneral: fixture.game.session.general });
            }
            if (operation === 'general.me') return response(generalContext);
            if (operation === 'world.getMapLayout') return response(fixture.game.mapLayout);
            if (operation === 'world.getMap') {
                return response({
                    ...fixture.game.map,
                    spyList: {},
                    shownByGeneralList: [],
                    myCity: 1,
                    myNation: 1,
                });
            }
            if (operation === 'public.getMapLayout') return response(fixture.game.mapLayout);
            if (operation === 'public.getCachedMap') {
                return response({ ...fixture.game.map, history: cachedHistory });
            }
            if (operation === 'public.getWorldTrend') {
                return response({ year: 200, month: 1, turnTerm: 10 });
            }
            if (operation === 'public.getNationList') return response([]);
            if (operation === 'public.getGeneralList') return response([]);
            if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
            if (operation === 'turns.reserved.getGeneral') return response([]);
            if (operation === 'messages.getRecent') return response(emptyMessages);
            if (operation === 'messages.getContacts') return response([]);
            if (operation === 'board.getAccess') {
                return response({ canMeeting: false, canSecret: false, permission: 0 });
            }
            if (operation === 'general.getRecentRecords') {
                return failRecords
                    ? errorResponse(operation, '동향 정보를 불러오지 못했습니다.')
                    : response(frontRecords);
            }
            if (operation === 'general.getFrontStatus') {
                return response({
                    onlineUserCount: 1,
                    onlineNations: '【재야】',
                    onlineGenerals: '지도 장수',
                    nationNotice: '',
                    lastExecuted: '2026-07-26T00:00:00.000Z',
                    latestVote: null,
                });
            }
            if (operation === 'tournament.getState') return response({ stage: 0 });
            if (operation === 'public.recordAccess') return response({ recorded: true });
            return errorResponse(operation, `Unhandled main operation: ${operation}`);
        });
        await fulfill(route, results);
    });
};

test('shows the representative cached map and cached history before login', async ({ page }) => {
    await installGatewayMapFixture(page);
    await page.setViewportSize({ width: 1280, height: 900 });
    await page.goto(gatewayUrl());

    await expect(page.locator('.map-preview-body')).toBeVisible();
    await expect(page.locator('.status-history')).toContainText('유비가 촉을 건국하였습니다.');
    const historyStyle = await page.locator('.status-history').evaluate((element) => {
        const rect = element.getBoundingClientRect();
        const style = getComputedStyle(element);
        return { width: rect.width, fontFamily: style.fontFamily, fontSize: style.fontSize };
    });
    expect(historyStyle.width).toBeCloseTo(698, 0);
    expect(historyStyle.fontFamily).toContain('Times New Roman');
    expect(historyStyle.fontSize).toBe('14px');
    await expect(page.locator('.status-history span').first()).toHaveCSS('color', 'rgb(255, 255, 0)');

    if (artifactRoot) {
        await page.screenshot({
            path: resolve(artifactRoot, 'gateway-cached-map-history.png'),
            fullPage: true,
            animations: 'disabled',
        });
    }
});

test('shows the current in-game map and all three recent record streams', async ({ page }) => {
    await installMainFixture(page);
    await page.setViewportSize({ width: 1440, height: 1000 });
    await page.goto(gameUrl());

    await expect(page.locator('.map-area')).toBeVisible();
    await expect(page.getByText('관우가 장수 동향을 남겼습니다.')).toBeVisible();
    await expect(page.getByText('유비의 개인 기록입니다.')).toBeVisible();
    await expect(page.getByText('유비가 촉을 건국하였습니다.')).toBeVisible();
    await expect(page.getByText('실시간 스트림으로 연결 예정')).toHaveCount(0);

    const geometry = await page.locator('.map-area').evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return { width: rect.width, height: rect.height };
    });
    expect(geometry).toEqual({ width: 700, height: 500 });
    await expect(page.locator('[data-record-bucket="global"] .record-line span').first()).toHaveCSS(
        'color',
        'rgb(255, 255, 0)'
    );

    if (artifactRoot) {
        await page.screenshot({
            path: resolve(artifactRoot, 'ingame-map-trends-desktop.png'),
            fullPage: true,
            animations: 'disabled',
        });
    }
});

test('uses the shared content font for public game history on desktop and mobile', async ({ page }) => {
    await installMainFixture(page);

    for (const viewport of [
        { width: 1200, height: 900 },
        { width: 500, height: 900 },
    ]) {
        await page.setViewportSize(viewport);
        await page.goto(gameUrl('/public'));
        await expect(page.locator('.recent-log-list')).toContainText('유비가 촉을 건국하였습니다.');

        const font = await page.locator('.recent-log-list').evaluate((element) => {
            const style = getComputedStyle(element);
            return { family: style.fontFamily, size: style.fontSize };
        });
        expect(font.family).toContain('Pretendard');
        expect(font.size).toBe('14px');

        const pretendardFont = await page.evaluate(async () => {
            await document.fonts.load('400 14px Pretendard', '유비가 촉을 건국하였습니다.');
            await document.fonts.ready;
            const statuses = [...document.fonts]
                .filter((face) => face.family.replaceAll('"', '') === 'Pretendard')
                .map((face) => face.status);
            return {
                statuses,
                koreanGlyphsLoaded: document.fonts.check('400 14px Pretendard', '유비가 촉을 건국하였습니다.'),
            };
        });
        expect(pretendardFont.statuses).toContain('loaded');
        expect(pretendardFont.koreanGlyphsLoaded).toBe(true);
    }
});

test('keeps the current map visible when the recent record request fails', async ({ page }) => {
    await installMainFixture(page, true);
    await page.setViewportSize({ width: 1440, height: 1000 });
    await page.goto(gameUrl());

    await expect(page.locator('.map-area')).toBeVisible();
    await expect(page.getByRole('alert').first()).toContainText('동향 정보를 불러오지 못했습니다.');
    await expect(page.getByRole('link', { name: '낙양', exact: true })).toBeVisible();
});

test('shows map and trend tabs in the mobile in-game layout', async ({ page }) => {
    await installMainFixture(page);
    await page.setViewportSize({ width: 500, height: 900 });
    await page.goto(gameUrl());

    await expect(page.locator('.map-area')).toBeVisible();
    const mapGeometry = await page.locator('.map-area').evaluate((element) => {
        const rect = element.getBoundingClientRect();
        return { width: rect.width, height: rect.height };
    });
    expect(mapGeometry.width).toBe(438);
    expect(mapGeometry.height).toBeCloseTo(312.86, 1);
    expect(mapGeometry.width / mapGeometry.height).toBeCloseTo(7 / 5, 2);

    await page.getByRole('button', { name: '동향', exact: true }).click();
    await expect(page.getByText('관우가 장수 동향을 남겼습니다.')).toBeVisible();
    await expect(page.getByText('유비의 개인 기록입니다.')).toBeVisible();
    await expect(page.getByText('유비가 촉을 건국하였습니다.')).toBeVisible();

    if (artifactRoot) {
        await page.screenshot({
            path: resolve(artifactRoot, 'ingame-map-trends-mobile.png'),
            fullPage: true,
            animations: 'disabled',
        });
    }
});
