/* eslint-disable @typescript-eslint/naming-convention */
import {expectType, expectAssignable, expectNotType} from 'tsd';
import camelcaseKeys, {type CamelCaseKeys} from './index.js';

const fooBarObject = {'foo-bar': true};
const camelFooBarObject = camelcaseKeys(fooBarObject);
expectType<{fooBar: boolean}>(camelFooBarObject);

const fooBarArray = [{'foo-bar': true}];
const camelFooBarArray = camelcaseKeys(fooBarArray);
expectType<Array<{fooBar: boolean}>>(camelFooBarArray);

expectType<Array<{fooBar: boolean}>>(camelcaseKeys([{'foo-bar': true}]));

expectType<readonly [{readonly fooBar: true}, {readonly fooBaz: true}]>(camelcaseKeys([{'foo-bar': true}, {'foo-baz': true}] as const));

expectType<{fooBar: boolean}>(camelcaseKeys({'foo-bar': true}));
expectType<{fooBar: boolean}>(camelcaseKeys({'--foo-bar': true}));
expectType<{fooBar: boolean}>(camelcaseKeys({foo_bar: true}));
expectType<{fooBar: boolean}>(camelcaseKeys({'foo bar': true}));

expectType<{readonly fooBar: true}>(camelcaseKeys({'foo-bar': true} as const));
expectType<{readonly fooBar: true}>(camelcaseKeys({'--foo-bar': true} as const));
expectType<{readonly fooBar: true}>(camelcaseKeys({foo_bar: true} as const));
expectType<{readonly fooBar: true}>(camelcaseKeys({'foo bar': true} as const));

expectType<{fooBar: {fooBar: {fooBar: boolean}}}>(camelcaseKeys({'foo-bar': {foo_bar: {'foo bar': true}}}, {deep: true}));
expectType<{fooBar: Array<{fooBar: {fooBar: boolean}}>}>(camelcaseKeys({'foo-bar': [{foo_bar: {'foo bar': true}}]}, {deep: true}));

type ObjectOrUndefined = {
	foo_bar: {
		foo_bar:
			| {
				foo_bar: boolean;
			}
			| undefined;
	};
};

const objectOrUndefined: ObjectOrUndefined = {
	foo_bar: {
		foo_bar: {
			foo_bar: true,
		},
	},
};

expectType<{fooBar: {fooBar: {fooBar: boolean} | undefined}}>(camelcaseKeys(objectOrUndefined, {deep: true}));

expectType<{FooBar: boolean}>(camelcaseKeys({'foo-bar': true}, {pascalCase: true}));
expectType<{readonly FooBar: true}>(camelcaseKeys({'foo-bar': true} as const, {pascalCase: true}));
expectType<{FooBar: boolean}>(camelcaseKeys({'--foo-bar': true}, {pascalCase: true}));
expectType<{FooBar: boolean}>(camelcaseKeys({foo_bar: true}, {pascalCase: true}));
expectType<{FooBar: boolean}>(camelcaseKeys({'foo bar': true}, {pascalCase: true}));
expectType<{FooBar: {FooBar: {FooBar: boolean}}}>(camelcaseKeys(
	{'foo-bar': {foo_bar: {'foo bar': true}}},
	{deep: true, pascalCase: true},
));

expectType<{fooBAR: boolean}>(camelcaseKeys({'foo-BAR': true}, {preserveConsecutiveUppercase: true}));
expectType<{readonly fooBAR: true}>(camelcaseKeys({foo_BAR: true} as const, {preserveConsecutiveUppercase: true}));
expectType<{fooBAR: boolean}>(camelcaseKeys({'--foo-BAR': true}, {preserveConsecutiveUppercase: true}));
expectType<{fooBAR: boolean}>(camelcaseKeys({foo_BAR: true}, {preserveConsecutiveUppercase: true}));
expectType<{fooBAR: boolean}>(camelcaseKeys({'foo BAR': true}, {preserveConsecutiveUppercase: true}));
expectType<{FooBAR: boolean}>(camelcaseKeys({'foo BAR': true}, {preserveConsecutiveUppercase: true, pascalCase: true}));
expectType<{fooBAR: {fooBAR: {fooBAR: boolean}}}>(camelcaseKeys(
	{'foo-BAR': {foo_BAR: {'foo BAR': true}}},
	{deep: true, preserveConsecutiveUppercase: true},
));

