import type { IsAny } from '../any/any_type.js'
import type { IsArray } from '../array/array_type.js'
import type { IsBigint } from '../bigint/bigint_type.js'
import type { IsStrictBigint } from '../bigint/strict_bigint_type.js'
import type { IsBoolean } from '../boolean/boolean_type.js'
import type { IsFalse } from '../boolean/false_type.js'
import type { IsStrictBoolean } from '../boolean/strict_boolean_type.js'
import type { IsTrue } from '../boolean/true_type.js'
import type { IsEqual } from '../equal/equal.js'
import type { IsFunction } from '../function/function_type.js'
import type { IsStrictFunction } from '../function/strict_function_type.js'
import type { IsNever } from '../never/never_type.js'
import type { IsNull } from '../null/null_type.js'
import type { IsNumber } from '../number/number_type.js'
import type { IsStrictNumber } from '../number/strict_number_type.js'
import type { IsObject } from '../object/object_type.js'
import type { CanAssign, StrictCanAssign } from '../predicates/CanAssign.js'
import type { IsStrictString } from '../string/strict_string_type.js'
import type { IsString } from '../string/string_type.js'
import type { IsSymbol } from '../symbol/symbol_type.js'
import type { IsTuple } from '../tuple/tuple_type.js'
import type { IsUndefined } from '../undefined/undefined_type.js'
import type { IsUnknown } from '../unknown/unknown_type.js'
import type { IsVoid } from '../void/void_type.js'

export namespace testType {
	export interface TestType {
		/**
		 * Check if type `A` is equal to type `B` and `C`.
		 *
		 * @return `expected` as `A` for type inspection.
		 */
		equal<A, B, C>(expected: IsEqual<A, B> & IsEqual<A, C>): A,
		/**
		 * Check if type `A` is equal to type `B`.
		 *
		 * @return `expected` as `A` for type inspection.
		 */
		equal<A, B>(expected: IsEqual<A, B>): A,
		/**
		 * Check if `A` can assign to `B`.
		 *
		 * If `A` is a union,
		 * the check is distributive.
		 *
		 * Meaning the result can be `boolean`,
		 * meaning both `true` and `false` will pass.
		 *
		 * If you want to avoid the distributivity,
		 * use `testType.strictCanAssign()` instead.
		 *
		 * @example
		 * ```ts
		 * testType.canAssign<123, number> // true
		 *
		 * testType.canAssign<number | string, number> // boolean
		 * ```
		 *
		 * @return `expected` as `A` for type inspection.
		 */
		canAssign<A, B>(expected: CanAssign<A, B>): A,
		/**
		 * Check if `A` can fully assign to `B`.
		 *
		 * This checks all branches in an union `A` are assignable to `B`.
		 *
		 * @example
		 * ```ts
		 * testType.strictCanAssign<number | string, number | string> // true
		 *
		 * testType.strictCanAssign<number | string, number> // false
		 * ```
		 *
		 * @return `expected` as `A` for type inspection.
		 */
		strictCanAssign<A, B>(expected: StrictCanAssign<A, B>): A,
		/**
		 * Check if type `T` is exactly `any`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		any<T>(expected: IsAny<T>): T,
		/**
		 * Check if type `T` is exactly `array`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		array<T>(expected: IsArray<T>): T,
		/**
		 * Check if type `T` is exactly `bigint`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		strictBigint<T>(expected: IsStrictBigint<T>): T,
		/**
		 * Check if type `T` is `bigint` or bigint literals.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		bigint<T>(expected: IsBigint<T>): T,
		/**
		 * Check if type `T` is exactly `boolean`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		strictBoolean<T>(expected: IsStrictBoolean<T>): T,
		/**
		 * Check if type `T` is `boolean` and boolean literals.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		boolean<T>(expected: IsBoolean<T>): T,
		/**
		 * Check if type `T` is exactly `true`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		true<T>(expected: IsTrue<T>): T,
		/**
		 * Check if type `T` is exactly `false`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		false<T>(expected: IsFalse<T>): T,
		/**
		 * Check if type `T` is exactly `boolean`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		strictFunction<T>(expected: IsStrictFunction<T>): T,
		/**
		 * Check if type `T` is `boolean` and boolean literals.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		function<T>(expected: IsFunction<T>): T,
		/**
		 * Check if type `T` is exactly `never`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		never<T>(expected: IsNever<T>): T,
		/**
		 * Check if type `T` is exactly `null`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		null<T>(expected: IsNull<T>): T,
		/**
		 * Check if type `T` is exactly `number`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		strictNumber<T>(expected: IsStrictNumber<T>): T,
		/**
		 * Check if type `T` is `number` or number literals.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		number<T>(expected: IsNumber<T>): T,
		/**
		 * Check if type `T` is `object`.
		 *
		 * Note that `Function`, `Array`, and *tuple* are also `object`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		object<T>(expected: IsObject<T>): T,
		/**
		 * Check if type `T` is exactly `string`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		strictString<T>(expected: IsStrictString<T>): T,
		/**
		 * Check if type `T` is `string` or string literals.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		string<T>(expected: IsString<T>): T,
		/**
		 * Check if type `T` is a `symbol`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		symbol<T>(expected: IsSymbol<T>): T,
		/**
		 * Check if type `T` is a *tuple*.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		tuple<T>(expected: IsTuple<T>): T,
		/**
		 * Check if type `T` is exactly `undefined`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		undefined<T>(expected: IsUndefined<T>): T,
		// hasUndefined<T>(expected: CanAssign<T, undefined>): T
		/**
		 * Check if type `T` is exactly `unknown`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		unknown<T>(expected: IsUnknown<T>): T,
		/**
		 * Check if type `T` is exactly `void`.
		 *
		 * @return `expected` as `T` for type inspection.
		 */
		void<T>(expected: IsVoid<T>): T,
		/**
		 * A quick way to inspect a type.
		 *
		 * The handler receives a `InspectedType` object.
		 * It contains `value` which is typed to `T`,
		 * and many other properties to inspect the behavior of `T`.
		 *
		 * The handler is not being call,
		 * it is use to hold the type in value for inspection.
		 *
		 * 🧪 *testing*
		 * 🦴 *utilities*
		 *
		 * @example
		 * ```ts
		 * testType.inspect<SomeType>(t => {
		 *   type T = typeof t.value // resolve and inspect the type `T`
		 *   t.extend_boolean // result of `T extends boolean`
		 * })
		 * ```
		 *
		 * After trying out the type, remove the line.
		 */
		inspect<T>(handler: (t: InspectedType<T>) => unknown): T
	}

