import { testSuite } from 'manten';
import { expectTypeOf } from 'expect-type';
import {
	typeFlag,
	type Flags,
	type TypeFlag,
	type TypeFlagOptions,
	type IgnoreFunction,
} from '#type-flag';

// Test Helpers
const toDate = (s: string) => new Date(s);
const toCustomObject = (s: string) => ({ value: s });
const toAny = (v: string) => v as any; // eslint-disable-line @typescript-eslint/no-explicit-any
const toUnknown = (v: string) => v as unknown;

export default testSuite(({ describe }) => {
	describe('Types', ({ describe, test }) => {
		describe('Errors', ({ test }) => {
			test('Only one element in array allowed', () => {
				typeFlag({
					// @ts-expect-error only one element allowed
					flagB: [String, String],

					// @ts-expect-error only one element allowed
					flagC: {
						type: [String, String],
						alias: 'a',
					},
				}, []);
			});

			test('Alias should be a string', () => {
				typeFlag({
					// @ts-expect-error alias must be a string
					badAlias: {
						type: String,
						alias: 1,
					},
				}, []);
			});
		});

		describe('Inference', ({ test }) => {
			test('typeFlag return type (Comprehensive)', () => {
				const parsed = typeFlag({
					// Basic types
					booleanFlag: Boolean,
					booleanFlagDefault: {
						type: Boolean,
						default: false,
					},
					stringFlag: String,
					stringFlagDefault: {
						type: String,
						default: 'hello',
					},
					numberFlag: Number,
					numberFlagDefault: {
						type: Number,
						default: 1,
					},

					// Array types
					booleanArr: [Boolean],
					stringArr: [String],
					numberArr: [Number],
					stringArrDefault: {
						type: [String],
						default: () => ['a', 'b'],
					},

					// Custom type functions
					date: toDate,
					dateDefault: {
						type: toDate,
						default: () => new Date(),
					},
					customObj: {
						type: toCustomObject,
						alias: 'c',
					},

					// Custom array type functions
					dates: [toDate],
					datesDefault: {
						type: [toDate],
						default: () => [new Date()],
					},

					// Extra options
					extraOptions: {
						type: Boolean,
						alias: 'e',
						default: false,
						description: 'Some description',
					},
				});

				expectTypeOf(parsed).toEqualTypeOf<{
					flags: {
						// Basic types
						booleanFlag: boolean | undefined;
						booleanFlagDefault: boolean;
						stringFlag: string | undefined;
						stringFlagDefault: string;
						numberFlag: number | undefined;
						numberFlagDefault: number;

						// Array types
						booleanArr: boolean[];
						stringArr: string[];
						numberArr: number[];
						stringArrDefault: string[];

						// Custom
						date: Date | undefined;
						dateDefault: Date;
						customObj: { value: string } | undefined;

						// Custom array
						dates: Date[];
						datesDefault: Date[];

						// Extra
						extraOptions: boolean;
					};
					unknownFlags: {
						[flag: string]: (string | boolean)[];
					};
					_: string[] & { '--': string[] };
				}>();
			});

			test('Schema with as const assertion', () => {
				const parsed = typeFlag({
					flagA: {
						type: [String],
					},
					flagB: String,
				} as const, []);

				expectTypeOf(parsed.flags.flagA).toEqualTypeOf<string[]>();
				expectTypeOf(parsed.flags.flagB).toEqualTypeOf<string | undefined>();
			});

			test('Schema wrapped in Readonly generic', () => {
				const readonly = <F extends Flags>(
					options: Readonly<F>,
					argv: string[],
				) => typeFlag(options, argv);

				const parsed = readonly({
					flagA: {
						type: String,
					},
					flagB: {
						type: [String],
					},
				}, ['--flag-a', 'hello']);

				expectTypeOf(parsed.flags.flagA).toEqualTypeOf<string | undefined>();
				expectTypeOf(parsed.flags.flagB).toEqualTypeOf<string[]>();
			});

			test('Default type inference edge cases', () => {
				const parsed = typeFlag({
					// Single type, array default
					singleWithArrDefault: {
						type: String,
						default: () => ['a', 'b'],
					},
					// Array type, single default
					arrWithSingleDefault: {
						type: [Number],
						default: 123,
					},
					// Type/default literal mismatch
					typeMismatch: {
						type: String,
						default: 123,
					},
					// Type/default function mismatch - preserves literal type
					typeMismatchFn: {
						type: Boolean,
						default: () => 'hello',
					},
					// Type assertion widens to base type
					widenedFlag: {
						type: String,
						default: () => 'hello' as string,
					},
					// as const preserves literal type explicitly
					constFlag: {
						type: Number,
						default: () => 42 as const,
					},
				});

				expectTypeOf(parsed.flags.singleWithArrDefault).toEqualTypeOf<string | string[]>();
				expectTypeOf(parsed.flags.arrWithSingleDefault).toEqualTypeOf<number[] | number>();
				expectTypeOf(parsed.flags.typeMismatch).toEqualTypeOf<string | number>();
				expectTypeOf(parsed.flags.typeMismatchFn).toEqualTypeOf<boolean | 'hello'>();
				expectTypeOf(parsed.flags.widenedFlag).toEqualTypeOf<string>();
				expectTypeOf(parsed.flags.constFlag).toEqualTypeOf<number | 42>();
			});

			test('any/unknown/never types', () => {
				const parsed = typeFlag({
					anyFlag: toAny,
					unknownFlag: toUnknown,
					anyArr: [toAny],
					unknownArr: [toUnknown],
					// A function returning never
					neverFlag: {
						type: () => {
							throw new Error('This function never returns');
						},
					},
				});

				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				expectTypeOf(parsed.flags.anyFlag).toEqualTypeOf<any | undefined>();
				expectTypeOf(parsed.flags.unknownFlag).toEqualTypeOf<unknown | undefined>();

				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				expectTypeOf(parsed.flags.anyArr).toEqualTypeOf<any[]>();
				expectTypeOf(parsed.flags.unknownArr).toEqualTypeOf<unknown[]>();
				expectTypeOf(parsed.flags.neverFlag).toBeNever();
			});

			test('Flags type errors on invalid custom options', () => {
				type CustomOptions = {
					newProperty: string;
				};

				expectTypeOf<{
					verbose: {
						type: BooleanConstructor;
						alias: string;
						newProperty: number;
					};
				}>().not.toMatchObjectType<Flags<CustomOptions>>();
			});

			test('TypeFlag generic with schema', () => {
				type Schema = {
					name: StringConstructor;
					age: {
						type: NumberConstructor;
						default: number;
					};
					verbose: [BooleanConstructor];
					custom: typeof toDate;
					customArr: readonly [typeof toDate];
				};

				expectTypeOf<TypeFlag<Schema>>().toEqualTypeOf<{
					flags: {
						name: string | undefined;
						age: number;
						verbose: boolean[];
						custom: Date | undefined;
						customArr: Date[];
					};
					unknownFlags: {
						[flagName: string]: (string | boolean)[];
					};
					_: string[] & { '--': string[] };
				}>();
			});

			test('TypeFlag generic without parameter (default)', () => {
				expectTypeOf<TypeFlag>().toEqualTypeOf<{
					flags: {
						[flag: string]: unknown;
					};
					unknownFlags: {
						[flagName: string]: (string | boolean)[];
					};
					_: string[] & { '--': string[] };
				}>();
			});

			test('Types work in function signatures', () => {
				// Test that exported types can be used to type function parameters/returns
				const processFlags = (
					parsed: TypeFlag<{
						name: StringConstructor;
						date: typeof toDate;
					}>,
				) => ({
					name: parsed.flags.name,
					date: parsed.flags.date,
				});

				const result = processFlags(typeFlag({
					name: String,
					date: toDate,
				}));
				expectTypeOf(result.name).toEqualTypeOf<string | undefined>();
				expectTypeOf(result.date).toEqualTypeOf<Date | undefined>();

				// Test Flags as parameter type
				const createParser = (schema: Flags) => typeFlag(schema);

				const parser = createParser({ test: Boolean });
				expectTypeOf(parser.flags.test).toBeUnknown(); // Correct

				// Test TypeFlagOptions as parameter (pure type test, no runtime)
				type OptionsTest = TypeFlagOptions;
				const options: OptionsTest = { ignore: () => false };
				expectTypeOf(options).toExtend<TypeFlagOptions>();
			});
		});

		describe('TypeFlagOptions', ({ test }) => {
			test('Call-Site Signatures', () => {
				const ignoreFunction: IgnoreFunction = () => {};

				// Valid calls - 2 or 3 params
				ignoreFunction('argument', 'some/path');
				ignoreFunction('argument', 'some/path', undefined);
				ignoreFunction('argument', 'some/path', 'value');
				ignoreFunction('known-flag', '--foo', 'bar');
				ignoreFunction('unknown-flag', '--baz', undefined);
				ignoreFunction('known-flag', '--foo');

				// @ts-expect-error 2nd param must be string
				ignoreFunction('argument', 123);

				// @ts-expect-error 2nd param must be string
				ignoreFunction('known-flag', 123);

				// @ts-expect-error 3rd param must be string or undefined
				ignoreFunction('known-flag', '--foo', 123);

				// @ts-expect-error 'type' must be one of the three constants
				ignoreFunction('other-type', 'foo');
			});

			test('Implementation with explicit types', () => {
				const options: TypeFlagOptions = {
					ignore: (
						type: 'argument' | 'known-flag' | 'unknown-flag',
						argvElement: string,
						flagValue?: string,
					) => {
						expectTypeOf(type).toEqualTypeOf<'argument' | 'known-flag' | 'unknown-flag'>();
						expectTypeOf(argvElement).toEqualTypeOf<string>();
						expectTypeOf(flagValue).toEqualTypeOf<string | undefined>();
						return false;
					},
				};
				expectTypeOf(options).toExtend<TypeFlagOptions>();
			});

			test('ignore option is optional', () => {
				const noIgnore: TypeFlagOptions = {};
				expectTypeOf(noIgnore).toExtend<TypeFlagOptions>();

				// Test runtime usage compiles
				typeFlag({}, [], noIgnore);
			});

			test('ignore option accepts a minimal implementation', () => {
				// Test minimal (un-typed) signature
				const minimalOptions: TypeFlagOptions = {
					ignore: type => type === 'unknown-flag',
				};
				expectTypeOf(minimalOptions).toExtend<TypeFlagOptions>();

				// Test runtime usage compiles
				typeFlag({}, [], minimalOptions);
			});

			test('3-parameter callback works', () => {
				const options: TypeFlagOptions = {
					ignore(type, argvElement, flagValue) {
						expectTypeOf(type).toEqualTypeOf<'argument' | 'known-flag' | 'unknown-flag'>();
						expectTypeOf(argvElement).toEqualTypeOf<string>();
						expectTypeOf(flagValue).toEqualTypeOf<string | undefined>();
						return false;
					},
				};
				expectTypeOf(options).toExtend<TypeFlagOptions>();
			});
		});

		test('Generic with Readonly<T> parameter inference (TypeScript compiler bug)', () => {
			/**
			 * Tests workaround for TS bug: Readonly<T> in parameter position breaks
			 * conditional type matching in return type. Without `& Object_` workaround
			 * in InferFlagType/InferDefaultType, this returns `never` instead of correct type.
			 *
			 * @see https://github.com/microsoft/TypeScript/issues/62720
			 */
			const wrapper = <Options extends Flags>(options: Readonly<Options>) => typeFlag(options);

			const argv = wrapper({
				booleanFlag: Boolean,
				booleanFlagDefault: {
					type: Boolean,
					default: false,
				},
				booleanArray: {
					type: [Boolean],
					default: [false],
				},
			});

			expectTypeOf(argv.flags.booleanFlag).toEqualTypeOf<boolean | undefined>();
			expectTypeOf(argv.flags.booleanFlagDefault).toBeBoolean();
			expectTypeOf(argv.flags.booleanArray).toEqualTypeOf<boolean[]>();
		});
	});
});