expectType<{fooBar: boolean; foo_bar: true}>(camelcaseKeys(
	{'foo-bar': true, foo_bar: true},
	{exclude: ['foo', 'foo_bar', /bar/] as const},
));

expectType<{fooBar: boolean}>(camelcaseKeys({'foo-bar': true}, {stopPaths: ['foo']}));
expectType<{topLevel: {fooBar: {'bar-baz': boolean}}; fooFoo: boolean}>(camelcaseKeys(
	{'top-level': {'foo-bar': {'bar-baz': true}}, 'foo-foo': true},
	{deep: true, stopPaths: ['top-level.foo-bar'] as const},
));

expectAssignable<Record<string, string>>(camelcaseKeys({} as Record<string, string>));

expectAssignable<Record<string, string>>(camelcaseKeys({} as Record<string, string>, {deep: true}));

type SomeObject = {
	someProperty: string;
};

const someObject: SomeObject = {
	someProperty: 'hello',
};

expectType<SomeObject>(camelcaseKeys(someObject));
expectType<SomeObject[]>(camelcaseKeys([someObject]));
expectType<{someObject: SomeObject}>(camelcaseKeys({some_object: someObject}));
expectType<Array<{someObject: SomeObject}>>(camelcaseKeys([{some_object: someObject}]));

type SomeTypeAlias = {
	someProperty: string;
};

const objectWithTypeAlias = {
	someProperty: 'this should also work',
};

expectType<SomeTypeAlias>(camelcaseKeys(objectWithTypeAlias));
expectType<SomeTypeAlias[]>(camelcaseKeys([objectWithTypeAlias]));

// Using exported type
expectType<CamelCaseKeys<typeof fooBarArray>>(camelFooBarArray);

const arrayItems = [{fooBar: true}, {fooBaz: true}] as const;
expectType<CamelCaseKeys<typeof arrayItems>>(camelcaseKeys(arrayItems));

expectType<CamelCaseKeys<{'foo-bar': boolean}>>(camelcaseKeys({'foo-bar': true}));
expectType<CamelCaseKeys<{'--foo-bar': boolean}>>(camelcaseKeys({'--foo-bar': true}));
expectType<CamelCaseKeys<{foo_bar: boolean}>>(camelcaseKeys({foo_bar: true}));
expectType<CamelCaseKeys<{'foo bar': boolean}>>(camelcaseKeys({'foo bar': true}));

expectType<CamelCaseKeys<{readonly 'foo-bar': true}>>(camelcaseKeys({'foo-bar': true} as const));
expectType<CamelCaseKeys<{readonly '--foo-bar': true}>>(camelcaseKeys({'--foo-bar': true} as const));
expectType<CamelCaseKeys<{readonly foo_bar: true}>>(camelcaseKeys({foo_bar: true} as const));
expectType<CamelCaseKeys<{readonly 'foo bar': true}>>(camelcaseKeys({'foo bar': true} as const));

const nestedItem = {'foo-bar': {foo_bar: {'foo bar': true}}};
expectType<CamelCaseKeys<typeof nestedItem, true>>(camelcaseKeys(nestedItem, {deep: true}));

expectType<CamelCaseKeys<ObjectOrUndefined, true>>(camelcaseKeys(objectOrUndefined, {deep: true}));

