import { randomUUID } from 'node:crypto';

import { expect, test, type Page, type Route } from '@playwright/test';
import {
    createGamePostgresConnector,
    createRedisConnector,
    GamePrisma,
    resolvePostgresConfigFromEnv,
    resolveRedisConfigFromEnv,
    type GamePrismaClient,
    type RedisConnector,
} from '../../packages/infra/src/index.js';

const marker = `main-front-status-live-${randomUUID()}`;
const accessToken = `ga_${randomUUID()}`;
let postgres: ReturnType<typeof createGamePostgresConnector>;
let redis: RedisConnector;
let prisma: GamePrismaClient;
let fixture: { generalId: number; freeGeneralId: number; nationId: number; userId: string; voteId: number } | null =
    null;

const accessKey = (token: string) => `sammo:game:access:che:default:${token}`;
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 = decodeURIComponent(new URL(route.request().url()).pathname);
    return pathname.slice(pathname.lastIndexOf('/trpc/') + 6).split(',');
};

test.beforeAll(async () => {
    postgres = createGamePostgresConnector(resolvePostgresConfigFromEnv({ schema: 'che' }));
    redis = createRedisConnector(resolveRedisConfigFromEnv());
    await postgres.connect();
    await redis.connect();
    prisma = postgres.prisma;

    const world = await prisma.worldState.findFirst({ orderBy: { id: 'asc' }, select: { id: true } });
    if (!world) {
        throw new Error('The live che profile needs a world state.');
    }
    const [generalMax, nationMax] = await Promise.all([
        prisma.general.aggregate({ _max: { id: true } }),
        prisma.nation.aggregate({ _max: { id: true } }),
    ]);
    const nationId = (nationMax._max.id ?? 0) + 10_000;
    const generalId = (generalMax._max.id ?? 0) + 10_000;
    const freeGeneralId = generalId + 1;
    const userId = `main-front-status-${randomUUID()}`;
    const freeUserId = `main-front-status-free-${randomUUID()}`;
    await prisma.$executeRaw(
        GamePrisma.sql`
            INSERT INTO nation (id, name, color, meta)
            VALUES (
                ${nationId},
                ${'검증국'},
                ${'#336699'},
                ${JSON.stringify({ notice: `<p>${marker} 국가 방침</p>` })}::jsonb
            )
        `
    );
    await prisma.$executeRaw(
        GamePrisma.sql`
            INSERT INTO general (id, user_id, name, nation_id, city_id, turn_time)
            VALUES
                (${generalId}, ${userId}, ${'현황검증장수'}, ${nationId}, ${0}, ${new Date()}),
                (${freeGeneralId}, ${freeUserId}, ${'재야행동검증장수'}, ${0}, ${0}, ${new Date()})
        `
    );
    const actionAt = new Date();
    await prisma.generalAccessLog.createMany({
        data: [
            {
                generalId,
                userId,
                lastRefresh: actionAt,
                lastActionAt: actionAt,
                refresh: 1,
                refreshTotal: 1,
                refreshScore: 1,
                refreshScoreTotal: 1,
            },
            {
                generalId: freeGeneralId,
                userId: freeUserId,
                lastRefresh: actionAt,
                lastActionAt: actionAt,
                refresh: 1,
                refreshTotal: 1,
                refreshScore: 1,
                refreshScoreTotal: 1,
            },
        ],
    });
    const poll = await prisma.votePoll.create({
        data: {
            title: '검증 설문',
            body: '',
            options: ['찬성', '반대'],
            multipleOptions: 1,
            revealMode: 'after_vote',
            openerGeneralId: generalId,
            openerName: '현황검증장수',
            startAt: new Date(Date.now() - 60_000),
            endAt: new Date(Date.now() + 3_600_000),
        },
        select: { id: true },
    });
    fixture = { generalId, freeGeneralId, nationId, userId, voteId: poll.id };

    const issuedAt = new Date();
    await redis.client.set(
        accessKey(accessToken),
        JSON.stringify({
            version: 1,
            profile: 'che:default',
            issuedAt: issuedAt.toISOString(),
            expiresAt: new Date(issuedAt.getTime() + 30 * 60 * 1000).toISOString(),
            sessionId: `main-front-status-${randomUUID()}`,
            user: {
                id: userId,
                username: 'main-front-status-live',
                displayName: '현황검증장수',
                roles: [],
            },
            sanctions: {},
        }),
        { EX: 1800 }
    );
});

test.afterAll(async () => {
    if (fixture) {
        await prisma.votePoll.deleteMany({ where: { id: fixture.voteId } });
        const generalIds = [fixture.generalId, fixture.freeGeneralId];
        await prisma.generalAccessLog.deleteMany({ where: { generalId: { in: generalIds } } });
        await prisma.general.deleteMany({ where: { id: { in: generalIds } } });
        await prisma.$executeRaw(GamePrisma.sql`DELETE FROM nation WHERE id = ${fixture.nationId}`);
        const [generalCount, nationCount, voteCount, accessCount] = await Promise.all([
            prisma.general.count({ where: { id: { in: generalIds } } }),
            prisma.nation.count({ where: { id: fixture.nationId } }),
            prisma.votePoll.count({ where: { id: fixture.voteId } }),
            prisma.generalAccessLog.count({ where: { generalId: { in: generalIds } } }),
        ]);
        expect([generalCount, nationCount, voteCount, accessCount]).toEqual([0, 0, 0, 0]);
    }
    await redis.client.del(accessKey(accessToken));
    await redis.disconnect();
    await postgres.disconnect();
});

