import {
    validateOfficeSnapshot,
    makeEmptyOfficeSnapshot,
} from '../src/features/astraOffice/schema';
import {
    validateLayout,
    migrateLayout,
} from '../src/features/astraOffice/view/layoutSchema';
import { presentOfficeSnapshot, normalizeAgentId } from '../src/features/astraOffice/presenter';

describe('OfficeSnapshot schema', () => {
    test('makeEmptyOfficeSnapshot returns idle / null active', () => {
        const s = makeEmptyOfficeSnapshot();
        expect(s.phase).toBe('idle');
        expect(s.activeAgentId).toBeNull();
        expect(s.roster).toEqual([]);
        expect(s.activity).toEqual([]);
        expect(s.newBubbles).toEqual([]);
    });

    test('validateOfficeSnapshot rejects null / non-object', () => {
        expect(validateOfficeSnapshot(null)).toBeNull();
        expect(validateOfficeSnapshot(undefined)).toBeNull();
        expect(validateOfficeSnapshot('idle')).toBeNull();
        expect(validateOfficeSnapshot(42)).toBeNull();
    });

    test('validateOfficeSnapshot fills defaults for sparse input', () => {
        const s = validateOfficeSnapshot({});
        expect(s).not.toBeNull();
        expect(s!.phase).toBe('idle');
        expect(s!.activeAgentId).toBeNull();
        expect(s!.roster).toEqual([]);
        expect(typeof s!.updatedAt).toBe('number');
    });

    test('validateOfficeSnapshot rejects invalid enums quietly', () => {
        const s = validateOfficeSnapshot({
            phase: 'totally_made_up_phase',
            activeAgentId: 'ceo',
            roster: [{ agentId: 'ceo', agentName: 'CEO', roleCategory: 'BOGUS', status: 'NOT_REAL', lastActivityAt: 100 }],
        });
        expect(s!.phase).toBe('idle'); // fell back
        expect(s!.roster[0].roleCategory).toBe('support'); // fell back
        expect(s!.roster[0].status).toBe('idle'); // fell back
    });

    test('validateOfficeSnapshot keeps valid roster + bubbles', () => {
        const s = validateOfficeSnapshot({
            phase: 'executing',
            activeAgentId: 'developer',
            roster: [
                { agentId: 'developer', agentName: '개발', roleCategory: 'developer', status: 'executing', lastActivityAt: 100 },
                { agentId: 'inspector', agentName: '감리', roleCategory: 'inspector', status: 'idle', lastActivityAt: 90 },
            ],
            newBubbles: [
                { agentId: 'developer', text: '코드 들어간다', type: 'event' },
                { agentId: 'inspector', text: 'should_be_dropped', type: 'wrong_type' }, // type 잘못 → 'status' 로 폴백
            ],
        });
        expect(s!.phase).toBe('executing');
        expect(s!.activeAgentId).toBe('developer');
        expect(s!.roster).toHaveLength(2);
        expect(s!.newBubbles).toHaveLength(2);
        expect(s!.newBubbles[1].type).toBe('status'); // fell back
    });

    test('validateOfficeSnapshot drops malformed roster entries', () => {
        const s = validateOfficeSnapshot({
            phase: 'idle',
            roster: [
                { agentId: 'ok', roleCategory: 'ceo', status: 'idle', lastActivityAt: 0 },
                { /* no agentId */ roleCategory: 'ceo' },
                null,
                'string',
            ],
        });
        expect(s!.roster).toHaveLength(1);
        expect(s!.roster[0].agentId).toBe('ok');
    });
});

describe('Layout schema', () => {
    test('validateLayout rejects non-v2 raw', () => {
        expect(validateLayout(null)).toBeNull();
        expect(validateLayout({ cells: [] })).toBeNull(); // empty cells, no schema marker → null
        expect(validateLayout({ cells: [{ roleKey: 'x', deskX: 0, deskY: 0 }] })).toBeNull(); // missing v2 markers
    });

    test('validateLayout accepts explicit schema:2', () => {
        const v = validateLayout({
            schema: 2,
            cells: [
                {
                    roleKey: 'ceo',
                    agentKey: 'ceo',
                    label: 'CEO',
                    charRow: 0,
                    deskSprite: 'desk-boss',
                    face: 'R',
                    boss: true,
                    deskX: 100, deskY: 50, deskW: 136,
                    seatX: 130, seatY: 80,
                    deskRot: 0, deskZ: 0, charRot: 0, charZ: 0,
                    noChar: false,
                },
            ],
            objs: [{ id: 'obj_0', name: 'plant', x: 10, y: 20, w: 32, rot: 0, z: 0 }],
        });
        expect(v).not.toBeNull();
        expect(v!.cells).toHaveLength(1);
        expect(v!.cells[0].deskSprite).toBe('desk-boss');
        expect(v!.objs[0].name).toBe('plant');
    });

    test('validateLayout normalizes invalid face to R', () => {
        const v = validateLayout({
            schema: 2,
            cells: [{
                roleKey: 'x', deskSprite: 'desk-main', face: 'XYZ', charRow: 0,
                deskX: 0, deskY: 0, deskW: 100, seatX: 0, seatY: 0,
                deskRot: 0, deskZ: 0, charRot: 0, charZ: 0,
            }],
        });
        expect(v!.cells[0].face).toBe('R');
    });

    test('validateLayout detects v2 via field presence (no explicit schema)', () => {
        const v = validateLayout({
            cells: [{
                roleKey: 'a', deskSprite: 'desk-main', charRow: 2,
                deskX: 0, deskY: 0, deskW: 100, seatX: 0, seatY: 0,
            }],
        });
        expect(v).not.toBeNull();
        expect(v!.cells[0].charRow).toBe(2);
    });

    test('migrateLayout upgrades v1 (coord-only) cells', () => {
        const v1 = {
            cells: [{ roleKey: 'ceo', deskX: 100, deskY: 50, deskW: 136, seatX: 130, seatY: 80, deskRot: 0, deskZ: 0, charRot: 0, charZ: 0 }],
            objs: [],
        };
        const v = migrateLayout(v1);
        expect(v).not.toBeNull();
        expect(v!.schema).toBe(2);
        expect(v!.cells[0].agentKey).toBe('ceo'); // fallback: roleKey == agentKey
        expect(v!.cells[0].deskSprite).toBe('desk-main');
        expect(v!.cells[0].charRow).toBe(0);
    });
});

