/**
 * Unit tests for ApprovalQueue.
 *
 * Strategy: drive enqueue → approve / reject / clear / pre-empt directly,
 * confirm the onChange event fires at the right moments and callbacks fire
 * exactly once.
 */

import { ApprovalQueue, Approval } from '../src/features/approval/approvalQueue';

function makeApproval(id: string = 'txn-1'): Approval {
    return {
        id,
        kind: 'transaction',
        title: 'Pending file changes',
        summary: '2 files',
        files: ['/tmp/a.ts', '/tmp/b.ts'],
        createdAt: Date.now(),
    };
}

describe('ApprovalQueue', () => {
    test('starts empty', () => {
        const q = new ApprovalQueue();
        expect(q.current()).toBeNull();
        expect(q.pendingCount()).toBe(0);
    });

    test('enqueue sets current and fires onChange', () => {
        const q = new ApprovalQueue();
        let fired = 0;
        q.onChange(() => fired++);
        q.enqueue(makeApproval(), { approve: () => {}, reject: () => {} });
        expect(q.pendingCount()).toBe(1);
        expect(q.current()?.id).toBe('txn-1');
        expect(fired).toBe(1);
    });

    test('approve invokes the approve callback exactly once and clears state', async () => {
        const q = new ApprovalQueue();
        let approveCount = 0;
        let rejectCount = 0;
        q.enqueue(makeApproval(), {
            approve: () => { approveCount++; },
            reject: () => { rejectCount++; },
        });
        await q.approve('txn-1');
        expect(approveCount).toBe(1);
        expect(rejectCount).toBe(0);
        expect(q.current()).toBeNull();
        // Idempotent — second approve does nothing.
        await q.approve('txn-1');
        expect(approveCount).toBe(1);
    });

    test('reject invokes the reject callback exactly once', async () => {
        const q = new ApprovalQueue();
        let approveCount = 0;
        let rejectCount = 0;
        q.enqueue(makeApproval(), {
            approve: () => { approveCount++; },
            reject: () => { rejectCount++; },
        });
        await q.reject('txn-1');
        expect(rejectCount).toBe(1);
        expect(approveCount).toBe(0);
        expect(q.current()).toBeNull();
    });

    test('mismatched id is ignored — protects against stale webview button clicks', async () => {
        const q = new ApprovalQueue();
        let count = 0;
        q.enqueue(makeApproval('txn-1'), {
            approve: () => { count++; },
            reject: () => { count++; },
        });
        await q.approve('txn-OLD');
        expect(count).toBe(0);
        expect(q.current()?.id).toBe('txn-1');
    });

    test('approve/reject without id picks current', async () => {
        const q = new ApprovalQueue();
        let approveCount = 0;
        q.enqueue(makeApproval(), { approve: () => { approveCount++; }, reject: () => {} });
        await q.approve();
        expect(approveCount).toBe(1);
    });

    test('enqueue while pending pre-empts the previous one without firing its callbacks', () => {
        const q = new ApprovalQueue();
        let oldApprove = 0, oldReject = 0;
        q.enqueue(makeApproval('old'), {
            approve: () => { oldApprove++; },
            reject: () => { oldReject++; },
        });
        let newApprove = 0;
        q.enqueue(makeApproval('new'), {
            approve: () => { newApprove++; },
            reject: () => {},
        });
        expect(q.current()?.id).toBe('new');
        expect(oldApprove).toBe(0);
        expect(oldReject).toBe(0);
        // Approving "new" must hit only the new callback.
        return q.approve('new').then(() => {
            expect(newApprove).toBe(1);
            expect(oldApprove).toBe(0);
        });
    });

    test('clear() resets without firing callbacks', () => {
        const q = new ApprovalQueue();
        let cb = 0;
        q.enqueue(makeApproval(), { approve: () => { cb++; }, reject: () => { cb++; } });
        q.clear();
        expect(q.current()).toBeNull();
        expect(cb).toBe(0);
    });

    test('onChange fires on enqueue, approve, reject, clear', async () => {
        const q = new ApprovalQueue();
        const events: string[] = [];
        q.onChange(() => events.push(`change-${q.pendingCount()}`));
        q.enqueue(makeApproval('a'), { approve: () => {}, reject: () => {} });
        await q.approve('a');
        q.enqueue(makeApproval('b'), { approve: () => {}, reject: () => {} });
        await q.reject('b');
        q.enqueue(makeApproval('c'), { approve: () => {}, reject: () => {} });
        q.clear();
        expect(events).toEqual([
            'change-1', 'change-0', // enqueue a, approve a
            'change-1', 'change-0', // enqueue b, reject b
            'change-1', 'change-0', // enqueue c, clear
        ]);
    });

    test('callback exception is swallowed (next enqueue still works)', async () => {
        const q = new ApprovalQueue();
        q.enqueue(makeApproval('boom'), {
            approve: () => { throw new Error('callback boom'); },
            reject: () => {},
        });
        await q.approve('boom');
        expect(q.current()).toBeNull();
        // Verify we can still operate the queue afterwards.
        let next = 0;
        q.enqueue(makeApproval('next'), { approve: () => { next++; }, reject: () => {} });
        await q.approve('next');
        expect(next).toBe(1);
    });

    test('dispose drops state and prevents further events', () => {
        const q = new ApprovalQueue();
        let fired = 0;
        q.onChange(() => fired++);
        q.enqueue(makeApproval(), { approve: () => {}, reject: () => {} });
        expect(fired).toBe(1);
        q.dispose();
        expect(q.current()).toBeNull();
        // Re-enqueueing after dispose: the emitter is disposed but should not crash.
        // The contract is "dispose terminates the queue" — callers shouldn't reuse it.
    });
});
