import { describe, expect, it } from 'vitest';

import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import type { RedisConnector } from '@sammo-ts/infra';
import { LogCategory } from '@sammo-ts/logic';

import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
import type { DatabaseClient, GameApiContext, GameProfile } from '../src/context.js';
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
import { appRouter } from '../src/router.js';

const profile: GameProfile = {
    id: 'che',
    scenario: 'default',
    name: 'che:default',
};

const archiveServerId = 'hwe_260725_archive';
const currentServerId = 'che_260731_runtime';
const archiveRows = [
    {
        id: 1,
        profileName: archiveServerId,
        sourceId: 101,
        year: 200,
        month: 1,
        map: { year: 200, month: 1, startYear: 190, cityList: [], nationList: [] },
        nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1000, generalCount: 8, cities: ['성도'] }],
        globalHistory: ['<C>●</>1월: 기록 없음'],
        globalAction: ['<C>●</>1월: 기록 없음'],
        hash: 'archive-1',
        createdAt: new Date('2026-07-25T00:00:00.000Z'),
    },
    {
        id: 2,
        profileName: archiveServerId,
        sourceId: 102,
        year: 200,
        month: 2,
        map: { year: 200, month: 2, startYear: 190, cityList: [], nationList: [] },
        nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1200, generalCount: 9, cities: ['성도'] }],
        globalHistory: ['<C>●</>2월: 기록 없음'],
        globalAction: ['<C>●</>2월: 기록 없음'],
        hash: 'archive-2',
        createdAt: new Date('2026-07-25T01:00:00.000Z'),
    },
    {
        id: 3,
        profileName: currentServerId,
        sourceId: 0,
        year: 219,
        month: 12,
        map: { year: 219, month: 12, startYear: 190, cityList: [], nationList: [] },
        nations: [
            { id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] },
        ],
        globalHistory: ['저장된 현재 기수 과거 기록'],
        globalAction: ['저장된 현재 기수 과거 행동'],
        hash: 'current-archive',
        createdAt: new Date('2026-07-31T00:00:00.000Z'),
    },
    {
        id: 4,
        profileName: profile.id,
        sourceId: 201,
        year: 219,
        month: 11,
        map: { year: 219, month: 11, startYear: 190, cityList: [], nationList: [] },
        nations: [],
        globalHistory: ['레거시 프로필 별칭 기록'],
        globalAction: [],
        hash: 'legacy-profile-alias',
        createdAt: new Date('2026-07-30T00:00:00.000Z'),
    },
];

const authFor = (userId: string): GameSessionTokenPayload => ({
    version: 1,
    profile: profile.name,
    issuedAt: '2026-07-25T00:00:00.000Z',
    expiresAt: '2026-07-26T00:00:00.000Z',
    sessionId: `session-${userId}`,
    user: {
        id: userId,
        username: userId,
        displayName: userId,
        roles: [],
    },
    sanctions: {},
});