expectType<CamelCaseKeys<{'foo-bar': boolean}, false, true>>(camelcaseKeys({'foo-bar': true}, {pascalCase: true}));
expectType<CamelCaseKeys<{readonly 'foo-bar': true}, false, true>>(camelcaseKeys({'foo-bar': true} as const, {pascalCase: true}));
expectType<CamelCaseKeys<{'--foo-bar': boolean}, false, true>>(camelcaseKeys({'foo-bar': true}, {pascalCase: true}));
expectType<CamelCaseKeys<{foo_bar: boolean}, false, true>>(camelcaseKeys({'foo-bar': true}, {pascalCase: true}));
expectType<CamelCaseKeys<{'foo bar': boolean}, false, true>>(camelcaseKeys({'foo-bar': true}, {pascalCase: true}));
expectType<CamelCaseKeys<typeof nestedItem, true, true>>(camelcaseKeys(nestedItem, {deep: true, pascalCase: true}));

const data = {'foo-bar': true, foo_bar: true};
const exclude = ['foo', 'foo_bar', /bar/] as const;

expectType<CamelCaseKeys<typeof data, false, false, false, typeof exclude>>(camelcaseKeys(data, {exclude}));

const nonNestedWithStopPathData = {'foo-bar': true, foo_bar: true};
expectType<
	CamelCaseKeys<typeof nonNestedWithStopPathData, false, false, false, ['foo']>
>(camelcaseKeys({'foo-bar': true}, {stopPaths: ['foo']}));
const nestedWithStopPathData = {
	'top-level': {'foo-bar': {'bar-baz': true}},
	'foo-foo': true,
};
const stopPaths = ['top-level.foo-bar'] as const;
expectType<
	CamelCaseKeys<
	typeof nestedWithStopPathData,
		true,
		false,
		false,
		readonly never[],
	typeof stopPaths
	>
>(camelcaseKeys(nestedWithStopPathData, {deep: true, stopPaths}));

expectAssignable<CamelCaseKeys<Record<string, string>>>(camelcaseKeys({} as Record<string, string>));

expectAssignable<CamelCaseKeys<Record<string, string>, true>>(camelcaseKeys({} as Record<string, string>, {deep: true}));

expectType<CamelCaseKeys<SomeObject>>(camelcaseKeys(someObject));
expectType<CamelCaseKeys<SomeObject[]>>(camelcaseKeys([someObject]));

expectType<CamelCaseKeys<SomeTypeAlias>>(camelcaseKeys(objectWithTypeAlias));
expectType<CamelCaseKeys<SomeTypeAlias[]>>(camelcaseKeys([objectWithTypeAlias]));

// Verify exported type `CamelcaseKeys`
// Mapping types and retaining properties of keys
// https://github.com/microsoft/TypeScript/issues/13224

type ObjectDataType = {
	foo_bar?: string;
	bar_baz?: string;
	baz: string;
};
type InvalidConvertedObjectDataType = {
	fooBar: string;
	barBaz: string;
	baz: string;
};
type ConvertedObjectDataType = {
	fooBar?: string;
	barBaz?: string;
	baz: string;
};

const objectInputData: ObjectDataType = {
	foo_bar: 'foo_bar',
	baz: 'baz',
};
expectType<ConvertedObjectDataType>(camelcaseKeys(objectInputData));
expectNotType<InvalidConvertedObjectDataType>(camelcaseKeys(objectInputData));

// Array
type ArrayDataType = ObjectDataType[];

const arrayInputData: ArrayDataType = [
	{
		foo_bar: 'foo_bar',
		baz: 'baz',
	},
];
expectType<ConvertedObjectDataType[]>(camelcaseKeys(arrayInputData));
expectNotType<InvalidConvertedObjectDataType[]>(camelcaseKeys(arrayInputData));