const installMainFixture = async (
    page: Page,
    liveStatus: {
        onlineUserCount: number;
        onlineNations: string;
        onlineGenerals: string;
        nationNotice: string;
        lastExecuted: string | null;
        latestVote: { id: number; title: string; hasVoted: boolean } | null;
    },
    failStatus: () => boolean
) => {
    const generalContext = {
        general: {
            id: fixture?.generalId ?? 1,
            name: '현황검증장수',
            npcState: 0,
            nationId: fixture?.nationId ?? 1,
            cityId: 1,
            troopId: 0,
            picture: null,
            imageServer: 0,
            officerLevel: 0,
            stats: { leadership: 55, strength: 55, intelligence: 55 },
            gold: 1000,
            rice: 1000,
            crew: 0,
            train: 0,
            atmos: 0,
            injury: 0,
            experience: 0,
            dedication: 0,
            items: { horse: null, weapon: null, book: null, item: null },
        },
        city: null,
        nation: null,
        settings: {},
        penalties: {},
    };
    await page.addInitScript(
        ({ token }) => {
            window.localStorage.setItem('sammo-game-token', token);
            window.localStorage.setItem('sammo-game-profile', 'che:default');
        },
        { token: accessToken }
    );
    await page.route('**/che/api/trpc/**', async (route) => {
        const results = operationNames(route).map((operation) => {
            if (operation === 'dashboard.getContextBundleDelta') {
                return response({
                    context: {
                        kind: 'snapshot',
                        revision: 'AAAAAAAAAAAAAAAAAAAAAA',
                        data: generalContext,
                    },
                    commandTable: {
                        kind: 'snapshot',
                        revision: 'BBBBBBBBBBBBBBBBBBBBBB',
                        data: { general: [], nation: [] },
                    },
                    boardAccess: {
                        kind: 'snapshot',
                        revision: 'CCCCCCCCCCCCCCCCCCCCCC',
                        data: { canMeeting: false, canSecret: false, permission: 0 },
                    },
                });
            }
            if (operation === 'general.me') {
                return response(generalContext);
            }
            if (operation === 'general.getFrontStatus') {
                return failStatus()
                    ? errorResponse(operation, '상단 현황을 불러오지 못했습니다.')
                    : response(liveStatus);
            }
            if (operation === 'general.getRecentRecords') return response({ global: [], general: [], history: [] });
            if (operation === 'auth.status') return response({ userId: 'frontend-parity-user' });
            if (operation === 'lobby.info') {
                return response({
                    year: 190,
                    month: 1,
                    userCnt: 1,
                    npcCnt: 0,
                    nationCnt: 1,
                    maxUserCnt: 50,
                    turnTerm: 60,
                    fictionMode: '가상',
                    otherTextInfo: '',
                    starttime: '0190-01-01T00:00:00.000Z',
                    myGeneral: { name: '현황검증장수', picture: null },
                });
            }
            if (operation === 'world.getMapLayout') {
                return response({
                    mapName: 'che',
                    cityList: [{ id: 1, name: '낙양', level: 7, region: 1, x: 350, y: 245, path: [] }],
                    regionMap: { 1: '사예' },
                    levelMap: { 7: '수도' },
                });
            }
            if (operation === 'world.getState') {
                return response({
                    currentYear: 190,
                    currentMonth: 1,
                    tickSeconds: 60,
                    config: { npcMode: 0, const: {}, environment: {} },
                    meta: {},
                });
            }
            if (operation === 'world.getMap') {
                return response({
                    year: 190,
                    month: 1,
                    startYear: 190,
                    cityList: [[1, 7, 0, 0, 1, 1]],
                    nationList: [],
                    spyList: {},
                    shownByGeneralList: [],
                    myCity: 1,
                    myNation: fixture?.nationId ?? 1,
                });
            }
            if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
            if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
                return response([]);
            }
            if (operation === 'messages.getRecent') {
                return response({
                    private: [],
                    public: [],
                    national: [],
                    diplomacy: [],
                    permission: 0,
                    latestRead: { private: 0, diplomacy: 0 },
                    canRespondDiplomacy: false,
                });
            }
            if (operation === 'messages.getContacts') return response([]);
            if (operation === 'board.getAccess') {
                return response({ canMeeting: false, canSecret: false, permission: 0 });
            }
            if (operation === 'tournament.getState') return response({ stage: 0 });
            if (operation === 'public.recordAccess') return response({ recorded: true });
            throw new Error(`Unhandled main front status operation: ${operation}`);
        });
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(results),
        });
    });
};