const buildContext = (
    auth: GameSessionTokenPayload | null,
    options: {
        hasGeneral?: boolean;
        worldMeta?: unknown;
        liveLogs?: { history: string[]; action: string[] };
        liveNations?: Array<Record<string, unknown>>;
        liveCities?: Array<Record<string, unknown>>;
        liveGenerals?: Array<Record<string, unknown>>;
    } = {}
): GameApiContext => {
    const db = {
        $queryRaw: async () => [],
        general: {
            findFirst: async ({ where }: { where: { userId: string } }) =>
                options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
            findMany: async () => options.liveGenerals ?? [],
        },
        city: {
            findMany: async () => options.liveCities ?? [],
        },
        nation: {
            findMany: async () => options.liveNations ?? [],
        },
        logEntry: {
            findMany: async ({ where }: { where: { category: unknown } }) => {
                if (where.category === LogCategory.HISTORY) {
                    return (options.liveLogs?.history ?? []).map((text) => ({ text }));
                }
                const categories =
                    typeof where.category === 'object' && where.category !== null && 'in' in where.category
                        ? (where.category as { in: unknown }).in
                        : null;
                if (
                    Array.isArray(categories) &&
                    categories.includes(LogCategory.SUMMARY) &&
                    categories.includes(LogCategory.ACTION)
                ) {
                    return (options.liveLogs?.action ?? []).map((text) => ({ text }));
                }
                return [];
            },
        },
        worldState: {
            findFirst: async () => ({
                currentYear: 220,
                currentMonth: 1,
                config: {},
                meta: options.worldMeta ?? { serverId: currentServerId },
            }),
        },
        yearbookHistory: {
            findFirst: async (args: {
                where: { profileName: string; year?: number; month?: number };
                orderBy?: Array<{ year?: 'asc' | 'desc'; month?: 'asc' | 'desc' }>;
            }) => {
                const candidates = archiveRows.filter(
                    (row) =>
                        row.profileName === args.where.profileName &&
                        (args.where.year === undefined || row.year === args.where.year) &&
                        (args.where.month === undefined || row.month === args.where.month)
                );
                if (!args.orderBy) {
                    return candidates[0] ?? null;
                }
                const descending = args.orderBy[0]?.year === 'desc';
                return (descending ? candidates.at(-1) : candidates[0]) ?? null;
            },
        },
    };
    const redis = {
        get: async () => null,
        set: async () => null,
    } as unknown as RedisConnector['client'];

    return {
        db: db as unknown as DatabaseClient,
        turnDaemon: new InMemoryTurnDaemonTransport(),
        battleSim: new InMemoryBattleSimTransport(),
        profile,
        auth,
        uploadDir: 'uploads',
        uploadPath: '/uploads',
        uploadPublicUrl: null,
        redis,
        accessTokenStore: new RedisAccessTokenStore(redis, profile.name),
        flushStore: new InMemoryFlushStore(),
        gameTokenSecret: 'test-secret',
    };
};

