import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// Belt-and-braces detector for the no-factory vi.mock anti-pattern named in
// ADR-012 (the PR #657 failure class). A `vi.mock('$app/navigation')` with no
// factory does NOT auto-resolve to an adjacent __mocks__ file the way Jest's
// __mocks__/ does: for SvelteKit virtual modules, vitest substitutes some
// exports (plain function refs like goto) but leaves others bound to the live
// implementation (replaceState, which delegates through a getter). The result
// is a partial mock that crashes when an unsubstituted export is hit.
//
// The sanctioned form keeps an INLINE sync factory:
//   vi.mock('$app/forms', () => ({ enhance(node, submit) { ... } }));
// (Sharing the body via a module imported into the factory is infeasible in
// browser mode — vitest hoists vi.mock above the import; see ADR-012.)
//
// ESLint and the CI grep guard catch the pattern earlier; this in-suite test
// catches it at every vitest invocation — the layer hardest to disable. It
// also forecloses ADR-012's rejected Option C (config-level auto-resolve).
//
// We scan source text rather than parsing AST: fast, no parser dependency,
// good enough for the named anti-pattern. The pattern matches a `vi.mock`
// call whose only argument is a string literal (no factory after a comma).

const NO_FACTORY_VI_MOCK = /vi\.mock\(\s*['"][^'"]+['"]\s*\)/;

export function hasNoFactoryViMock(source: string): boolean {
	return NO_FACTORY_VI_MOCK.test(source);
}

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SRC_ROOT = path.resolve(__dirname, '..');

function findBrowserSpecs(): string[] {
	const entries = readdirSync(SRC_ROOT, { recursive: true, withFileTypes: true });
	return entries
		.filter(
			(e) =>
				e.isFile() && (e.name.endsWith('.svelte.test.ts') || e.name.endsWith('.svelte.spec.ts'))
		)
		.map((e) => path.join(e.parentPath ?? (e as { path: string }).path, e.name));
}

describe('scan: hasNoFactoryViMock', () => {
	it('flags a vi.mock with a string id and no factory', () => {
		expect(hasNoFactoryViMock(`vi.mock('$app/navigation');`)).toBe(true);
	});

	it('flags a no-factory vi.mock written across multiple lines', () => {
		const fixture = `vi.mock(
			'$app/forms'
		);`;
		expect(hasNoFactoryViMock(fixture)).toBe(true);
	});

	it('does not flag a vi.mock with an inline factory', () => {
		expect(hasNoFactoryViMock(`vi.mock('$app/forms', () => ({ enhance: () => () => {} }));`)).toBe(
			false
		);
	});

	it('does not flag a vi.mock with a multi-line inline factory', () => {
		const fixture = `vi.mock('$app/forms', () => ({
			enhance: (node, submit) => ({ destroy() {} })
		}));`;
		expect(hasNoFactoryViMock(fixture)).toBe(false);
	});

	it('does not flag a vi.mock with a named factory reference', () => {
		expect(hasNoFactoryViMock(`vi.mock('$app/state', factory);`)).toBe(false);
	});

	it('does not flag source with no vi.mock at all', () => {
		expect(hasNoFactoryViMock(`const x = vi.fn();`)).toBe(false);
	});
});

describe('browser specs: no no-factory vi.mock of a virtual module', () => {
	it('every src/**/*.svelte.{test,spec}.ts file keeps its factory', () => {
		const specFiles = findBrowserSpecs();
		expect(specFiles.length).toBeGreaterThan(0);
		const offenders = specFiles.filter((file) => hasNoFactoryViMock(readFileSync(file, 'utf-8')));
		expect(offenders).toEqual([]);
	});
});