describe('presenter', () => {
    test('normalizeAgentId resolves aliases', () => {
        expect(normalizeAgentId('writer')).toBe('writer');
        expect(normalizeAgentId('Editor')).toBe('designer');
        expect(normalizeAgentId('Secretary')).toBe('support');
        expect(normalizeAgentId('business')).toBe('inspector');
        expect(normalizeAgentId(undefined)).toBeNull();
    });

    test('presentOfficeSnapshot builds single-agent roster from old AgentWorkState', () => {
        const snap = presentOfficeSnapshot({
            activeState: {
                agentId: 'inspector',
                agentName: '감리',
                status: 'reviewing',
                currentStep: '라운드 2/3',
                currentTask: '타입 정합성 검수',
                recentLogs: ['타입 누락 발견'],
                updatedAt: 1000,
            } as any,
        });
        expect(snap.phase).toBe('reviewing');
        expect(snap.activeAgentId).toBe('inspector');
        expect(snap.roster).toHaveLength(1);
        expect(snap.roster[0].roleCategory).toBe('inspector');
        expect(snap.roster[0].lastLog).toBe('타입 누락 발견');
        expect(snap.task?.goal).toBe('타입 정합성 검수');
    });

    test('presentOfficeSnapshot returns empty when no input', () => {
        const snap = presentOfficeSnapshot({});
        expect(snap.phase).toBe('idle');
        expect(snap.activeAgentId).toBeNull();
        expect(snap.roster).toEqual([]);
    });

    test('presentOfficeSnapshot maps need_clarification to awaiting-approval phase', () => {
        const snap = presentOfficeSnapshot({
            activeState: {
                agentId: 'ceo',
                agentName: 'CEO',
                status: 'need_clarification',
                needUserInput: ['배포 대상 환경은?'],
                updatedAt: 0,
            } as any,
        });
        expect(snap.phase).toBe('awaiting-approval');
        expect(snap.awaiting?.kind).toBe('clarification');
        expect(snap.awaiting?.questions).toEqual(['배포 대상 환경은?']);
    });

    test('presentOfficeSnapshot with roster places active agent in working state and others idle', () => {
        const snap = presentOfficeSnapshot({
            activeState: {
                agentId: 'developer',
                agentName: '개발',
                status: 'executing',
                currentStep: '함수 추출',
                recentLogs: ['파일 수정 완료'],
                updatedAt: 1000,
            } as any,
            roster: [
                { agentId: 'ceo', agentName: 'CEO', roleCategory: 'ceo' },
                { agentId: 'developer', agentName: '개발', roleCategory: 'developer' },
                { agentId: 'inspector', agentName: '감리', roleCategory: 'inspector' },
            ],
        });
        expect(snap.roster).toHaveLength(3);
        const active = snap.roster.find((a) => a.agentId === 'developer');
        const idle = snap.roster.find((a) => a.agentId === 'ceo');
        expect(active?.status).toBe('executing');
        expect(active?.lastLog).toBe('파일 수정 완료');
        expect(idle?.status).toBe('idle');
        expect(idle?.lastLog).toBeUndefined();
    });

    test('presentOfficeSnapshot folds recentActivity into snapshot.activity (ring buffer)', () => {
        const activity = Array.from({ length: 40 }, (_, i) => ({
            ts: i * 100,
            agentId: 'developer',
            text: `step ${i}`,
        }));
        const snap = presentOfficeSnapshot({ recentActivity: activity });
        // presenter 가 32개로 잘라야 함.
        expect(snap.activity).toHaveLength(32);
        expect(snap.activity[0].text).toBe('step 8'); // 처음 8개 dropped
        expect(snap.activity[31].text).toBe('step 39');
    });
});
