import { testSuite, expect } from 'manten';
import { createFixture } from 'fs-fixture';
import { createTsconfigJson } from '../../utils/fixture-helpers.js';
import { getTscTsconfig } from '../../utils/typescript-helpers.js';
import { parseTsconfig } from '#get-tsconfig';

export default testSuite(({ describe }) => {
	describe('parses tsconfig', ({ describe, test }) => {
		describe('errors', ({ test }) => {
			test('non-existent path', async () => {
				expect(
					() => parseTsconfig('non-existent-path'),
				).toThrow('Cannot resolve tsconfig at path: ');
			});

			test('empty file', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': '',
				});

				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('json invalid', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': 'asdf',
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				expect(parsedTsconfig).toStrictEqual({
					compilerOptions: {},
				});
			});

			test('json non-object', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': '"asdf"',
				});

				expect(
					() => parseTsconfig(fixture.getPath('tsconfig.json')),
				).toThrow('Failed to parse tsconfig at');
			});

			test('json empty', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': '{}',
				});

				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		test('parses a path', async () => {
			await using fixture = await createFixture({
				'file.ts': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						moduleResolution: 'node10',
						isolatedModules: true,
						module: 'esnext',
						esModuleInterop: true,
						declaration: true,
						outDir: 'dist',
						declarationDir: 'dist-declaration',
						strict: true,
						target: 'esnext',
						rootDir: 'root-dir',
					},
				}),
			});

			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			// TODO: TS 5.5 resolve excludes paths
			if (expectedTsconfig.exclude) {
				expectedTsconfig.exclude = expectedTsconfig.exclude.map(excludePath => excludePath.split('/').pop()!);
			}

			expect(expectedTsconfig).toStrictEqual(parsedTsconfig);
		});

		test('implicit config', async () => {
			await using fixture = await createFixture({
				'file.ts': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						target: 'es2022',
						module: 'preserve',
					},
				}),
			});

			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			expect(expectedTsconfig).toStrictEqual(parsedTsconfig);
		});

		describe('baseUrl', ({ test }) => {
			test('relative path', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							baseUrl: '.',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));

				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('absolute path', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							baseUrl: process.platform === 'win32' ? 'C:\\' : '/',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));

				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('rewriteRelativeImportExtensions', ({ test }) => {
			test('sets allowImportingTsExtensions implicitly', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							rewriteRelativeImportExtensions: true,
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('respects explicit allowImportingTsExtensions', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							rewriteRelativeImportExtensions: true,
							allowImportingTsExtensions: false,
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('does not set allowImportingTsExtensions when false', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							rewriteRelativeImportExtensions: false,
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('composite', ({ test }) => {
			test('implies declaration and incremental', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							composite: true,
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('lib', ({ test }) => {
			test('normalizes to lowercase', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							lib: ['ES2020', 'DOM'],
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('nodenext', ({ test }) => {
			test('implies resolveJsonModule', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							module: 'nodenext',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('useDefineForClassFields', ({ test }) => {
			test('not implied when target is below ES2022', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							target: 'es5',
							module: 'node16',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('module node18 and node20', ({ test }) => {
			test('module: node18 implications', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							module: 'node18',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('module: node20 implications', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							module: 'node20',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('checkJs', ({ test }) => {
			test('implies allowJs', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							checkJs: true,
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});
		});

		describe('exclude', ({ test }) => {
			test('does not add outDir when exclude is explicit', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							outDir: 'dist',
						},
						exclude: ['node_modules'],
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('does not add outDir when exclude is empty array', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							outDir: 'dist',
						},
						exclude: [],
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('does not add outDir when exclude is inherited', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'base.json': createTsconfigJson({
						compilerOptions: {
							outDir: 'dist',
						},
						exclude: ['node_modules'],
					}),
					'tsconfig.json': createTsconfigJson({
						extends: './base.json',
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				const expectedTsconfig = await getTscTsconfig(fixture.path);
				delete expectedTsconfig.files;

				expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
			});

			test('auto-adds outDir when exclude is not specified', async () => {
				await using fixture = await createFixture({
					'file.ts': '',
					'tsconfig.json': createTsconfigJson({
						compilerOptions: {
							outDir: 'dist',
						},
					}),
				});

				const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
				expect(parsedTsconfig.exclude).toStrictEqual(['dist']);
			});
		});

		test('cache', async () => {
			await using fixture = await createFixture({
				'file.ts': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						baseUrl: '.',
						moduleResolution: 'node10',
						isolatedModules: true,
						module: 'esnext',
						esModuleInterop: true,
						declaration: true,
						outDir: 'dist',
						strict: true,
						target: 'esnext',
					},
				}),
			});

			const cache = new Map();
			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'), cache);
			expect(cache.size).toBe(1);

			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			// TODO: TS 5.5 resolve excludes paths
			if (expectedTsconfig.exclude) {
				expectedTsconfig.exclude = expectedTsconfig.exclude.map(excludePath => excludePath.split('/').pop()!);
			}

			expect(expectedTsconfig).toStrictEqual(parsedTsconfig);

			const parsedTsconfigCached = parseTsconfig(fixture.getPath('tsconfig.json'), cache);
			expect(cache.size).toBe(1);

			expect(expectedTsconfig).toStrictEqual(parsedTsconfigCached);
		});
	});

	describe('enum case normalization', ({ test }) => {
		test('normalizes jsx to lowercase', async () => {
			await using fixture = await createFixture({
				'file.tsx': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						// @ts-expect-error testing mixed case input
						jsx: 'React-JSX',
					},
				}),
			});

			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
		});

		test('normalizes moduleDetection to lowercase', async () => {
			await using fixture = await createFixture({
				'file.ts': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						// @ts-expect-error testing mixed case input
						moduleDetection: 'Force',
					},
				}),
			});

			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
		});

		test('normalizes importsNotUsedAsValues to lowercase', async () => {
			await using fixture = await createFixture({
				'file.ts': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						// @ts-expect-error testing mixed case input
						importsNotUsedAsValues: 'Preserve',
					},
				}),
			});

			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
		});

		test('normalizes newLine to lowercase', async () => {
			await using fixture = await createFixture({
				'file.ts': '',
				'tsconfig.json': createTsconfigJson({
					compilerOptions: {
						newLine: 'CRLF',
					},
				}),
			});

			const parsedTsconfig = parseTsconfig(fixture.getPath('tsconfig.json'));
			const expectedTsconfig = await getTscTsconfig(fixture.path);
			delete expectedTsconfig.files;

			expect(parsedTsconfig).toStrictEqual(expectedTsconfig);
		});
	});
});
