// Tests for per-thread state transitions and derived helpers.
// Covers: ensureThread, enqueue/dequeue, derived helpers (isRunActive,
// hasQueue, hasBlockers, canDispatchNext, isBusy), blocker transitions,
// run state updates, and edge cases.
import { describe, test, expect, beforeEach } from 'vitest';
import { store } from '../store.js';
import * as threadState from './thread-runtime-state.js';
import { pureMarkDispatching, pureMarkFinished } from './state.js';
function makeMessage(prompt) {
    return {
        prompt,
        userId: 'u1',
        username: 'TestUser',
        queuedAt: Date.now(),
    };
}
describe('thread-runtime-state', () => {
    beforeEach(() => {
        // Reset the threads Map before each test
        store.setState({ threads: new Map() });
    });
    describe('ensureThread / removeThread', () => {
        test('ensureThread creates initial state', () => {
            threadState.ensureThread('t1');
            const t = threadState.getThreadState('t1');
            expect(t).toBeDefined();
            expect(t.sessionId).toBeUndefined();
            expect(t.queueItems).toEqual([]);
            expect(t.runState.phase).toBe('waiting-dispatch');
            expect(t.runController).toBeUndefined();
        });
        test('ensureThread is idempotent', () => {
            threadState.ensureThread('t1');
            threadState.setSessionId('t1', 'session-1');
            threadState.ensureThread('t1'); // should NOT reset
            expect(threadState.getThreadState('t1').sessionId).toBe('session-1');
        });
        test('removeThread removes from store', () => {
            threadState.ensureThread('t1');
            threadState.removeThread('t1');
            expect(threadState.getThreadState('t1')).toBeUndefined();
        });
        test('removeThread on non-existent thread is no-op', () => {
            threadState.removeThread('nonexistent');
            expect(threadState.getThreadState('nonexistent')).toBeUndefined();
        });
        test('getThreadIds returns all thread IDs', () => {
            threadState.ensureThread('t1');
            threadState.ensureThread('t2');
            threadState.ensureThread('t3');
            expect(threadState.getThreadIds().sort()).toEqual(['t1', 't2', 't3']);
        });
    });
    describe('enqueue / dequeue', () => {
        test('enqueueItem adds to queue', () => {
            threadState.ensureThread('t1');
            threadState.enqueueItem('t1', makeMessage('hello'));
            threadState.enqueueItem('t1', makeMessage('world'));
            const t = threadState.getThreadState('t1');
            expect(t.queueItems).toHaveLength(2);
            expect(t.queueItems[0].prompt).toBe('hello');
            expect(t.queueItems[1].prompt).toBe('world');
        });
        test('dequeueItem returns first and removes it', () => {
            threadState.ensureThread('t1');
            threadState.enqueueItem('t1', makeMessage('first'));
            threadState.enqueueItem('t1', makeMessage('second'));
            const item = threadState.dequeueItem('t1');
            expect(item?.prompt).toBe('first');
            expect(threadState.getThreadState('t1').queueItems).toHaveLength(1);
            expect(threadState.getThreadState('t1').queueItems[0].prompt).toBe('second');
        });
        test('dequeueItem returns undefined on empty queue', () => {
            threadState.ensureThread('t1');
            expect(threadState.dequeueItem('t1')).toBeUndefined();
        });
        test('dequeueItem returns undefined for non-existent thread', () => {
            expect(threadState.dequeueItem('nonexistent')).toBeUndefined();
        });
        test('clearQueueItems empties the queue', () => {
            threadState.ensureThread('t1');
            threadState.enqueueItem('t1', makeMessage('a'));
            threadState.enqueueItem('t1', makeMessage('b'));
            threadState.clearQueueItems('t1');
            expect(threadState.getThreadState('t1').queueItems).toHaveLength(0);
        });
        test('enqueueItem on non-existent thread is no-op', () => {
            threadState.enqueueItem('nonexistent', makeMessage('nope'));
            expect(threadState.getThreadState('nonexistent')).toBeUndefined();
        });
    });
    describe('derived helpers', () => {
        test('isRunActive for each phase', () => {
            const base = threadState.initialThreadState();
            expect(threadState.isRunActive({ ...base, runState: { ...base.runState, phase: 'waiting-dispatch' } })).toBe(false);
            expect(threadState.isRunActive({ ...base, runState: { ...base.runState, phase: 'collecting-baseline' } })).toBe(true);
            expect(threadState.isRunActive({ ...base, runState: { ...base.runState, phase: 'dispatching' } })).toBe(true);
            expect(threadState.isRunActive({ ...base, runState: { ...base.runState, phase: 'prompt-resolved' } })).toBe(true);
            expect(threadState.isRunActive({ ...base, runState: { ...base.runState, phase: 'finished' } })).toBe(false);
            expect(threadState.isRunActive({ ...base, runState: { ...base.runState, phase: 'aborted' } })).toBe(false);
        });
        test('hasQueue returns true when items exist', () => {
            const base = threadState.initialThreadState();
            expect(threadState.hasQueue(base)).toBe(false);
            expect(threadState.hasQueue({ ...base, queueItems: [makeMessage('x')] })).toBe(true);
        });
        test('hasBlockers checks all blocker fields', () => {
            const base = threadState.initialThreadState();
            expect(threadState.hasBlockers(base)).toBe(false);
            expect(threadState.hasBlockers({
                ...base,
                blockers: { ...base.blockers, permissionCount: 1 },
            })).toBe(true);
            expect(threadState.hasBlockers({
                ...base,
                blockers: { ...base.blockers, questionCount: 1 },
            })).toBe(true);
            expect(threadState.hasBlockers({
                ...base,
                blockers: { ...base.blockers, actionButtonsPending: true },
            })).toBe(true);
            expect(threadState.hasBlockers({
                ...base,
                blockers: { ...base.blockers, fileUploadPending: true },
            })).toBe(true);
        });
        test('isBusy is true when run active OR queue has items', () => {
            const base = threadState.initialThreadState();
            expect(threadState.isBusy(base)).toBe(false);
            expect(threadState.isBusy({ ...base, queueItems: [makeMessage('x')] })).toBe(true);
            expect(threadState.isBusy({
                ...base,
                runState: { ...base.runState, phase: 'dispatching' },
            })).toBe(true);
        });
    });
    describe('canDispatchNext edge cases', () => {
        test('cannot dispatch without sessionId', () => {
            const base = threadState.initialThreadState();
            const t = { ...base, queueItems: [makeMessage('x')] };
            expect(threadState.canDispatchNext(t)).toBe(false);
        });
        test('cannot dispatch with empty queue', () => {
            const base = threadState.initialThreadState();
            const t = { ...base, sessionId: 'sess-1' };
            expect(threadState.canDispatchNext(t)).toBe(false);
        });
        test('cannot dispatch while run is active', () => {
            const base = threadState.initialThreadState();
            const t = {
                ...base,
                sessionId: 'sess-1',
                queueItems: [makeMessage('x')],
                runState: { ...base.runState, phase: 'dispatching' },
            };
            expect(threadState.canDispatchNext(t)).toBe(false);
        });
        test('cannot dispatch with pending blockers', () => {
            const base = threadState.initialThreadState();
            const t = {
                ...base,
                sessionId: 'sess-1',
                queueItems: [makeMessage('x')],
                blockers: { ...base.blockers, permissionCount: 1 },
            };
            expect(threadState.canDispatchNext(t)).toBe(false);
        });
        test('can dispatch when session exists, queue has items, run idle, no blockers', () => {
            const base = threadState.initialThreadState();
            const t = {
                ...base,
                sessionId: 'sess-1',
                queueItems: [makeMessage('x')],
            };
            expect(threadState.canDispatchNext(t)).toBe(true);
        });
        test('can dispatch after run finishes', () => {
            const base = threadState.initialThreadState();
            const t = {
                ...base,
                sessionId: 'sess-1',
                queueItems: [makeMessage('x')],
                runState: { ...base.runState, phase: 'finished' },
            };
            expect(threadState.canDispatchNext(t)).toBe(true);
        });
        test('can dispatch after run aborted', () => {
            const base = threadState.initialThreadState();
            const t = {
                ...base,
                sessionId: 'sess-1',
                queueItems: [makeMessage('x')],
                runState: { ...base.runState, phase: 'aborted' },
            };
            expect(threadState.canDispatchNext(t)).toBe(true);
        });
    });
    describe('blocker transitions', () => {
        test('incrementBlocker / decrementBlocker for permissionCount', () => {
            threadState.ensureThread('t1');
            threadState.incrementBlocker('t1', 'permissionCount');
            expect(threadState.getThreadState('t1').blockers.permissionCount).toBe(1);
            threadState.incrementBlocker('t1', 'permissionCount');
            expect(threadState.getThreadState('t1').blockers.permissionCount).toBe(2);
            threadState.decrementBlocker('t1', 'permissionCount');
            expect(threadState.getThreadState('t1').blockers.permissionCount).toBe(1);
            threadState.decrementBlocker('t1', 'permissionCount');
            expect(threadState.getThreadState('t1').blockers.permissionCount).toBe(0);
        });
        test('decrementBlocker does not go below 0', () => {
            threadState.ensureThread('t1');
            threadState.decrementBlocker('t1', 'permissionCount');
            expect(threadState.getThreadState('t1').blockers.permissionCount).toBe(0);
        });
        test('incrementBlocker / decrementBlocker for questionCount', () => {
            threadState.ensureThread('t1');
            threadState.incrementBlocker('t1', 'questionCount');
            expect(threadState.getThreadState('t1').blockers.questionCount).toBe(1);
            threadState.decrementBlocker('t1', 'questionCount');
            expect(threadState.getThreadState('t1').blockers.questionCount).toBe(0);
        });
        test('setBlockerFlag for actionButtonsPending', () => {
            threadState.ensureThread('t1');
            threadState.setBlockerFlag('t1', 'actionButtonsPending', true);
            expect(threadState.getThreadState('t1').blockers.actionButtonsPending).toBe(true);
            threadState.setBlockerFlag('t1', 'actionButtonsPending', false);
            expect(threadState.getThreadState('t1').blockers.actionButtonsPending).toBe(false);
        });
        test('setBlockerFlag for fileUploadPending', () => {
            threadState.ensureThread('t1');
            threadState.setBlockerFlag('t1', 'fileUploadPending', true);
            expect(threadState.getThreadState('t1').blockers.fileUploadPending).toBe(true);
            threadState.setBlockerFlag('t1', 'fileUploadPending', false);
            expect(threadState.getThreadState('t1').blockers.fileUploadPending).toBe(false);
        });
    });
    describe('updateRunState', () => {
        test('updates run state using pure transition', () => {
            threadState.ensureThread('t1');
            threadState.updateRunState('t1', pureMarkDispatching);
            expect(threadState.getThreadState('t1').runState.phase).toBe('dispatching');
        });
        test('chains multiple run state transitions', () => {
            threadState.ensureThread('t1');
            threadState.updateRunState('t1', pureMarkDispatching);
            threadState.updateRunState('t1', pureMarkFinished);
            expect(threadState.getThreadState('t1').runState.phase).toBe('finished');
        });
        test('updateRunState on non-existent thread is no-op', () => {
            threadState.updateRunState('nonexistent', pureMarkDispatching);
            expect(threadState.getThreadState('nonexistent')).toBeUndefined();
        });
    });
    describe('setSessionId', () => {
        test('sets session ID on existing thread', () => {
            threadState.ensureThread('t1');
            threadState.setSessionId('t1', 'sess-abc');
            expect(threadState.getThreadState('t1').sessionId).toBe('sess-abc');
        });
    });
    describe('setRunController', () => {
        test('sets and clears run controller', () => {
            threadState.ensureThread('t1');
            const controller = new AbortController();
            threadState.setRunController('t1', controller);
            expect(threadState.getThreadState('t1').runController).toBe(controller);
            threadState.setRunController('t1', undefined);
            expect(threadState.getThreadState('t1').runController).toBeUndefined();
        });
    });
    describe('immutability', () => {
        test('enqueue produces new queueItems array', () => {
            threadState.ensureThread('t1');
            const before = threadState.getThreadState('t1').queueItems;
            threadState.enqueueItem('t1', makeMessage('x'));
            const after = threadState.getThreadState('t1').queueItems;
            expect(before).not.toBe(after);
        });
        test('updateThread produces new Map reference', () => {
            threadState.ensureThread('t1');
            const before = store.getState().threads;
            threadState.setSessionId('t1', 'new-session');
            const after = store.getState().threads;
            expect(before).not.toBe(after);
        });
    });
});