test('renders actual online, nation policy, and survey data with ref geometry and preserves it on error', async ({
    page,
}) => {
    const liveResponse = await page.request.get(
        'http://127.0.0.1:15113/che/api/trpc/general.getFrontStatus?input=%7B%22json%22%3Anull%7D',
        { headers: { authorization: `Bearer ${accessToken}` } }
    );
    expect(liveResponse.ok()).toBe(true);
    const liveStatus = (
        (await liveResponse.json()) as {
            result: {
                data: {
                    onlineUserCount: number;
                    onlineNations: string;
                    onlineGenerals: string;
                    nationNotice: string;
                    lastExecuted: string | null;
                    latestVote: { id: number; title: string; hasVoted: boolean } | null;
                };
            };
        }
    ).result.data;
    expect(liveStatus.onlineUserCount).toBe(2);
    expect(liveStatus.onlineNations).toContain('【검증국】');
    expect(liveStatus.onlineNations).not.toContain('재야');
    expect(liveStatus.onlineGenerals).toContain('현황검증장수');
    expect(liveStatus.nationNotice).toContain(marker);
    expect(liveStatus.latestVote).toMatchObject({ title: '검증 설문', hasVoted: false });

    let failStatus = false;
    await installMainFixture(page, liveStatus, () => failStatus);
    await page.setViewportSize({ width: 1000, height: 900 });
    await page.goto('./', { waitUntil: 'networkidle' });

    const status = page.locator('.front-status');
    await expect(status).toContainText(marker);
    await expect(page.locator('.online-users')).toContainText('현황검증장수');
    await expect(page.locator('.survey-notice')).toContainText('새로운 설문조사가 있습니다.');
    await expect(page.locator('.execution-status')).toContainText('동작 시각:');
    await expect(page.locator('.tournament-status')).toHaveText('토너먼트: 경기 없음');
    await expect(page.locator('.vote-status')).toHaveText('설문: 검증 설문');
    const desktop = await status.evaluate((element) => {
        const style = getComputedStyle(element);
        const onlineRow = element.querySelector<HTMLElement>('.online-nations');
        const executionRow = element.querySelector<HTMLElement>('.execution-status');
        const voteRow = element.querySelector<HTMLElement>('.vote-status');
        const tournamentRow = element.querySelector<HTMLElement>('.tournament-status');
        if (!onlineRow || !executionRow || !voteRow || !tournamentRow) throw new Error('status row missing');
        return {
            rect: element.getBoundingClientRect().toJSON(),
            fontSize: style.fontSize,
            lineHeight: style.lineHeight,
            backgroundImage: style.backgroundImage,
            onlineRow: {
                rect: onlineRow.getBoundingClientRect().toJSON(),
                borderTop: getComputedStyle(onlineRow).borderTop,
                padding: getComputedStyle(onlineRow).padding,
            },
            executionRow: executionRow.getBoundingClientRect().toJSON(),
            tournamentRow: tournamentRow.getBoundingClientRect().toJSON(),
            voteRow: voteRow.getBoundingClientRect().toJSON(),
        };
    });
    expect(desktop.rect).toMatchObject({ x: 0, width: 1000 });
    expect(desktop).toMatchObject({
        fontSize: '14px',
        lineHeight: '21px',
        onlineRow: {
            borderTop: '1px solid rgb(128, 128, 128)',
            padding: '7px',
        },
    });
    expect(desktop.onlineRow.rect.height).toBeCloseTo(36, 0);
    expect(desktop.executionRow.x).toBeCloseTo(0, 0);
    expect(desktop.executionRow.width).toBeCloseTo(333.33, 0);
    expect(desktop.executionRow.height).toBeCloseTo(36, 0);
    expect(desktop.tournamentRow.x).toBeCloseTo(333.33, 0);
    expect(desktop.tournamentRow.width).toBeCloseTo(333.33, 0);
    expect(desktop.tournamentRow.height).toBeCloseTo(36, 0);
    expect(desktop.voteRow.x).toBeCloseTo(666.67, 0);
    expect(desktop.voteRow.width).toBeCloseTo(333.33, 0);
    expect(desktop.voteRow.height).toBeCloseTo(36, 0);
    expect(desktop.backgroundImage).toContain('/game/back_walnut.jpg');

    await page.setViewportSize({ width: 500, height: 900 });
    await expect(status).toHaveCSS('width', '500px');
    const mobileActivity = await page.locator('.activity-status').evaluate((element) => ({
        columns: getComputedStyle(element).gridTemplateColumns,
        widths: [...element.children].map((child) => child.getBoundingClientRect().width),
    }));
    expect(mobileActivity.columns.split(' ')).toHaveLength(3);
    for (const width of mobileActivity.widths) expect(width).toBeCloseTo(166.67, 0);

    failStatus = true;
    await page.getByRole('button', { name: '갱 신', exact: true }).click();
    await expect(page.getByRole('alert').filter({ hasText: '상단 현황을 불러오지 못했습니다.' })).toBeVisible();
    await expect(status).toContainText(marker);

    failStatus = false;
    await page.reload({ waitUntil: 'networkidle' });
    await expect(page.locator('.front-status')).toContainText(marker);
    await expect(page.locator('.survey-notice')).toHaveCount(0);
});
