import { setImmediate } from 'node:timers/promises';
import { describe, test, expect } from 'manten';
import { cli } from '#cleye';

describe('cli', () => {
	describe('error-handling', () => {
		test('must pass in options', () => {
			expect(() => {
				// @ts-expect-error no options
				cli();
			}).toThrow('Options is required');
		});

		test('missing name', () => {
			expect(() => {
				cli({
					name: '',
				});
			}).toThrow('Invalid script name: ""');
		});

		test('name with leading space', () => {
			expect(() => {
				cli({
					name: ' a',
				});
			}).toThrow('Invalid script name: " a"');
		});

		test('name with trailing space', () => {
			expect(() => {
				cli({
					name: 'a ',
				});
			}).toThrow('Invalid script name: "a "');
		});

		test('allowed name formats', () => {
			cli({ name: 'a.b_' });
			cli({ name: 'multiple parent commands' });
		});
	});

	test('async callbacks', async () => {
		let asyncCompleted = false;
		await cli({}, async () => {
			await setImmediate();
			asyncCompleted = true;
		});
		expect(asyncCompleted).toBe(true);
	});

	describe('callback error handling', () => {
		test('callback throws synchronous error', () => {
			expect(() => {
				cli({}, () => {
					throw new Error('Callback error');
				});
			}).toThrow('Callback error');
		});

		test('callback returns rejected Promise', async () => {
			const result = cli({}, async () => {
				throw new Error('Async error');
			});
			await expect(result).rejects.toThrow('Async error');
		});

		test('callback with fake promise thenable', () => {
			const fakePromise = {
				then: (resolve: any) => resolve('fake'), // eslint-disable-line unicorn/no-thenable
			};
			const result = cli({}, () => fakePromise as any);
			expect(result).toHaveProperty('then');
		});
	});

	describe('Promise edge cases', () => {
		test('result properties accessible on Promise return', async () => {
			const result = cli({
				parameters: ['<value>'],
			}, async () => {
				await setImmediate();
			}, ['test']);

			// Properties should be accessible even though callback returns Promise
			expect<string>(result._.value).toBe('test');

			// And it's awaitable
			await result;
		});

		test('cli Promise waits for callback to complete', async () => {
			let callbackCompleted = false;

			const result = cli({}, async () => {
				await setImmediate();
				callbackCompleted = true;
			});

			// Callback shouldn't have completed yet
			expect(callbackCompleted).toBe(false);

			// After awaiting cli, callback should be complete
			await result;
			expect(callbackCompleted).toBe(true);
		});

		test('cli Promise never resolves if callback never resolves', async () => {
			let cliResolved = false;

			const result = cli({}, async () => {
				// Never resolve - hang forever
				await new Promise(() => {});
			});

			// Race the cli promise against a timeout
			await Promise.race([
				result.then(() => {
					cliResolved = true;
				}),
				setImmediate(50),
			]);

			// cli should not have resolved
			expect(cliResolved).toBe(false);
		});
	});
});
