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

import type { TurnDaemonCommand } from '@sammo-ts/common';

import type { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { normalizeTurnDaemonCommand } from '../src/turn/commandRegistry.js';
import { createTurnDaemonCommandHandler } from '../src/turn/worldCommandHandler.js';

const buildActorBoundCommands = (userId = 'old-owner'): TurnDaemonCommand[] => [
    { type: 'troopCreate', requestId: 'troopCreate', userId, generalId: 7, troopName: '백마대' },
    { type: 'troopJoin', requestId: 'troopJoin', userId, generalId: 7, troopId: 8 },
    { type: 'troopExit', requestId: 'troopExit', userId, generalId: 7 },
    { type: 'troopKick', requestId: 'troopKick', userId, generalId: 7, troopId: 7, targetGeneralId: 8 },
    { type: 'troopRename', requestId: 'troopRename', userId, generalId: 7, troopId: 7, troopName: '신대' },
    { type: 'vacation', requestId: 'vacation', userId, generalId: 7 },
    { type: 'setMySetting', requestId: 'setMySetting', userId, generalId: 7, settings: { tnmt: 1 } },
    { type: 'dropItem', requestId: 'dropItem', userId, generalId: 7, itemType: 'weapon' },
    {
        type: 'auctionOpen',
        requestId: 'auctionOpen',
        userId,
        generalId: 7,
        auctionType: 'BUY_RICE',
        amount: 1_000,
        closeTurnCnt: 3,
        startBidAmount: 100,
        finishBidAmount: 500,
    },
    { type: 'auctionBid', requestId: 'auctionBid', userId, generalId: 7, auctionId: 1, amount: 200 },
    {
        type: 'changePermission',
        requestId: 'changePermission',
        userId,
        generalId: 7,
        isAmbassador: true,
        targetGeneralIds: [],
    },
    { type: 'kick', requestId: 'kick', userId, generalId: 7, destGeneralId: 8 },
    {
        type: 'appoint',
        requestId: 'appoint',
        userId,
        generalId: 7,
        destGeneralId: 8,
        destCityId: 1,
        officerLevel: 4,
    },
    { type: 'voteReward', requestId: 'voteReward', userId, generalId: 7, voteId: 1, selection: [0] },
];

const buildReadOnlyWorld = (ownerUserId: string) => {
    const mutation = vi.fn();
    const world = {
        getGeneralById: vi.fn(() => ({ id: 7, userId: ownerUserId })),
        updateGeneral: mutation,
        updateNation: mutation,
        createTroop: mutation,
        updateTroop: mutation,
        removeTroop: mutation,
        pushLog: mutation,
        queueMessage: mutation,
    } as unknown as InMemoryTurnWorld;
    return { world, mutation };
};

describe('authenticated actor-bound command registry and execution', () => {
    it.each(['horse', 'weapon', 'book', 'item'] as const)(
        'accepts the Ref equipment slot %s for dropItem',
        (itemType) => {
            expect(
                normalizeTurnDaemonCommand({
                    requestId: `drop-item:${itemType}`,
                    sentAt: '2026-08-24T00:00:00.000Z',
                    command: { type: 'dropItem', userId: 'user-7', generalId: 7, itemType },
                })
            ).toEqual({
                type: 'dropItem',
                requestId: `drop-item:${itemType}`,
                userId: 'user-7',
                generalId: 7,
                itemType,
            });
        }
    );

    it.each(['armor', '', 0, null])('rejects the invalid dropItem slot %j at the daemon boundary', (itemType) => {
        expect(
            normalizeTurnDaemonCommand({
                requestId: 'drop-item:invalid-slot',
                sentAt: '2026-08-24T00:00:00.000Z',
                command: {
                    type: 'dropItem',
                    userId: 'user-7',
                    generalId: 7,
                    itemType,
                } as unknown as TurnDaemonCommand,
            })
        ).toBeNull();
    });

    it('rejects every actor-bound queue payload that omits userId', () => {
        for (const command of buildActorBoundCommands()) {
            const {
                userId: _userId,
                requestId: _requestId,
                ...payload
            } = command as unknown as Record<string, unknown>;
            expect(
                normalizeTurnDaemonCommand({
                    requestId: `missing-user:${command.type}`,
                    sentAt: '2026-08-24T00:00:00.000Z',
                    command: payload as TurnDaemonCommand,
                }),
                command.type
            ).toBeNull();
        }
    });

    it('rejects all stale-owner commands before any world mutation', async () => {
        const commands = buildActorBoundCommands();
        const { world, mutation } = buildReadOnlyWorld('new-owner');
        const eventTypes = new Map(commands.map((command) => [command.requestId, command.type]));
        const db = {
            inputEvent: {
                findUnique: vi.fn(async ({ where }: { where: { requestId: string } }) => ({
                    actorUserId: 'old-owner',
                    target: 'ENGINE',
                    eventType: eventTypes.get(where.requestId),
                })),
            },
        };
        const handler = createTurnDaemonCommandHandler({ world });

        for (const command of commands) {
            await expect(handler.handle(command, { db: db as never }), command.type).resolves.toMatchObject({
                type: 'commandRejected',
                ok: false,
                commandType: command.type,
            });
        }
        expect(mutation).not.toHaveBeenCalled();
    });

    it.each([
        ['missing event', null],
        ['actor mismatch', { actorUserId: 'other-owner', target: 'ENGINE', eventType: 'vacation' }],
        ['target mismatch', { actorUserId: 'old-owner', target: 'API', eventType: 'vacation' }],
        ['event type mismatch', { actorUserId: 'old-owner', target: 'ENGINE', eventType: 'dropItem' }],
    ])('returns commandRejected for %s without looking up or mutating the general', async (_label, event) => {
        const { world, mutation } = buildReadOnlyWorld('old-owner');
        const getGeneralById = world.getGeneralById as ReturnType<typeof vi.fn>;
        const handler = createTurnDaemonCommandHandler({ world });
        const result = await handler.handle(
            { type: 'vacation', requestId: 'vacation', userId: 'old-owner', generalId: 7 },
            {
                db: {
                    inputEvent: { findUnique: vi.fn(async () => event) },
                } as never,
            }
        );

        expect(result).toMatchObject({ type: 'commandRejected', ok: false, commandType: 'vacation' });
        expect(getGeneralById).not.toHaveBeenCalled();
        expect(mutation).not.toHaveBeenCalled();
    });

    it('preserves direct in-memory invocation when no command database is supplied', async () => {
        const updateGeneral = vi.fn();
        const world = {
            getGeneralById: vi.fn(() => ({ id: 7, userId: 'current-owner', meta: { killturn: 12 } })),
            getState: vi.fn(() => ({ meta: { killturn: 24, autorun_user: {} } })),
            updateGeneral,
        } as unknown as InMemoryTurnWorld;
        const handler = createTurnDaemonCommandHandler({ world });

        await expect(handler.handle({ type: 'vacation', userId: 'different-owner', generalId: 7 })).resolves.toEqual({
            type: 'vacation',
            ok: true,
            generalId: 7,
        });
        expect(updateGeneral).toHaveBeenCalledWith(7, { meta: { killturn: 72 } });
    });
});