describe('historical yearbook access from dynasty', () => {
    it('selects the archived server range without treating it as the live month', async () => {
        const caller = appRouter.createCaller(buildContext(authFor('owner-a')));
        const result = await caller.yearbook.getRange({ serverID: archiveServerId });

        expect(result).toEqual({
            firstYearMonth: 200 * 12,
            lastYearMonth: 200 * 12 + 1,
            currentYearMonth: 200 * 12 + 1,
        });
    });

    it('returns the same archived public history for distinct general owners', async () => {
        const ownerA = appRouter.createCaller(buildContext(authFor('owner-a')));
        const ownerB = appRouter.createCaller(buildContext(authFor('owner-b')));

        const [resultA, resultB] = await Promise.all([
            ownerA.yearbook.getHistory({ serverID: archiveServerId, year: 200, month: 2 }),
            ownerB.yearbook.getHistory({ serverID: archiveServerId, year: 200, month: 2 }),
        ]);

        expect(resultB).toEqual(resultA);
        expect(resultA).toMatchObject({
            notModified: false,
            data: {
                year: 200,
                month: 2,
                nations: [{ id: 1, name: '촉', generalCount: 9, cities: ['성도'] }],
                globalHistory: ['<C>●</>2월: 기록 없음'],
                globalAction: ['<C>●</>2월: 기록 없음'],
            },
        });
    });

    it('requires both an authenticated user and a user-owned general like legacy v_history.php', async () => {
        const anonymous = appRouter.createCaller(buildContext(null));
        const noGeneral = appRouter.createCaller(buildContext(authFor('owner-a'), { hasGeneral: false }));

        await expect(anonymous.yearbook.getRange({ serverID: archiveServerId })).rejects.toMatchObject({
            code: 'UNAUTHORIZED',
        });
        await expect(noGeneral.yearbook.getRange({ serverID: archiveServerId })).rejects.toMatchObject({
            code: 'NOT_FOUND',
            message: 'General not found',
        });
    });

    it('rejects unknown archived server IDs instead of falling back to the live profile', async () => {
        const caller = appRouter.createCaller(buildContext(authFor('owner-a')));

        await expect(caller.yearbook.getRange({ serverID: 'missing-server' })).rejects.toMatchObject({
            code: 'NOT_FOUND',
            message: '연감 범위를 찾을 수 없습니다.',
        });
    });

    it('treats the canonical server ID and profile alias as the same live generation', async () => {
        const caller = appRouter.createCaller(buildContext(authFor('owner-a')));

        const [canonical, alias, omitted] = await Promise.all([
            caller.yearbook.getRange({ serverID: currentServerId }),
            caller.yearbook.getRange({ serverID: profile.name }),
            caller.yearbook.getRange(),
        ]);

        expect(canonical).toEqual(alias);
        expect(alias).toEqual(omitted);
        expect(canonical).toEqual({
            firstYearMonth: 219 * 12 + 11,
            lastYearMonth: 219 * 12 + 11,
            currentYearMonth: 220 * 12,
        });
    });

    it('reads imported history under the short profile ID when world metadata has no server ID', async () => {
        const caller = appRouter.createCaller(buildContext(authFor('owner-a'), { worldMeta: {} }));

        await expect(caller.yearbook.getRange()).resolves.toEqual({
            firstYearMonth: 219 * 12 + 10,
            lastYearMonth: 219 * 12 + 10,
            currentYearMonth: 220 * 12,
        });
    });

    it('uses stored logs for a past month of the current generation', async () => {
        const caller = appRouter.createCaller(buildContext(authFor('owner-a')));

        const result = await caller.yearbook.getHistory({
            serverID: currentServerId,
            year: 219,
            month: 12,
        });

        expect(result).toMatchObject({
            notModified: false,
            data: {
                globalHistory: ['저장된 현재 기수 과거 기록'],
                globalAction: ['저장된 현재 기수 과거 행동'],
            },
        });
    });

    it('recovers an already archived current-generation month from retained summary logs', async () => {
        const caller = appRouter.createCaller(
            buildContext(authFor('owner-a'), {
                liveLogs: {
                    history: ['복구한 현재 기수 과거 정세'],
                    action: ['복구한 현재 기수 장수 동향'],
                },
            })
        );

        const result = await caller.yearbook.getHistory({
            serverID: currentServerId,
            year: 219,
            month: 12,
        });

        expect(result).toMatchObject({
            notModified: false,
            data: {
                globalHistory: ['저장된 현재 기수 과거 기록'],
                globalAction: ['복구한 현재 기수 장수 동향'],
            },
        });
    });

    it('returns summary and compatible action logs as the live month general trend', async () => {
        const caller = appRouter.createCaller(
            buildContext(authFor('owner-a'), {
                liveLogs: {
                    history: ['현재 천하 동향'],
                    action: ['최신 호환 행동', '최신 장수 동향'],
                },
            })
        );

        const result = await caller.yearbook.getHistory({ year: 220, month: 1 });

        expect(result).toMatchObject({
            notModified: false,
            data: {
                globalHistory: ['현재 천하 동향'],
                globalAction: ['최신 호환 행동', '최신 장수 동향'],
            },
        });
    });

    it('uses stored Ref nation projections, canonical neutral values, and descending power in the live month', async () => {
        const zeroCityStats = {
            population: 0,
            agriculture: 0,
            commerce: 0,
            security: 0,
            defence: 0,
            wall: 0,
            populationMax: 1,
            agricultureMax: 1,
            commerceMax: 1,
            securityMax: 1,
            defenceMax: 1,
            wallMax: 1,
        };
        const caller = appRouter.createCaller(
            buildContext(authFor('owner-a'), {
                liveNations: [
                    {
                        id: 0,
                        name: '오염된 재야',
                        color: '#ffffff',
                        level: 9,
                        gold: 0,
                        rice: 0,
                        tech: 0,
                        meta: { power: 90, gennum: 90 },
                    },
                    {
                        id: 1,
                        name: '촉',
                        color: '#ff0000',
                        level: 7,
                        gold: 0,
                        rice: 0,
                        tech: 0,
                        meta: { power: 777, gennum: 9 },
                    },
                    {
                        id: 2,
                        name: '위',
                        color: '#0000ff',
                        level: 7,
                        gold: 0,
                        rice: 0,
                        tech: 0,
                        meta: { power: 0, gennum: 2 },
                    },
                ],
                liveCities: [
                    { id: 0, name: '낙양', nationId: 0, ...zeroCityStats },
                    { id: 1, name: '성도', nationId: 1, ...zeroCityStats },
                    { id: 2, name: '허창', nationId: 2, ...zeroCityStats },
                ],
            })
        );

        const result = await caller.yearbook.getHistory({ year: 220, month: 1 });

        expect(result).toMatchObject({
            notModified: false,
            data: {
                nations: [
                    expect.objectContaining({ id: 1, power: 777, generalCount: 9 }),
                    expect.objectContaining({
                        id: 0,
                        name: '재야',
                        color: '#000000',
                        level: 0,
                        power: 1,
                        generalCount: 1,
                    }),
                    expect.objectContaining({ id: 2, power: 0, generalCount: 2 }),
                ],
            },
        });
    });
});