// Deep
type DeepObjectType = {
	foo_bar?: string;
	bar_baz?: string;
	baz: string;
	first_level: {
		foo_bar?: string;
		bar_baz?: string;
		second_level: {
			foo_bar: string;
			bar_baz?: string;
		};
	};
	optional_first_level?: {
		foo_bar?: string;
		bar_baz?: true;
		optional_second_level?: {
			foo_bar: number;
		};
	};
};
type InvalidConvertedDeepObjectDataType = {
	fooBar?: string;
	barBaz?: string;
	baz: string;
	first_level?: {
		fooBar?: string;
		barBaz?: string;
		second_level?: {
			fooBar: string;
			barBaz?: string;
		};
	};
	optional_first_level?: {
		fooBar?: string;
		barBaz?: true;
		optional_second_level?: {
			fooBar: number;
		};
	};
};
type ConvertedDeepObjectDataType = {
	fooBar?: string;
	barBaz?: string;
	baz: string;
	firstLevel: {
		foo_bar?: string;
		bar_baz?: string;
		second_level: {
			foo_bar: string;
			bar_baz?: string;
		};
	};
	optionalFirstLevel?: {
		foo_bar?: string;
		bar_baz?: true;
		optional_second_level?: {
			foo_bar: number;
		};
	};
};
type ConvertedDeepObjectDataTypeWithDeepTrue = {
	fooBar?: string | undefined;
	barBaz?: string | undefined;
	baz: string;
	firstLevel: {
		fooBar?: string | undefined;
		barBaz?: string | undefined;
		secondLevel: {
			fooBar: string;
			barBaz?: string | undefined;
		};
	};
	optionalFirstLevel?: {
		fooBar?: string;
		barBaz?: true;
		optionalSecondLevel?: {
			fooBar: number;
		};
	};
};
const deepInputData: DeepObjectType = {
	foo_bar: 'foo_bar',
	baz: 'baz',
	first_level: {
		bar_baz: 'bar_baz',
		second_level: {
			foo_bar: 'foo_bar',
		},
	},
};
expectType<ConvertedDeepObjectDataType>(camelcaseKeys(deepInputData, {deep: false}));
expectType<ConvertedDeepObjectDataTypeWithDeepTrue>(camelcaseKeys(deepInputData, {deep: true}));
expectNotType<InvalidConvertedDeepObjectDataType>(camelcaseKeys(deepInputData, {deep: false}));

// Exclude
type InvalidConvertedExcludeObjectDataType = {
	foo_bar?: string;
	bar_baz?: string;
	baz: string;
};
type ConvertedExcludeObjectDataType = {
	foo_bar?: string;
	barBaz?: string;
	baz: string;
};
const excludeInputData: ObjectDataType = {
	foo_bar: 'foo_bar',
	bar_baz: 'bar_baz',
	baz: 'baz',
};
expectType<ConvertedExcludeObjectDataType>(camelcaseKeys(excludeInputData, {
	exclude,
}));
expectNotType<InvalidConvertedExcludeObjectDataType>(camelcaseKeys(excludeInputData, {
	exclude,
}));

expectType<{
	funcFoo: () => 'foo';
	recordBar: {foo: string};
	promiseBaz: Promise<unknown>;
}>(camelcaseKeys({
			func_foo: () => 'foo',
			record_bar: {foo: 'bar'},
			promise_baz: new Promise(resolve => {
				resolve(true);
			}),
		}));

// Test for function with inferred type
function camelcaseKeysDeep<
	T extends Record<string, unknown> | ReadonlyArray<Record<string, unknown>>,
>(response: T): CamelCaseKeys<T, true> {
	return camelcaseKeys(response, {deep: true});
}

function camelcaseKeysPascalCase<
	T extends Record<string, unknown> | ReadonlyArray<Record<string, unknown>>,
>(response: T): CamelCaseKeys<T, false, true> {
	return camelcaseKeys(response, {pascalCase: true});
}

expectType<{fooBar: {hogeHoge: string}}>(camelcaseKeysDeep({foo_bar: {hoge_hoge: 'hoge_hoge'}}));
expectType<{FooBar: string}>(camelcaseKeysPascalCase({foo_bar: 'foo_bar'}));

// Test for union type
const objectCamelcased: CamelCaseKeys<{foo_bar: {foo_prop: string} | undefined}, true>
	= camelcaseKeys({foo_bar: {foo_prop: 'foo_props'}}, {deep: true});
const nullCamelcased: CamelCaseKeys<{foo_bar: {foo_prop: string} | undefined}, true>
	= camelcaseKeys({foo_bar: undefined}, {deep: true});

