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 duplicate-id birpc race named in
// ADR-012 / #553. When the same resolved module URL is mocked via two
// distinct vi.mock id strings (e.g. '$lib/foo.svelte' and
// '$lib/foo.svelte.js'), @vitest/browser-playwright registers two
// Playwright routes against one cleanup slot — the orphan survives, fires
// after the next session's birpc closes, and crashes the run with
// "[birpc] rpc is closed, cannot call resolveManualMock".
//
// Fixed upstream in vitest PR #10267; until that fix reaches a published
// release, normalisation in user-land is the practical guard. This test
// catches the pattern at every vitest invocation — the layer hardest to
// disable or scope around.

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

function extractMockIds(source: string): string[] {
	const ids: string[] = [];
	for (const match of source.matchAll(VI_MOCK_ID)) {
		ids.push(match[1]);
	}
	return ids;
}

function canonicalise(id: string): string {
	if (id.endsWith('.svelte.js')) return id.slice(0, -3);
	if (id.endsWith('.svelte.ts')) return id.slice(0, -3);
	return id;
}

export function findDuplicateMockIds(
	specSources: Record<string, string>
): Map<string, Set<string>> {
	const byCanonical = new Map<string, Set<string>>();
	for (const source of Object.values(specSources)) {
		for (const raw of extractMockIds(source)) {
			const canonical = canonicalise(raw);
			const existing = byCanonical.get(canonical) ?? new Set<string>();
			existing.add(raw);
			byCanonical.set(canonical, existing);
		}
	}
	const duplicates = new Map<string, Set<string>>();
	for (const [canonical, raws] of byCanonical) {
		if (raws.size >= 2) duplicates.set(canonical, raws);
	}
	return duplicates;
}

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: findDuplicateMockIds', () => {
	it('flags two specs mocking the same module under .svelte and .svelte.js', () => {
		const dup = findDuplicateMockIds({
			'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
			'b.spec.ts': `vi.mock('$lib/foo.svelte.js', () => ({}));`
		});
		expect(dup.get('$lib/foo.svelte')).toEqual(new Set(['$lib/foo.svelte', '$lib/foo.svelte.js']));
	});

	it('does not flag two specs both using $lib/foo.svelte', () => {
		const dup = findDuplicateMockIds({
			'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
			'b.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`
		});
		expect(dup.size).toBe(0);
	});

	it('does not flag $app/state and $app/stores (different modules)', () => {
		const dup = findDuplicateMockIds({
			'a.spec.ts': `vi.mock('$app/state', () => ({}));`,
			'b.spec.ts': `vi.mock('$app/stores', () => ({}));`
		});
		expect(dup.size).toBe(0);
	});

	it('does not flag $lib/foo and $lib/bar (different canonical paths)', () => {
		const dup = findDuplicateMockIds({
			'a.spec.ts': `vi.mock('$lib/foo', () => ({}));`,
			'b.spec.ts': `vi.mock('$lib/bar', () => ({}));`
		});
		expect(dup.size).toBe(0);
	});

	it('flags both spellings within a single file', () => {
		const dup = findDuplicateMockIds({
			'a.spec.ts': `
				vi.mock('$lib/foo.svelte', () => ({}));
				vi.mock('$lib/foo.svelte.js', () => ({}));
			`
		});
		expect(dup.get('$lib/foo.svelte')?.size).toBe(2);
	});

	it('canonicalises .svelte.ts the same way as .svelte.js', () => {
		const dup = findDuplicateMockIds({
			'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
			'b.spec.ts': `vi.mock('$lib/foo.svelte.ts', () => ({}));`
		});
		expect(dup.get('$lib/foo.svelte')?.size).toBe(2);
	});
});

describe('browser specs: no duplicate-id vi.mock calls across the suite', () => {
	it('every mocked module is referenced under exactly one id string', () => {
		const specFiles = findBrowserSpecs();
		expect(specFiles.length).toBeGreaterThan(0);
		const sources = Object.fromEntries(
			specFiles.map((file) => [file, readFileSync(file, 'utf-8')])
		);
		const duplicates = findDuplicateMockIds(sources);
		const report = Object.fromEntries([...duplicates].map(([k, v]) => [k, [...v]]));
		expect(report).toEqual({});
	});
});
