import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Path } from '@boost/common';
import { mockNormalizedFilePath } from '@boost/common/test';
import { Artifact } from '../src/Artifact';
import { nodeFileSystem } from '../src/FileSystem';
import { Package } from '../src/Package';
import type { Build, ConfigFile, Platform, Support } from '../src/types';
import { createStubbedFileSystem, getFixturePath, loadPackageAtPath } from './helpers';

vi.mock('rollup', () => ({ rollup: vi.fn() }));

describe('Package', () => {
	const fixturePath = getFixturePath('project');
	let pkg: Package;

	function createArtifacts() {
		const a = new Artifact(pkg, []);
		const b = new Artifact(pkg, []);
		const c = new Artifact(pkg, []);

		return [a, b, c];
	}

	function createCodeArtifact(
		builds: Build[],
		platform: Platform = 'node',
		support: Support = 'stable',
	) {
		const artifact = new Artifact(pkg, builds);
		artifact.platform = platform;
		artifact.support = support;
		artifact.inputs = {
			index: 'src/index.ts',
		};

		artifact.build = () => Promise.resolve();

		return artifact;
	}

	beforeEach(() => {
		pkg = loadPackageAtPath(fixturePath, null, createStubbedFileSystem());
	});

	it('sets properties on instantiation', () => {
		expect(pkg.path).toEqual(mockNormalizedFilePath(fixturePath));
		expect(pkg.jsonPath).toEqual(new Path(fixturePath, 'package.json'));
		expect(pkg.json).toEqual(
			expect.objectContaining({
				name: 'project',
			}),
		);
	});

	it('supports packages without a `packemon` block', () => {
		expect(() => {
			pkg = new Package(new Path(getFixturePath('project')), {
				name: 'test',
				version: '0.0.0',
				description: 'Test',
				keywords: ['test'],
			});
		}).not.toThrow();
	});

	describe('build()', () => {
		let config: ConfigFile;

		beforeEach(() => {
			config = {};
		});

		it('calls `build` on each artifact', async () => {
			const [a, b, c] = createArtifacts();
			const aSpy = vi.spyOn(a, 'build').mockImplementation(() => Promise.resolve());
			const bSpy = vi.spyOn(b, 'build').mockImplementation(() => Promise.resolve());
			const cSpy = vi.spyOn(c, 'build').mockImplementation(() => Promise.resolve());

			pkg.artifacts.push(a, b, c);

			await pkg.build({ concurrency: 1 }, config);

			expect(aSpy).toHaveBeenCalledWith({ concurrency: 1 }, expect.any(Object), expect.any(Object));
			expect(bSpy).toHaveBeenCalledWith({ concurrency: 1 }, expect.any(Object), expect.any(Object));
			expect(cSpy).toHaveBeenCalledWith({ concurrency: 1 }, expect.any(Object), expect.any(Object));
		});

		it('sets passed state and result time', async () => {
			const artifact = new Artifact(pkg, []);

			pkg.artifacts.push(artifact);

			expect(artifact.state).toBe('pending');
			expect(artifact.buildResult.time).toBe(0);

			await pkg.build({}, config);

			expect(artifact.state).toBe('passed');
			// not working on windows?
			// expect(artifact.buildResult.time).not.toBe(0);
		});

		it('sets failed state and result time on error', async () => {
			const artifact = new Artifact(pkg, []);

			vi.spyOn(artifact, 'build').mockImplementation(() => {
				throw new Error('Whoops');
			});

			pkg.artifacts.push(artifact);

			expect(artifact.state).toBe('pending');

			try {
				await pkg.build({}, config);
			} catch (error: unknown) {
				expect(error).toEqual(new Error('Whoops'));
			}

			expect(artifact.state).toBe('failed');
		});

		it('syncs `package.json` when done building', async () => {
			const artifact = new Artifact(pkg, []);
			const spy = vi.spyOn(pkg, 'syncJson');

			pkg.artifacts.push(artifact);

			await pkg.build({ addEntries: true }, config);

			expect(spy).toHaveBeenCalled();
		});

		describe('stamp', () => {
			it('does nothing if `stamp` is false', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }], 'browser'));

				await pkg.build({ stamp: false }, config);

				expect(pkg.json.release).toBeUndefined();
			});

			it('adds if `stamp` is true', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }], 'browser'));

				await pkg.build({ stamp: true }, config);

				expect(pkg.json.release).toBeDefined();
			});
		});

		describe('engines', () => {
			it('does nothing if `addEngines` is false', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }], 'browser'));

				await pkg.build({ addEngines: false }, config);

				expect(pkg.json.engines).toBeUndefined();
			});

			it('does nothing if builds is not `node`', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }], 'browser'));

				await pkg.build({ addEngines: true }, config);

				expect(pkg.json.engines).toBeUndefined();
			});

			it('adds node engines for `node` build', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }]));

				await pkg.build({ addEngines: true }, config);

				expect(pkg.json.engines).toEqual({ node: '>=18.12.0' });
			});

			it('uses oldest `node` build', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }]));

				const old = createCodeArtifact([{ format: 'lib' }]);
				old.support = 'legacy';
				pkg.artifacts.push(old);

				await pkg.build({ addEngines: true }, config);

				expect(pkg.json.engines).toEqual({ node: '>=16.12.0' });
			});

			it('merges with existing engines', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }]));

				pkg.json.engines = {
					packemon: '*',
				};

				expect(pkg.json.engines).toEqual({ packemon: '*' });

				await pkg.build({ addEngines: true }, config);

				expect(pkg.json.engines).toEqual({
					packemon: '*',
					node: '>=18.12.0',
				});
			});
		});

		describe('entries', () => {
			describe('main', () => {
				it('adds "main" for node `lib` format', async () => {
					pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }]));

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/index.js',
						}),
					);
				});

				it('adds "main" for node `cjs` format', async () => {
					pkg.artifacts.push(createCodeArtifact([{ format: 'cjs' }]));

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './cjs/index.cjs',
						}),
					);
				});

				it('adds "main" for node `mjs` format', async () => {
					pkg.artifacts.push(createCodeArtifact([{ format: 'mjs' }]));

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './mjs/index.mjs',
						}),
					);
				});

				it('adds "main" for browser `lib` format', async () => {
					const a = createCodeArtifact([{ format: 'lib' }], 'browser');
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/index.js',
						}),
					);
				});

				it('adds "main" for browser `esm` format', async () => {
					pkg.artifacts.push(createCodeArtifact([{ format: 'esm' }], 'browser'));

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './esm/index.js',
						}),
					);
				});

				it('adds "main" for native `lib` format', async () => {
					const a = createCodeArtifact([{ format: 'lib' }], 'native');
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/index.js',
						}),
					);
				});

				it('adds "main" if output name is not "index"', async () => {
					const a = createCodeArtifact([{ format: 'lib' }]);
					a.inputs = { server: 'src/index.ts' };
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json.main).toBe('./lib/server.js');
				});

				it('adds "main" when using shared `lib` format', async () => {
					const a = createCodeArtifact([{ format: 'lib' }]);
					a.sharedLib = true;
					pkg.artifacts.push(a);

					const b = createCodeArtifact([{ format: 'lib' }], 'browser');
					b.sharedLib = true;
					pkg.artifacts.push(b);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/node/index.js',
						}),
					);
				});

				it('node "main" always takes precedence when multiple `lib` formats', async () => {
					const b = createCodeArtifact([{ format: 'lib' }], 'browser');
					b.sharedLib = true;
					pkg.artifacts.push(b);

					const a = createCodeArtifact([{ format: 'lib' }]);
					a.sharedLib = true;
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/node/index.js',
						}),
					);
				});
			});

			describe('module', () => {
				it('adds "module" for browser `esm` format', async () => {
					const a = createCodeArtifact([{ format: 'esm' }], 'browser');
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							module: './esm/index.js',
						}),
					);
				});
			});

			describe('browser', () => {
				beforeEach(() => {
					const node = createCodeArtifact([{ format: 'lib' }]);
					node.sharedLib = true;

					const browser = createCodeArtifact([{ format: 'lib' }], 'browser');
					browser.sharedLib = true;

					pkg.artifacts.push(node, browser);
				});

				it('adds "browser" when browser and node are sharing a lib', async () => {
					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/node/index.js',
							browser: './lib/browser/index.js',
						}),
					);
				});

				it('adds "browser" for umd builds', async () => {
					pkg.artifacts[1] = createCodeArtifact([{ format: 'umd' }], 'browser');

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/node/index.js',
							browser: './umd/index.js',
						}),
					);
				});

				it('doesnt override "browser" field if its an object', async () => {
					// @ts-expect-error Types are wrong
					pkg.json.browser = { module: 'foo' };

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							main: './lib/node/index.js',
							browser: { module: 'foo' },
						}),
					);
				});
			});

			describe('types', () => {
				it('adds "types" when a types artifact exists', async () => {
					pkg.artifacts.push(createCodeArtifact([{ declaration: true, format: 'lib' }]));

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							types: './lib/index.d.ts',
						}),
					);
				});
			});

			describe('bin', () => {
				it('adds "bin" for node `lib` format', async () => {
					const a = createCodeArtifact([{ format: 'lib' }]);
					a.inputs = { bin: 'src/bin.ts' };
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							bin: './lib/bin.js',
						}),
					);
				});

				it('adds "bin" for node `cjs` format', async () => {
					const a = createCodeArtifact([{ format: 'cjs' }]);
					a.inputs = { bin: 'src/bin.ts' };
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							bin: './cjs/bin.cjs',
						}),
					);
				});

				it('adds "bin" for node `mjs` format', async () => {
					const a = createCodeArtifact([{ format: 'mjs' }]);
					a.inputs = { bin: 'src/bin.ts' };
					pkg.artifacts.push(a);

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json).toEqual(
						expect.objectContaining({
							bin: './mjs/bin.mjs',
						}),
					);
				});

				it('doesnt add "bin" if value is an object', async () => {
					const a = createCodeArtifact([{ format: 'lib' }]);
					a.inputs = { bin: 'src/bin.ts' };
					pkg.artifacts.push(a);

					pkg.json.bin = {};

					await pkg.build({ addEntries: true }, config);

					expect(pkg.json.bin).toEqual({});
				});
			});
		});

		describe('exports', () => {
			it('does nothing if no builds', async () => {
				pkg.artifacts.push(new Artifact(pkg, []));

				await pkg.build({}, config);

				expect(pkg.json.exports).toBeUndefined();
			});

			it('does nothing if `addExports` is false', async () => {
				pkg.artifacts.push(new Artifact(pkg, []));

				await pkg.build({ addExports: false }, config);

				expect(pkg.json.exports).toBeUndefined();
			});

			it('adds exports for a single artifact and format', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }]));

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for a single artifact and multiple format', async () => {
				pkg.artifacts.push(
					createCodeArtifact([{ format: 'lib' }, { format: 'mjs' }, { format: 'cjs' }]),
				);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for multiple artifacts of the same output name', async () => {
				const a = createCodeArtifact([{ format: 'lib' }]);
				a.sharedLib = true;
				pkg.artifacts.push(a);

				const b = createCodeArtifact([{ format: 'lib' }], 'browser');
				b.sharedLib = true;
				pkg.artifacts.push(b);

				const c = createCodeArtifact([{ format: 'lib' }], 'native');
				c.sharedLib = true;
				pkg.artifacts.push(c);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for multiple artifacts + formats of the same output name', async () => {
				const a = createCodeArtifact([{ format: 'lib' }, { format: 'mjs' }, { format: 'cjs' }]);
				pkg.artifacts.push(a);

				const b = createCodeArtifact(
					[{ format: 'lib' }, { format: 'esm' }, { format: 'umd' }],
					'browser',
				);
				pkg.artifacts.push(b);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for multiple artifacts of different output names', async () => {
				const a = createCodeArtifact([{ format: 'lib' }]);
				pkg.artifacts.push(a);

				const b = createCodeArtifact([{ format: 'lib' }], 'browser');
				b.inputs = { client: 'src/index.ts' };
				pkg.artifacts.push(b);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for multiple artifacts + formats of different output names', async () => {
				const a = createCodeArtifact([{ format: 'lib' }, { format: 'mjs' }, { format: 'cjs' }]);
				pkg.artifacts.push(a);

				const b = createCodeArtifact(
					[{ format: 'lib' }, { format: 'esm' }, { format: 'umd' }],
					'browser',
				);
				b.inputs = { client: 'src/index.ts' };
				pkg.artifacts.push(b);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for multiple artifacts with different bundle types', async () => {
				const a = createCodeArtifact([{ format: 'esm', declaration: true }], 'browser');
				a.inputs.utils = 'src/utils/index.ts';
				a.bundle = true;
				pkg.artifacts.push(a);

				const b = createCodeArtifact([{ format: 'lib', declaration: true }], 'node');
				b.inputs.utils = 'src/utils/index.ts';
				b.bundle = false;
				pkg.artifacts.push(b);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds exports for bundle and types artifacts in parallel', async () => {
				pkg.artifacts.push(createCodeArtifact([{ declaration: true, format: 'lib' }]));

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('merges with existing exports', async () => {
				pkg.json.exports = {
					'./foo': './lib/foo.js',
				};

				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }]));

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('adds "mjs wrapper" exports for a single cjs format', async () => {
				pkg.artifacts.push(createCodeArtifact([{ format: 'cjs' }]));

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});

			it('supports dual cjs/mjs exports', async () => {
				pkg.artifacts.push(
					createCodeArtifact([{ format: 'cjs' }]),
					createCodeArtifact([{ format: 'mjs' }], 'node', 'experimental'),
				);

				await pkg.build({ addExports: true }, config);

				expect(pkg.json.exports).toMatchSnapshot();
			});
		});

		describe('files', () => {
			it('adds "files" folder for each format format', async () => {
				pkg.artifacts.push(
					createCodeArtifact([{ format: 'cjs' }, { format: 'lib' }]),
					createCodeArtifact([{ format: 'umd' }], 'browser'),
				);

				await pkg.build({ addFiles: true }, config);

				expect(pkg.json).toEqual(
					expect.objectContaining({
						files: ['cjs/**/*', 'lib/**/*', 'src/**/*', 'umd/**/*'],
					}),
				);
			});

			it('merges with existing "files" list', async () => {
				pkg.json.files = ['templates/', 'test.js'];

				pkg.artifacts.push(createCodeArtifact([{ format: 'lib' }, { format: 'esm' }], 'browser'));

				await pkg.build({ addFiles: true }, config);

				expect(pkg.json).toEqual(
					expect.objectContaining({
						files: ['esm/**/*', 'lib/**/*', 'src/**/*', 'templates/', 'test.js'],
					}),
				);
			});

			it('includes assets folder if it exists', async () => {
				pkg = loadPackageAtPath(getFixturePath('project-assets'), null, createStubbedFileSystem());

				try {
					nodeFileSystem.createDirAll(pkg.path.append('assets').path());
				} catch {
					// Ignore
				}

				await pkg.build({ addFiles: true }, config);

				expect(pkg.json).toEqual(
					expect.objectContaining({
						files: ['assets/**/*'],
					}),
				);
			});
		});

		// https://github.com/milesj/packemon/issues/42#issuecomment-808793241
		it('private api: uses inputs as subpath imports', async () => {
			const a = createCodeArtifact([{ format: 'cjs', declaration: true }]);
			a.api = 'private';
			a.inputs = { index: 'src/node.ts' };

			const b = createCodeArtifact([{ format: 'lib', declaration: true }]);
			b.api = 'private';
			b.inputs = { bin: 'src/cli.ts' };

			const c = createCodeArtifact(
				[
					{ format: 'lib', declaration: true },
					{ format: 'esm', declaration: true },
				],
				'browser',
				'current',
			);
			c.api = 'private';
			c.inputs = { web: 'src/web.ts' };

			const d = createCodeArtifact([{ format: 'mjs', declaration: true }], 'node', 'current');
			d.api = 'private';
			d.inputs = { import: 'src/web.ts' };

			pkg.artifacts.push(a, b, c, d);

			await pkg.build({ addExports: true, addEntries: true }, config);

			expect(pkg.json).toEqual(
				expect.objectContaining({
					main: './cjs/index.cjs',
					bin: './lib/bin.js',
				}),
			);
			expect(pkg.json.exports).toMatchSnapshot();
		});

		it('public api + bundle: uses inputs as subpath imports (non-deep imports)', async () => {
			const a = createCodeArtifact([{ format: 'cjs', declaration: true }]);
			a.api = 'public';
			a.bundle = true;
			a.inputs = { index: 'src/node.ts' };

			const b = createCodeArtifact([{ format: 'lib', declaration: true }]);
			b.api = 'public';
			b.bundle = true;
			b.inputs = { bin: 'src/cli.ts' };

			const c = createCodeArtifact(
				[
					{ format: 'lib', declaration: true },
					{ format: 'esm', declaration: true },
				],
				'browser',
				'current',
			);
			c.api = 'public';
			c.bundle = true;
			c.inputs = { web: 'src/web.ts' };

			const d = createCodeArtifact([{ format: 'mjs', declaration: true }], 'node', 'current');
			d.api = 'public';
			d.bundle = true;
			d.inputs = { import: 'src/web.ts' };

			pkg.artifacts.push(a, b, c, d);

			await pkg.build({ addExports: true, addEntries: true }, config);

			expect(pkg.json).toEqual(
				expect.objectContaining({
					main: './cjs/node.cjs',
					bin: './lib/cli.js',
				}),
			);
			expect(pkg.json.exports).toMatchSnapshot();
		});

		it('public api + no bundle: uses patterns as subpath imports (deep imports)', async () => {
			const a = createCodeArtifact([{ format: 'cjs', declaration: true }]);
			a.api = 'public';
			a.bundle = false;
			a.inputs = { index: 'src/node.ts' };

			const b = createCodeArtifact([{ format: 'lib', declaration: true }]);
			b.api = 'public';
			b.bundle = false;
			b.inputs = { bin: 'src/cli.ts' };

			const c = createCodeArtifact(
				[
					{ format: 'lib', declaration: true },
					{ format: 'esm', declaration: true },
				],
				'browser',
				'current',
			);
			c.api = 'public';
			c.bundle = false;
			c.inputs = { web: 'src/web.ts' };

			const d = createCodeArtifact([{ format: 'mjs', declaration: true }], 'node', 'current');
			d.api = 'public';
			d.bundle = false;
			d.inputs = { import: 'src/web.ts' };

			pkg.artifacts.push(a, b, c, d);

			await pkg.build({ addExports: true, addEntries: true }, config);

			expect(pkg.json).toEqual(
				expect.objectContaining({
					main: './cjs/node.cjs',
					module: './esm/web.js',
					bin: './lib/cli.js',
				}),
			);
			expect(pkg.json.exports).toMatchSnapshot();
		});
	});

	describe('cleanup()', () => {
		it('calls `cleanup` on each artifact', async () => {
			const [a, b, c] = createArtifacts();
			const aSpy = vi.spyOn(a, 'clean');
			const bSpy = vi.spyOn(b, 'clean');
			const cSpy = vi.spyOn(c, 'clean');

			pkg.artifacts.push(a, b, c);

			await pkg.clean();

			expect(aSpy).toHaveBeenCalled();
			expect(bSpy).toHaveBeenCalled();
			expect(cSpy).toHaveBeenCalled();
		});
	});

	describe('getName()', () => {
		it('returns `name` from `package.json`', () => {
			expect(pkg.getName()).toBe('project');
		});
	});

	describe('getSlug()', () => {
		it('returns package folder name', () => {
			expect(pkg.getSlug()).toBe('project');
		});
	});

	describe('getFeatureFlags()', () => {
		describe('react', () => {
			it('returns "classic" if a * dependency', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/react-star'),
					).getFeatureFlags(),
				).toEqual({
					react: 'classic',
				});
			});

			it('returns "classic" if a non-satisfying dependency', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/react-classic'),
					).getFeatureFlags(),
				).toEqual({
					react: 'classic',
				});
			});

			it('returns "automatic" if a satisfying dependency', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/react-automatic'),
					).getFeatureFlags(),
				).toEqual({
					react: 'automatic',
				});
			});
		});

		describe('solid', () => {
			it('enables if a dependency', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/solid'),
					).getFeatureFlags(),
				).toEqual({
					solid: true,
				});
			});
		});

		describe('typescript', () => {
			it('returns true if a package dependency (peer)', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/ts'),
					).getFeatureFlags(),
				).toEqual({
					decorators: false,
					strict: false,
					typescript: true,
					typescriptComposite: false,
				});
			});

			it('returns true if package contains a `tsconfig.json`', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/ts-config'),
					).getFeatureFlags(),
				).toEqual({
					decorators: true,
					strict: true,
					typescript: true,
					typescriptComposite: false,
				});
			});

			it('extracts decorators and strict support from local `tsconfig.json`', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/ts-config'),
					).getFeatureFlags(),
				).toEqual({
					decorators: true,
					strict: true,
					typescript: true,
					typescriptComposite: false,
				});
			});

			it('handles composite/project references', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/ts-refs'),
					).getFeatureFlags(),
				).toEqual({
					decorators: false,
					strict: false,
					typescript: true,
					typescriptComposite: true,
				});
			});
		});

		describe('flow', () => {
			it('returns true if a package dependency (dev)', () => {
				expect(
					loadPackageAtPath(
						getFixturePath('workspaces-feature-flags', 'packages/flow'),
					).getFeatureFlags(),
				).toEqual({ flow: true });
			});
		});
	});

	describe('generateArtifacts()', () => {
		it('generates build artifacts for each config in a package', () => {
			const fs = createStubbedFileSystem();
			const pkg1 = loadPackageAtPath(
				getFixturePath('workspaces', 'packages/valid-array'),
				null,
				fs,
			);
			const pkg2 = loadPackageAtPath(
				getFixturePath('workspaces', 'packages/valid-object'),
				null,
				fs,
			);
			const pkg3 = loadPackageAtPath(
				getFixturePath('workspaces', 'packages/valid-object-private'),
				null,
				fs,
			);

			pkg1.generateArtifacts({});
			pkg2.generateArtifacts({});
			pkg3.generateArtifacts({});

			expect(pkg1.artifacts).toHaveLength(2);
			expect(pkg1.artifacts[0].builds).toEqual([{ format: 'lib' }]);

			expect(pkg1.artifacts[1].inputs).toEqual({ index: 'src/index.ts' });
			expect(pkg1.artifacts[1].builds).toEqual([{ format: 'esm' }]);

			expect(pkg2.artifacts).toHaveLength(1);
			expect(pkg2.artifacts[0].builds).toEqual([{ format: 'mjs' }]);

			expect(pkg3.artifacts).toHaveLength(1);
			expect(pkg3.artifacts[0].builds).toEqual([{ format: 'esm' }, { format: 'umd' }]);
		});

		it('generates type artifacts for each config in a package', () => {
			const fs = createStubbedFileSystem();
			const pkg1 = loadPackageAtPath(
				getFixturePath('workspaces', 'packages/valid-array'),
				null,
				fs,
			);
			const pkg2 = loadPackageAtPath(
				getFixturePath('workspaces', 'packages/valid-object'),
				null,
				fs,
			);
			const pkg3 = loadPackageAtPath(
				getFixturePath('workspaces', 'packages/valid-object-private'),
				null,
				fs,
			);

			pkg1.generateArtifacts({ declaration: true });
			pkg2.generateArtifacts({ declaration: true });
			pkg3.generateArtifacts({ declaration: true });

			expect(pkg1.artifacts).toHaveLength(2);
			expect(pkg1.artifacts[0].builds).toEqual([{ declaration: true, format: 'lib' }]);
			expect(pkg1.artifacts[1].builds).toEqual([{ declaration: true, format: 'esm' }]);

			expect(pkg2.artifacts).toHaveLength(1);
			expect(pkg2.artifacts[0].builds).toEqual([{ declaration: true, format: 'mjs' }]);

			expect(pkg3.artifacts).toHaveLength(1);
			expect(pkg3.artifacts[0].builds).toEqual([
				{ declaration: true, format: 'esm' },
				{ declaration: true, format: 'umd' },
			]);
		});

		it('generates build artifacts for projects with multiple platforms', () => {
			pkg = loadPackageAtPath(
				getFixturePath('project-multi-platform'),
				null,
				createStubbedFileSystem(),
			);

			pkg.generateArtifacts({});

			expect(pkg.artifacts[0].builds).toEqual([{ format: 'esm' }]);
			expect(pkg.artifacts[0].platform).toBe('browser');

			expect(pkg.artifacts[1].builds).toEqual([{ format: 'mjs' }]);
			expect(pkg.artifacts[1].platform).toBe('node');
		});

		it('filters formats using `filterFormats`', () => {
			pkg = loadPackageAtPath(
				getFixturePath('project-multi-platform'),
				null,
				createStubbedFileSystem(),
			);

			pkg.generateArtifacts({
				filterFormats: 'esm',
			});

			expect(pkg.artifacts[0].builds).toEqual([{ format: 'esm' }]);
			expect(pkg.artifacts[1]).toBeUndefined();
		});

		it('filters platforms using `filterPlatforms`', () => {
			pkg = loadPackageAtPath(
				getFixturePath('project-multi-platform'),
				null,
				createStubbedFileSystem(),
			);

			pkg.generateArtifacts({
				filterPlatforms: 'node',
			});

			expect(pkg.artifacts[0].builds).toEqual([{ format: 'mjs' }]);
			expect(pkg.artifacts[1]).toBeUndefined();
		});
	});

	describe('setConfigs()', () => {
		const COMMON_FEATURES = {
			cjsTypesCompat: false,
			helpers: 'bundled',
			swc: false,
		};

		beforeEach(() => {
			pkg = loadPackageAtPath(
				getFixturePath('workspaces-feature-flags', 'packages/common'),
				null,
				createStubbedFileSystem(),
			);
			// @ts-expect-error Allow override
			pkg.configs = [];
		});

		it('sets default formats for `browser` platform', () => {
			pkg.setConfigs([
				{
					// @ts-expect-error Allow empty
					format: '',
					inputs: {},
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: {},
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('adds `umd` default format when `namespace` is provided for `browser` platform', () => {
			pkg.setConfigs([
				{
					// @ts-expect-error Allow empty
					format: '',
					inputs: {},
					platform: 'browser',
					namespace: 'test',
					support: 'stable',
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['esm', 'umd'],
					inputs: {},
					platform: 'browser',
					namespace: 'test',
					support: 'stable',
				},
			]);
		});

		it('sets default formats for `native` platform', () => {
			pkg.setConfigs([
				{
					// @ts-expect-error Allow empty
					format: '',
					inputs: {},
					platform: 'native',
					namespace: '',
					support: 'stable',
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: {},
					platform: 'native',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('sets default formats for `node` platform', () => {
			pkg.setConfigs([
				{
					// @ts-expect-error Allow empty
					format: '',
					inputs: {},
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'public',
					bundle: false,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['mjs'],
					inputs: {},
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('doesnt set default formats when supplied manually', () => {
			pkg.setConfigs([
				{
					format: 'mjs',
					inputs: {},
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'public',
					bundle: false,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['mjs'],
					inputs: {},
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('sets default formats for multiple platforms', () => {
			pkg.setConfigs([
				{
					inputs: {},
					platform: ['browser', 'node'],
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: {},
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
				{
					api: 'public',
					bundle: false,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['mjs'],
					inputs: {},
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('sets multiple formats', () => {
			pkg.setConfigs([
				{
					inputs: {},
					format: ['lib', 'esm'],
				},
			]);

			expect(pkg.configs[0].formats).toEqual(['lib', 'esm']);
		});

		it('filters and divides formats for multiple platforms', () => {
			pkg.setConfigs([
				{
					format: 'esm',
					inputs: {},
					platform: ['browser', 'node', 'native'],
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: {},
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
				{
					api: 'public',
					bundle: false,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['mjs'],
					inputs: {},
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: {},
					platform: 'native',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('can override `bundle` defaults', () => {
			pkg.setConfigs([
				{
					api: 'private',
					bundle: true,
					platform: 'node',
				},
				{
					api: 'public',
					bundle: false,
					platform: 'browser',
					features: {
						helpers: 'runtime',
					},
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: [],
					features: COMMON_FEATURES,
					formats: ['mjs'],
					inputs: { index: 'src/index.ts' },
					platform: 'node',
					namespace: '',
					support: 'stable',
				},
				{
					api: 'public',
					bundle: false,
					externals: [],
					features: {
						...COMMON_FEATURES,
						helpers: 'runtime',
					},
					formats: ['esm'],
					inputs: { index: 'src/index.ts' },
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('sets `externals` option', () => {
			pkg.setConfigs([
				{
					externals: ['foo', 'bar'],
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: ['foo', 'bar'],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: { index: 'src/index.ts' },
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('sets `externals` option with a string', () => {
			pkg.setConfigs([
				{
					externals: 'foo',
				},
			]);

			expect(pkg.configs).toEqual([
				{
					api: 'private',
					bundle: true,
					externals: ['foo'],
					features: COMMON_FEATURES,
					formats: ['esm'],
					inputs: { index: 'src/index.ts' },
					platform: 'browser',
					namespace: '',
					support: 'stable',
				},
			]);
		});

		it('errors if invalid format is provided for `browser` platform', () => {
			expect(() => {
				pkg.setConfigs([
					{
						format: 'mjs',
						platform: 'browser',
					},
				]);
			}).toThrowErrorMatchingSnapshot();
		});

		it('errors if invalid format is provided for `native` platform', () => {
			expect(() => {
				pkg.setConfigs([
					{
						format: 'mjs',
						platform: 'native',
					},
				]);
			}).toThrowErrorMatchingSnapshot();
		});

		it('errors if invalid format is provided for `node` platform', () => {
			expect(() => {
				pkg.setConfigs([
					{
						format: 'umd',
						platform: 'node',
					},
				]);
			}).toThrowErrorMatchingSnapshot();
		});

		it('errors if input name contains a slash', () => {
			expect(() => {
				pkg.setConfigs([
					{
						inputs: {
							'foo/bar': 'src/foo.ts',
						},
					},
				]);
			}).toThrowErrorMatchingSnapshot();
		});

		it('errors if input name contains a space', () => {
			expect(() => {
				pkg.setConfigs([
					{
						inputs: {
							'foo bar': 'src/foo.ts',
						},
					},
				]);
			}).toThrowErrorMatchingSnapshot();
		});
	});

	describe('syncJson()', () => {
		it('writes to `package.json`', () => {
			const spy = vi.spyOn(pkg.fs, 'writeJson').mockImplementation(() => {});

			pkg.syncJson();

			expect(spy).toHaveBeenCalledWith(pkg.jsonPath.path(), {
				name: 'project',
				packemon: {
					inputs: {
						index: 'src/index.ts',
						test: 'src/sub/test.ts',
					},
				},
			});
		});
	});
});