expectType<{fooBar: {fooProp: string} | undefined}>(objectCamelcased);
expectType<{fooBar: {fooProp: string} | undefined}>(nullCamelcased);

// Test for union type in arrays (Issue #130)
const arrayOfUnionType: CamelCaseKeys<
	{
		foo_bar: Array<{foo_prop: string} | undefined>;
	},
	true
> = camelcaseKeys({foo_bar: [{foo_prop: 'foo_prop'}]}, {deep: true});

// This should work without TypeScript errors
expectType<{fooBar: Array<{fooProp: string} | undefined>}>(arrayOfUnionType);

// Test with null union (similar to original issue but following project style)
const arrayOfUnionWithNull: CamelCaseKeys<
	{
		foo_bar: Array<{foo_prop: string} | undefined>;
	},
	true
> = camelcaseKeys({foo_bar: [{foo_prop: 'foo_prop'}]}, {deep: true});

expectType<{fooBar: Array<{fooProp: string} | undefined>}>(arrayOfUnionWithNull);

// Test for issue #114 - Interface type constraint issue (Fixed)
// TypeScript interfaces can now be used directly with camelcase-keys
// Using interface specifically to test interface support (not type alias)
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface TestInterface {
	snake_case_prop: string;
	another_prop: number;
}

// Direct usage with interface now works after removing the constraint
const interfaceTest: TestInterface = {snake_case_prop: 'test', another_prop: 123};
const interfaceResult = camelcaseKeys(interfaceTest);
// Check that the transformation works (runtime will transform keys correctly)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const _typeCheck: typeof interfaceResult = {} as any;

// Test nested interfaces
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface NestedInterface {
	outer_key: {
		inner_key: string;
	};
}

const nestedInterfaceTest: NestedInterface = {outer_key: {inner_key: 'value'}};
const nestedInterfaceResult = camelcaseKeys(nestedInterfaceTest, {deep: true});
// Check that nested transformation works
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const _nestedCheck: typeof nestedInterfaceResult = {} as any;

// Test arrays of interfaces
const interfaceArray: TestInterface[] = [
	{snake_case_prop: 'test1', another_prop: 1},
	{snake_case_prop: 'test2', another_prop: 2},
];
const interfaceArrayResult = camelcaseKeys(interfaceArray);
// Check that array transformation works
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const _arrayCheck: typeof interfaceArrayResult = {} as any;

// Test numeric string keys are preserved (issue #68)
// eslint-disable-next-line @stylistic/quote-props
expectType<{'4.2': string}>(camelcaseKeys({'4.2': 'foo'}));
expectType<{42: string}>(camelcaseKeys({42: 'foo'}));
// eslint-disable-next-line @stylistic/quote-props
expectType<{'42': string}>(camelcaseKeys({'42': 'foo'}));

// Numeric keys should be preserved with deep option
// eslint-disable-next-line @stylistic/quote-props
expectType<{fooBar: {'4.2': string}}>(camelcaseKeys({foo_bar: {'4.2': 'nested'}}, {deep: true}));

// Numeric keys in arrays
// eslint-disable-next-line @stylistic/quote-props
expectType<Array<{'4.2': string}>>(camelcaseKeys([{'4.2': 'foo'}]));

// Non-numeric keys starting with numbers should still be transformed
expectType<{'42Foo': string}>(camelcaseKeys({'42-foo': 'value'}));

// Test for issue #83 - exclude without `as const`
type Language = {
	iso_639_1: string;
	name: string;
};

type Image = {
	aspect_ratio: number;
	file_path: string;
	height: number;
	iso_639_1: string | undefined;
	vote_average: number;
	vote_count: number;
	width: number;
};

type MovieDetails = {
	id: number;
	spoken_languages: Language[];
	images: Image[];
};

const movieData: MovieDetails = {
	id: 1,
	spoken_languages: [
		{iso_639_1: 'en', name: 'English'},
	],
	images: [
		{
			aspect_ratio: 1.5,
			file_path: '/path.jpg',
			height: 100,
			iso_639_1: 'en',
			vote_average: 5,
			vote_count: 10,
			width: 150,
		},
	],
};