	export type InspectedType<T> = {
		type: T,
		extends<R>(): T extends R ? true : false,
		extends_any: T extends any ? true : false,
		extends_unknown: T extends unknown ? true : false,
		extends_void: T extends void ? true : false,
		extends_never: T extends never ? true : false,
		extends_undefined: T extends undefined ? true : false,
		extends_null: T extends null ? true : false,
		extends_boolean: T extends boolean ? true : false,
		extends_true: T extends true ? true : false,
		extends_false: T extends false ? true : false,
		extends_number: T extends number ? true : false,
		extends_1: T extends 1 ? true : false,
		extends_bigint: T extends bigint ? true : false,
		extends_1n: T extends 1n ? true : false,
		extends_string: T extends string ? true : false,
		extends_a: T extends 'a' ? true : false,
		extends_symbol: T extends symbol ? true : false,
		extends_object: T extends object ? true : false,
		extends_function: T extends Function ? true : false,
		extends_array_unknown: T extends unknown[] ? true : false,
		extends_tuple_empty: T extends [] ? true : false,
		union<R>(): T | R,
		union_any: T | any,
		union_unknown: T | unknown,
		union_void: T | void,
		union_never: T | never,
		union_undefined: T | undefined,
		union_null: T | null,
		union_boolean: T | boolean,
		union_true: T | true,
		union_false: T | false,
		union_number: T | number,
		union_1: T | 1,
		union_bigint: T | bigint,
		union_1n: T | 1n,
		union_string: T | string,
		union_a: T | 'a',
		union_symbol: T | symbol,
		union_object: T | object,
		union_function: T | Function,
		union_array_unknown: T | unknown[],
		union_tuple_empty: T | [],
		intersect<R>(): T & R,
		intersect_any: T & any,
		intersect_unknown: T & unknown,
		intersect_void: T & void,
		intersect_never: T & never,
		intersect_undefined: T & undefined,
		intersect_null: T & null,
		intersect_boolean: T & boolean,
		intersect_true: T & true,
		intersect_false: T & false,
		intersect_number: T & number,
		intersect_1: T & 1,
		intersect_bigint: T & bigint,
		intersect_1n: T & 1n,
		intersect_string: T & string,
		intersect_a: T & 'a',
		intersect_symbol: T & symbol,
		intersect_object: T & object,
		intersect_function: T & Function,
		intersect_array_unknown: T & unknown[],
		intersect_tuple_empty: T & []
	}
}

/**
 * Test utilities for types.
 *
 * This is designed specifically for testing.
 * The return value is the input `expected` parameter asserted as the first type parameter,
 * so that the type can be further inspected.
 */
export const testType = new Proxy({} as testType.TestType, {
	get(_target, _prop, _receiver) {
		return (expected: unknown) => expected
	}
})