// WITHOUT `as const` - the problematic case
const result1 = camelcaseKeys(movieData, {
	exclude: ['iso_639_1', 'iso_3166_1'],
	deep: true,
});

// Since exclude is string[], TypeScript can't properly exclude specific keys
// It will convert everything to camelCase
expectType<{
	id: number;
	spokenLanguages: Array<{
		iso6391: string; // Bug: should be iso_639_1 but TypeScript doesn't know
		name: string;
	}>;
	images: Array<{
		aspectRatio: number;
		filePath: string;
		height: number;
		iso6391: string | undefined; // Bug: should be iso_639_1 but TypeScript doesn't know
		voteAverage: number;
		voteCount: number;
		width: number;
	}>;
}>(result1);

// WITH `as const` - this works correctly
const result2 = camelcaseKeys(movieData, {
	exclude: ['iso_639_1', 'iso_3166_1'] as const,
	deep: true,
});

// With as const, TypeScript knows the exact strings in the exclude array
expectType<{
	id: number;
	spokenLanguages: Array<{
		iso_639_1: string; // Correctly preserved
		name: string;
	}>;
	images: Array<{
		aspectRatio: number;
		filePath: string;
		height: number;
		iso_639_1: string | undefined; // Correctly preserved
		voteAverage: number;
		voteCount: number;
		width: number;
	}>;
}>(result2);

// Test for leading underscore and dollar sign preservation
expectType<{_fooBar: boolean}>(camelcaseKeys({_foo_bar: true}));
expectType<{$fooBar: boolean}>(camelcaseKeys({$foo_bar: true}));
expectType<{__fooBar: boolean}>(camelcaseKeys({__foo_bar: true}));
expectType<{$$fooBar: boolean}>(camelcaseKeys({$$foo_bar: true}));
expectType<{$_fooBar: boolean}>(camelcaseKeys({$_foo_bar: true}));
expectType<{_$fooBar: boolean}>(camelcaseKeys({_$foo_bar: true}));

// With pascalCase
expectType<{_FooBar: boolean}>(camelcaseKeys({_foo_bar: true}, {pascalCase: true}));
expectType<{$FooBar: boolean}>(camelcaseKeys({$foo_bar: true}, {pascalCase: true}));

// With deep option
expectType<{readonly _outer: {readonly $inner: true}}>(camelcaseKeys({_outer: {$inner: true}} as const, {deep: true}));

// Edge cases: keys that are only prefix characters
expectType<{readonly _: true}>(camelcaseKeys({_: true} as const));
expectType<{readonly __: true}>(camelcaseKeys({__: true} as const));
expectType<{readonly $: true}>(camelcaseKeys({$: true} as const));
expectType<{readonly $$$: true}>(camelcaseKeys({$$$: true} as const));

// Test number handling
// NOTE: Using splitOnNumbers: false fixes a1b_text but breaks foo2bar
// Runtime (camelcase):     a1b_text → a1bText ✓, foo2bar → foo2Bar
// Types (splitOn: false):  a1b_text → a1bText ✓, foo2bar → foo2bar ✗
// We prioritize keys with underscores as they're more common
// See https://github.com/sindresorhus/camelcase-keys/issues/124
expectType<{a1bText: string}>(camelcaseKeys({a1b_text: ''}));
// Known limitation: foo2bar (no underscores) types don't match runtime
// expectType<{foo2Bar: boolean}>(camelcaseKeys({foo2bar: true}));
expectType<{readonly version2: true}>(camelcaseKeys({version2: true} as const));
expectType<{version10Foo: boolean}>(camelcaseKeys({version10_foo: true}));

// With pascalCase
expectType<{A1bText: string}>(camelcaseKeys({a1b_text: ''}, {pascalCase: true}));
// ExpectType<{Foo2Bar: boolean}>(camelcaseKeys({foo2bar: true}, {pascalCase: true}));
