// #region ApiModuleTest [C:3] [TYPE Module] [SEMANTICS test, api, fetch, ws-url]
// @BRIEF Unit tests for the core API communication layer — fetch wrappers, WebSocket URL builders,
//   endpoint registry, error normalization, and toast suppression.
// @LAYER Tests
// @RELATION BINDS_TO -> [ApiModule]
// @TEST_CONTRACT: fetchApi -> Returns typed JSON for 2xx, null for 204, throws ApiError on error
// @TEST_CONTRACT: postApi -> Sends POST with JSON body, handles errors
// @TEST_CONTRACT: deleteApi -> Sends DELETE, always notifies on error
// @TEST_CONTRACT: requestApi -> Generic method with suppression heuristics
// @TEST_CONTRACT: wsUrl helpers -> Build correct ws:// URL with auth token
// @TEST_EDGE: network_failure -> fetch throws, error toast dispatched
// @TEST_EDGE: auth_token -> localStorage token injected as Bearer header
// @TEST_EDGE: suppress_toast -> Options.suppressToast suppresses error toast
// @TEST_EDGE: api_methods -> Registry methods call correct endpoints

// #region ApiModuleTest [C:3] [TYPE Module] [SEMANTICS test, api, fetch, ws-url]
// @BRIEF Unit tests for the core API communication layer — fetch wrappers, WebSocket URL builders,
//   endpoint registry, error normalization, and toast suppression.
// @LAYER Tests
// @RELATION BINDS_TO -> [ApiModule]
// @TEST_CONTRACT: fetchApi -> Returns typed JSON for 2xx, null for 204, throws ApiError on error
// @TEST_CONTRACT: postApi -> Sends POST with JSON body, handles errors
// @TEST_CONTRACT: deleteApi -> Sends DELETE, always notifies on error
// @TEST_CONTRACT: requestApi -> Generic method with suppression heuristics
// @TEST_CONTRACT: wsUrl helpers -> Build correct ws:// URL with auth token
// @TEST_EDGE: network_failure -> fetch throws, error toast dispatched
// @TEST_EDGE: auth_token -> localStorage token injected as Bearer header
// @TEST_EDGE: suppress_toast -> Options.suppressToast suppresses error toast
// @TEST_EDGE: api_methods -> Registry methods call correct endpoints

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// Mock the cot-logger to avoid side effects
vi.mock('$lib/cot-logger.js', () => ({
	log: vi.fn(),
	setTraceId: vi.fn(),
	getTraceId: vi.fn(() => 'test-trace-id'),
}));

// Mock toasts
vi.mock('$lib/toasts.svelte.js', () => ({
	addToast: vi.fn(),
}));

describe('ApiModule — module-level exports', () => {
	it('exports API_REQUEST_TIMEOUT constant', async () => {
		const { API_REQUEST_TIMEOUT } = await import('$lib/api.js');
		expect(API_REQUEST_TIMEOUT).toBe(30_000);
	});
});

describe('ApiModule — wsUrl helpers', () => {
	beforeEach(() => {
		vi.stubGlobal('window', {
			location: { protocol: 'http:', host: 'localhost:5173' },
		});
		localStorage.clear();
	});

	afterEach(() => {
		vi.unstubAllGlobals();
	});

	it('getWsUrl returns ws:// URL with auth token when present', async () => {
		localStorage.setItem('auth_token', 'my-token-123');
		const { getWsUrl } = await import('$lib/api.js');
		const url = getWsUrl('task-42');
		expect(url).toBe('ws://localhost:5173/ws/logs/task-42?token=my-token-123');
	});

	it('getWsUrl returns ws:// URL without token when auth_token missing', async () => {
		const { getWsUrl } = await import('$lib/api.js');
		const url = getWsUrl('task-99');
		expect(url).toBe('ws://localhost:5173/ws/logs/task-99');
	});

	it('getTaskEventsWsUrl builds correct URL with token', async () => {
		localStorage.setItem('auth_token', 'tok');
		const { getTaskEventsWsUrl } = await import('$lib/api.js');
		const url = getTaskEventsWsUrl();
		expect(url).toBe('ws://localhost:5173/ws/task-events?token=tok');
	});

	it('getMaintenanceEventsWsUrl builds correct URL', async () => {
		localStorage.setItem('auth_token', 'maint-tok');
		const { getMaintenanceEventsWsUrl } = await import('$lib/api.js');
		const url = getMaintenanceEventsWsUrl();
		expect(url).toBe('ws://localhost:5173/ws/maintenance/events?token=maint-tok');
	});

	it('getTranslateRunWsUrl builds correct URL with runId', async () => {
		localStorage.setItem('auth_token', 'run-tok');
		const { getTranslateRunWsUrl } = await import('$lib/api.js');
		const url = getTranslateRunWsUrl('run-1');
		expect(url).toBe('ws://localhost:5173/ws/translate/run/run-1?token=run-tok');
	});

	it('getWsUrl uses wss:// when window.location.protocol is https:', async () => {
		vi.stubGlobal('window', {
			location: { protocol: 'https:', host: 'app.example.com' },
		});
		localStorage.setItem('auth_token', 'sec-tok');
		const { getWsUrl } = await import('$lib/api.js');
		const url = getWsUrl('task-secure');
		expect(url).toBe('wss://app.example.com/ws/logs/task-secure?token=sec-tok');
	});
});

describe('ApiModule — wsUrl helpers SSR fallback (window undefined)', () => {
	beforeEach(() => {
		vi.stubGlobal('window', undefined);
		localStorage.clear();
	});

	afterEach(() => {
		vi.unstubAllGlobals();
	});

	it('getWsUrl falls back to ws://localhost:8000 when window is undefined', async () => {
		const { getWsUrl } = await import('$lib/api.js');
		const url = getWsUrl('task-ssr');
		expect(url).toBe('ws://localhost:8000/ws/logs/task-ssr');
	});

	it('getTaskEventsWsUrl falls back without auth when window is undefined', async () => {
		const { getTaskEventsWsUrl } = await import('$lib/api.js');
		const url = getTaskEventsWsUrl();
		expect(url).toBe('ws://localhost:8000/ws/task-events');
	});

	it('getMaintenanceEventsWsUrl falls back when window is undefined', async () => {
		const { getMaintenanceEventsWsUrl } = await import('$lib/api.js');
		const url = getMaintenanceEventsWsUrl();
		expect(url).toBe('ws://localhost:8000/ws/maintenance/events');
	});

	it('getTranslateRunWsUrl falls back when window is undefined', async () => {
		const { getTranslateRunWsUrl } = await import('$lib/api.js');
		const url = getTranslateRunWsUrl('run-ssr');
		expect(url).toBe('ws://localhost:8000/ws/translate/run/run-ssr');
	});
});

describe('ApiModule — fetch wrappers (global fetch mock)', () => {
	beforeEach(() => {
		vi.stubGlobal('fetch', vi.fn());
		vi.stubGlobal('window', {
			location: { protocol: 'http:', host: 'localhost:5173' },
		});
		localStorage.clear();
	});

	afterEach(() => {
		vi.unstubAllGlobals();
	});

	it('fetchApi returns JSON on 200', async () => {
		const mockResponse = { plugins: [{ id: 'p1', name: 'Plugin 1' }] };
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve(mockResponse),
		} as Response);

		const { api } = await import('$lib/api.js');
		const result = await api.getPlugins();
		expect(result).toEqual(mockResponse);
		expect(fetch).toHaveBeenCalledWith('/api/plugins', expect.any(Object));
	});

	it('fetchApi returns null on 204 No Content', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 204,
		} as Response);

		const { api } = await import('$lib/api.js');
		const result = await api.fetchApi('/some-empty-resource');
		expect(result).toBeNull();
	});

	it('fetchApi throws ApiError on 500 server error', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 500,
			json: () => Promise.resolve({ detail: 'Internal server error' }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.fetchApi('/fail')).rejects.toMatchObject({
			message: 'Internal server error',
			status: 500,
		});
	});

	it('fetchApi throws ApiError with fallback message when body is not JSON', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 502,
			json: () => Promise.reject(new Error('not JSON')),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.fetchApi('/bad-gateway')).rejects.toMatchObject({
			message: 'API request failed with status 502',
			status: 502,
		});
	});

	it('buildApiError extracts error_code from detail object', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 422,
			json: () => Promise.resolve({ detail: { error_code: 'VALIDATION_ERROR', message: 'Invalid field' } }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.fetchApi('/validate')).rejects.toMatchObject({
			message: 'Invalid field',
			status: 422,
			error_code: 'VALIDATION_ERROR',
		});
	});

	it('notifyApiError dispatches 401 Unauthorized toast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 401,
			json: () => Promise.resolve({ detail: 'Bad token' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.fetchApi('/secure')).rejects.toMatchObject({ status: 401 });
		expect(addToast).toHaveBeenCalledWith('401 Unauthorized: Bad token', 'error');
	});

	it('notifyApiError dispatches Server error toast for 500+', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 503,
			json: () => Promise.resolve({ detail: 'Service unavailable' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.fetchApi('/unavailable')).rejects.toMatchObject({ status: 503 });
		expect(addToast).toHaveBeenCalledWith('Server error (503): Service unavailable', 'error');
	});

	it('fetchApi passes signal to native fetch', async () => {
		const abortController = new AbortController();
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({}),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.fetchApi('/test', { signal: abortController.signal });
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({ signal: abortController.signal }),
		);
	});

	it('postApi passes signal to native fetch', async () => {
		const abortController = new AbortController();
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({}),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.postApi('/test', {}, { signal: abortController.signal });
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({ signal: abortController.signal }),
		);
	});

	it('deleteApi passes signal to native fetch', async () => {
		const abortController = new AbortController();
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({}),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.deleteApi('/test', { signal: abortController.signal });
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({ signal: abortController.signal }),
		);
	});

	it('requestApi passes signal to native fetch', async () => {
		const abortController = new AbortController();
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({}),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.requestApi('/test', 'GET', null, { signal: abortController.signal });
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({ signal: abortController.signal }),
		);
	});

	it('fetchApiBlob passes signal to native fetch', async () => {
		const abortController = new AbortController();
		const blob = new Blob(['data']);
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			blob: () => Promise.resolve(blob),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.fetchApiBlob('/test', { signal: abortController.signal });
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({ signal: abortController.signal }),
		);
	});

	it('fetchApiBlob dispatches error toast by default (notifyError=true)', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 500,
			json: () => Promise.resolve({ detail: 'Server boom' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.fetchApiBlob('/fail')).rejects.toMatchObject({ status: 500 });
		expect(addToast).toHaveBeenCalled();
	});

	it('fetchApiBlob handles 202 with fallback message when response has no JSON', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 202,
			json: () => Promise.reject(new Error('no body')),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.fetchApiBlob('/pending')).rejects.toMatchObject({
			message: 'Resource is being prepared',
			status: 202,
		});
	});

	it('requestApi suppression: git pull endpoint 409 GIT_UNFINISHED_MERGE suppresses toast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false, status: 409,
			json: () => Promise.resolve({ detail: { error_code: 'GIT_UNFINISHED_MERGE', message: 'Merge conflict' } }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/git/repositories/dash-1/pull', 'GET')).rejects.toMatchObject({ status: 409 });
		expect(addToast).not.toHaveBeenCalled();
	});

	it('requestApi suppression: clarification session 404 suppresses toast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false, status: 404,
			json: () => Promise.resolve({ detail: 'Clarification session not found' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/dataset-orchestration/sessions/s1/clarification', 'GET')).rejects.toMatchObject({ status: 404 });
		expect(addToast).not.toHaveBeenCalled();
	});

	it('requestApi suppression: git repo 400 env_id is required suppresses toast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false, status: 400,
			json: () => Promise.resolve({ detail: 'env_id is required' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/git/repositories/dash-1/status', 'GET')).rejects.toMatchObject({ status: 400 });
		expect(addToast).not.toHaveBeenCalled();
	});

	it('requestApi suppression: git config repos 409 already exists suppresses toast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false, status: 409,
			json: () => Promise.resolve({ detail: 'Repository already exists' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/git/config/env-1/repositories', 'GET')).rejects.toMatchObject({ status: 409 });
		expect(addToast).not.toHaveBeenCalled();
	});

	it('requestApi suppression: non-matching endpoint still dispatches toast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false, status: 400,
			json: () => Promise.resolve({ detail: 'Bad request' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/some-other-endpoint', 'GET')).rejects.toMatchObject({ status: 400 });
		expect(addToast).toHaveBeenCalled();
	});

	it('getAuthHeaders merges extra headers', async () => {
		localStorage.setItem('auth_token', 'test-tok');
		vi.mocked(fetch).mockResolvedValue({
			ok: true, status: 200,
			json: () => Promise.resolve({}),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.fetchApi('/test', { headers: { 'X-Custom': 'custom-val' } });
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({
				headers: expect.objectContaining({
					'Content-Type': 'application/json',
					'X-Custom': 'custom-val',
					Authorization: 'Bearer test-tok',
				}),
			}),
		);
	});

	it('fetchApi injects auth token into headers', async () => {
		localStorage.setItem('auth_token', 'bearer-tok');
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({}),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.fetchApi('/profile/preferences');
		expect(fetch).toHaveBeenCalledWith(
			expect.any(String),
			expect.objectContaining({
				headers: expect.objectContaining({
					Authorization: 'Bearer bearer-tok',
				}),
			}),
		);
	});

	it('postApi sends POST with JSON body', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ id: 'new-task' }),
		} as Response);

		const { api } = await import('$lib/api.js');
		const result = await api.postApi('/settings/environments', { name: 'Prod' });
		expect(result).toEqual({ id: 'new-task' });
		expect(fetch).toHaveBeenCalledWith(
			'/api/settings/environments',
			expect.objectContaining({
				method: 'POST',
				body: JSON.stringify({ name: 'Prod' }),
			}),
		);
	});

	it('postApi returns null on 204', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 204,
		} as Response);

		const { api } = await import('$lib/api.js');
		const result = await api.postApi('/no-content', {});
		expect(result).toBeNull();
	});

	it('deleteApi sends DELETE', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ success: true }),
		} as Response);

		const { api } = await import('$lib/api.js');
		const result = await api.deleteApi('/validation-tasks/task-1');
		expect(result).toEqual({ success: true });
		expect(fetch).toHaveBeenCalledWith(
			'/api/validation-tasks/task-1',
			expect.objectContaining({ method: 'DELETE' }),
		);
	});

	it('deleteApi always dispatches error toast on failure', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 500,
			json: () => Promise.resolve({ detail: 'Delete failed' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.deleteApi('/test/1')).rejects.toMatchObject({ status: 500 });
		expect(addToast).toHaveBeenCalled();
	});

	it('postApi dispatches error toast on failure without suppressToast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 400,
			json: () => Promise.resolve({ detail: 'Bad request' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.postApi('/test', {})).rejects.toMatchObject({ status: 400 });
		expect(addToast).toHaveBeenCalled();
	});

	it('postApi suppresses toast when options.suppressToast is true', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 400,
			json: () => Promise.resolve({ detail: 'Hidden' }),
		} as Response);

		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.postApi('/test', {}, { suppressToast: true })).rejects.toMatchObject({ status: 400 });
		expect(addToast).not.toHaveBeenCalled();
	});

	it('requestApi with PATCH sends correct method and body', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ success: true }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.requestApi('/profile/preferences', 'PATCH', { auto_open: true });
		expect(fetch).toHaveBeenCalledWith(
			'/api/profile/preferences',
			expect.objectContaining({
				method: 'PATCH',
				body: JSON.stringify({ auto_open: true }),
			}),
		);
	});

	it('getHealthSummary calls correct endpoint with suppressToast', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ summary: [] }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.getHealthSummary('env-1');
		expect(fetch).toHaveBeenCalledWith(
			'/api/health/summary?env_id=env-1',
			expect.any(Object),
		);
	});

	it('getHealthSummary without environmentId omits query param', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ summary: [] }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.getHealthSummary();
		expect(fetch).toHaveBeenCalledWith('/api/health/summary', expect.any(Object));
	});

	it('getTasks builds URLSearchParams from options', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ tasks: [], total: 0 }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.getTasks({ limit: 10, offset: 0, status: 'running' });
		const calledUrl = vi.mocked(fetch).mock.calls[0][0];
		expect(calledUrl).toContain('/api/tasks?limit=10&offset=0&status=running');
	});

	it('getDashboards builds filter params', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ dashboards: [], total: 0 }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.getDashboards('env-1', {
			page: '1',
			page_size: '20',
			filters: { title: ['Sales'], git_status: ['modified'] },
		});
		const calledUrl = vi.mocked(fetch).mock.calls[0][0];
		expect(calledUrl).toContain('env_id=env-1');
		expect(calledUrl).toContain('filter_title=Sales');
		expect(calledUrl).toContain('filter_git_status=modified');
	});

	it('getDatasets builds query params', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ datasets: [] }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.getDatasets('env-1', { search: 'revenue', page: '1', page_size: '20' });
		const calledUrl = vi.mocked(fetch).mock.calls[0][0];
		expect(calledUrl).toContain('/api/datasets?env_id=env-1&search=revenue&page=1&page_size=20');
	});

	it('lookupSupersetAccounts throws on empty environmentId', async () => {
		const { api } = await import('$lib/api.js');
		// Function throws synchronously — use toThrow wrapper (not rejects)
		expect(() => api.lookupSupersetAccounts('')).toThrow('environmentId is required');
	});

	it('lookupSupersetAccounts builds query params with all options', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true, status: 200,
			json: () => Promise.resolve({ accounts: [{ id: 'a1' }], total: 1 }),
		} as Response);
		const { api } = await import('$lib/api.js');
		const result = await api.lookupSupersetAccounts('env-1', {
			search: 'admin',
			page_index: 0,
			page_size: 25,
			sort_column: 'username',
			sort_order: 'asc',
		});
		expect(result).toEqual({ accounts: [{ id: 'a1' }], total: 1 });
		const calledUrl = vi.mocked(fetch).mock.calls[0][0];
		expect(calledUrl).toContain('environment_id=env-1');
		expect(calledUrl).toContain('search=admin');
		expect(calledUrl).toContain('page_index=0');
		expect(calledUrl).toContain('page_size=25');
		expect(calledUrl).toContain('sort_column=username');
		expect(calledUrl).toContain('sort_order=asc');
	});

	it('getValidationStatusBatch returns empty results stub', async () => {
		const { api } = await import('$lib/api.js');
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ results: [] }),
		} as Response);
		const result = await api.getValidationStatusBatch();
		expect(result).toEqual({ results: [] });
	});

	it('fetchApiBlob returns Blob on 200', async () => {
		const blob = new Blob(['img-data'], { type: 'image/png' });
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			blob: () => Promise.resolve(blob),
		} as Response);

		const { api } = await import('$lib/api.js');
		const result = await api.fetchApiBlob('/storage/file?path=test.png');
		expect(result).toBe(blob);
	});

	it('fetchApiBlob throws ApiError with message on 202 prepared', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 202,
			json: () => Promise.resolve({ message: 'Still generating…' }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.fetchApiBlob('/thumbnail')).rejects.toMatchObject({
			message: 'Still generating…',
			status: 202,
		});
	});

	it('fetchApiBlob throws on non-ok response with notifyError=false', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 404,
			json: () => Promise.resolve({ detail: 'Not found' }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.fetchApiBlob('/missing', { notifyError: false })).rejects.toMatchObject({
			status: 404,
		});
	});

	it('requestApi with suppressToast skips error toast (suppression heuristics)', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: false,
			status: 404,
			json: () => Promise.resolve({ detail: 'Not found' }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/git/repositories/dash-1/status', 'GET', null, { suppressToast: true })).rejects.toMatchObject({
			status: 404,
		});
	});

	// #region captureTraceIdTests [C:2] [TYPE Test] [SEMANTICS test,api,trace-id,headers]
	// @BRIEF Verify _captureTraceId behavior — seeds trace_id from x-trace-id header, handles missing gracefully.
	describe('_captureTraceId', () => {
		const testUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';

		beforeEach(() => {
			vi.clearAllMocks();
		});

		it('sets trace_id from x-trace-id response header', async () => {
			vi.mocked(fetch).mockResolvedValue({
				ok: true,
				status: 200,
				json: () => Promise.resolve({ data: 'ok' }),
				headers: {
					get: vi.fn((name: string) => name === 'x-trace-id' ? testUuid : null),
				},
			} as unknown as Response);

			const cotLogger = await import('$lib/cot-logger.js');
			cotLogger.setTraceId.mockClear();

			const { api } = await import('$lib/api.js');
			await api.fetchApi('/capture-trace-test');

			expect(cotLogger.setTraceId).toHaveBeenCalledWith(testUuid);
		});

		it('handles missing x-trace-id header gracefully', async () => {
			vi.mocked(fetch).mockResolvedValue({
				ok: true,
				status: 200,
				json: () => Promise.resolve({ data: 'ok' }),
				headers: {
					get: vi.fn(() => null),
				},
			} as unknown as Response);

			const cotLogger = await import('$lib/cot-logger.js');
			cotLogger.setTraceId.mockClear();

			const { api } = await import('$lib/api.js');
			await api.fetchApi('/no-trace-id');

			expect(cotLogger.setTraceId).not.toHaveBeenCalled();
		});

		it('handles missing headers API (undefined headers) without throwing', async () => {
			vi.mocked(fetch).mockResolvedValue({
				ok: true,
				status: 200,
				json: () => Promise.resolve({ data: 'ok' }),
			} as Response);

			const { api } = await import('$lib/api.js');

			// Should not throw despite missing headers
			const result = await api.fetchApi('/no-headers');
			expect(result).toEqual({ data: 'ok' });
		});
	});
	// #endregion captureTraceIdTests
});

describe('ApiModule — deleteValidationTask query param', () => {
	beforeEach(() => {
		vi.stubGlobal('fetch', vi.fn());
		vi.stubGlobal('window', {
			location: { protocol: 'http:', host: 'localhost:5173' },
		});
	});

	afterEach(() => {
		vi.unstubAllGlobals();
	});

	it('deleteValidationTask appends ?delete_runs=true by default', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ success: true }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.deleteValidationTask('task-1');
		expect(fetch).toHaveBeenCalledWith(
			'/api/validation-tasks/task-1?delete_runs=true',
			expect.objectContaining({ method: 'DELETE' }),
		);
	});

	it('deleteValidationTask omits query when deleteRuns=false', async () => {
		vi.mocked(fetch).mockResolvedValue({
			ok: true,
			status: 200,
			json: () => Promise.resolve({ success: true }),
		} as Response);

		const { api } = await import('$lib/api.js');
		await api.deleteValidationTask('task-1', false);
		expect(fetch).toHaveBeenCalledWith(
			'/api/validation-tasks/task-1',
			expect.objectContaining({ method: 'DELETE' }),
		);
	});
});

// ── Shared fetch mock for registry method tests ────────────────────
const _okJson = (d: unknown) => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(d) } as Response);
const _okBlob = (b: Blob) => Promise.resolve({ ok: true, status: 200, blob: () => Promise.resolve(b) } as Response);

describe('ApiModule — registry methods', () => {
	beforeEach(() => { vi.stubGlobal('fetch', vi.fn()); vi.stubGlobal('window', { location: { protocol: 'http:', host: 'localhost:5173' } }); });
	afterEach(() => { vi.unstubAllGlobals(); });

	it('getTask calls /api/tasks/{id}', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 't1' }));
		const { api } = await import('$lib/api.js');
		await api.getTask('t1');
		expect(fetch).toHaveBeenCalledWith('/api/tasks/t1', expect.any(Object));
	});
	it('getTaskLogs builds query params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ logs: [], total: 0 }));
		const { api } = await import('$lib/api.js');
		await api.getTaskLogs('t1', { level: 'ERROR', limit: 50 });
		expect(fetch).toHaveBeenCalledWith('/api/tasks/t1/logs?level=ERROR&limit=50', expect.any(Object));
	});
	it('createTask sends POST with plugin_id and params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ task_id: 'new-task' }));
		const { api } = await import('$lib/api.js');
		await api.createTask('plugin-v', { run: true });
		expect(fetch).toHaveBeenCalledWith('/api/tasks', expect.objectContaining({ method: 'POST', body: JSON.stringify({ plugin_id: 'plugin-v', params: { run: true } }) }));
	});
	it('getProfilePreferences returns data', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ auto_open_task_drawer: true }));
		const { api } = await import('$lib/api.js');
		expect(await api.getProfilePreferences()).toEqual({ auto_open_task_drawer: true });
	});
	it('updateProfilePreferences sends PATCH', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateProfilePreferences({ auto_open_task_drawer: false });
		expect(fetch).toHaveBeenCalledWith('/api/profile/preferences', expect.objectContaining({ method: 'PATCH' }));
	});
	it('getSettings returns settings', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ environments: [] }));
		const { api } = await import('$lib/api.js');
		expect(await api.getSettings()).toEqual({ environments: [] });
	});
	it('addEnvironment sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 'e2' }));
		const { api } = await import('$lib/api.js');
		await api.addEnvironment({ name: 'Staging', url: 'http://staging' });
		expect(fetch).toHaveBeenCalledWith('/api/settings/environments', expect.objectContaining({ method: 'POST' }));
	});
	it('deleteEnvironment sends DELETE', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.deleteEnvironment('e1');
		expect(fetch).toHaveBeenCalledWith('/api/settings/environments/e1', expect.objectContaining({ method: 'DELETE' }));
	});
	it('getConsolidatedSettings returns data', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ system: {} }));
		const { api } = await import('$lib/api.js');
		expect(await api.getConsolidatedSettings()).toEqual({ system: {} });
	});
	it('getAllowedLanguages returns string array', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson(['en', 'ru']));
		const { api } = await import('$lib/api.js');
		expect(await api.getAllowedLanguages()).toEqual(['en', 'ru']);
	});
	it('getDashboardDetail with env_id param', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 1 }));
		const { api } = await import('$lib/api.js');
		await api.getDashboardDetail('env-1', 'dash-1');
		expect(fetch).toHaveBeenCalledWith('/api/dashboards/dash-1?env_id=env-1', expect.any(Object));
	});
	it('getDashboardTaskHistory builds params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ tasks: [] }));
		const { api } = await import('$lib/api.js');
		await api.getDashboardTaskHistory('env-1', 'dash-1', { limit: 5 });
		expect(fetch).toHaveBeenCalledWith('/api/dashboards/dash-1/tasks?env_id=env-1&limit=5', expect.any(Object));
	});
	it('getDashboardThumbnail returns blob', async () => {
		const blob = new Blob(['png'], { type: 'image/png' });
		vi.mocked(fetch).mockResolvedValue(await _okBlob(blob));
		const { api } = await import('$lib/api.js');
		expect(await api.getDashboardThumbnail('env-1', 'dash-1')).toBe(blob);
	});
	it('getDatasetIds builds search param', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ ids: [] }));
		const { api } = await import('$lib/api.js');
		await api.getDatasetIds('env-1', { search: 'revenue' });
		expect(fetch).toHaveBeenCalledWith('/api/datasets/ids?env_id=env-1&search=revenue', expect.any(Object));
	});
	it('getDatasetDetail with env_id', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 42 }));
		const { api } = await import('$lib/api.js');
		await api.getDatasetDetail('env-1', '42');
		expect(fetch).toHaveBeenCalledWith('/api/datasets/42?env_id=env-1', expect.any(Object));
	});
	it('getDatabaseMappings builds dual env params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ mappings: [] }));
		const { api } = await import('$lib/api.js');
		await api.getDatabaseMappings('src', 'tgt');
		expect(fetch).toHaveBeenCalledWith('/api/dashboards/db-mappings?source_env_id=src&target_env_id=tgt', expect.any(Object));
	});
	it('calculateMigrationDryRun sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ diff: {} }));
		const { api } = await import('$lib/api.js');
		await api.calculateMigrationDryRun({ source_env_id: 's', target_env_id: 't', selected_dashboard_ids: [1] });
		expect(fetch).toHaveBeenCalledWith('/api/migration/dry-run', expect.objectContaining({ method: 'POST' }));
	});
	it('getLlmStatus returns configured status', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ configured: true, providers: [] }));
		const { api } = await import('$lib/api.js');
		expect((await api.getLlmStatus()).configured).toBe(true);
	});
	it('getLlmProviders returns list', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ providers: [] }));
		const { api } = await import('$lib/api.js');
		expect((await api.getLlmProviders()).providers).toEqual([]);
	});
	it('parseValidationUrl sends POST with url and env', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ dashboard_id: 1 }));
		const { api } = await import('$lib/api.js');
		await api.parseValidationUrl('http://superset/d/1', 'env-1');
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/parse-url', expect.objectContaining({ method: 'POST' }));
	});
	it('getValidationTasks builds all params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ tasks: [], total: 0 }));
		const { api } = await import('$lib/api.js');
		await api.getValidationTasks({ page: 1, page_size: 20, is_active: true, environment_id: 'env-1', search: 'test' });
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks?page=1&page_size=20&is_active=true&environment_id=env-1&search=test', expect.any(Object));
	});
	it('createValidationTask sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 'vt1' }));
		const { api } = await import('$lib/api.js');
		await api.createValidationTask({ name: 'Nightly', environment_id: 'env-1' });
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks', expect.objectContaining({ method: 'POST' }));
	});
	it('toggleValidationTaskStatus sends PATCH', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.toggleValidationTaskStatus('vt1', false);
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/vt1/status', expect.objectContaining({ method: 'PATCH' }));
	});
	it('getValidationRuns builds page params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ runs: [], total: 0 }));
		const { api } = await import('$lib/api.js');
		await api.getValidationRuns('vt1', { page: 1, page_size: 10 });
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/vt1/runs?page=1&page_size=10', expect.any(Object));
	});
	it('createApiKey returns secret', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 'ak1', key: 'sk-123' }));
		const { api } = await import('$lib/api.js');
		expect((await api.createApiKey({ name: 'CI key' })).key).toBe('sk-123');
	});
	it('requestApi suppression: git 404 suppresses toast', async () => {
		vi.mocked(fetch).mockResolvedValue({ ok: false, status: 404, json: () => Promise.resolve({ detail: 'Repository for dashboard x not found' }) } as Response);
		const { addToast } = await import('$lib/toasts.svelte.js');
		addToast.mockClear();
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/git/repositories/dash-1/status', 'GET')).rejects.toMatchObject({ status: 404 });
		expect(addToast).not.toHaveBeenCalled();
	});
	it('requestApi suppression: non-git 404 dispatches toast', async () => {
		vi.mocked(fetch).mockResolvedValue({ ok: false, status: 404, json: () => Promise.resolve({ detail: 'Not found' }) } as Response);
		const { api } = await import('$lib/api.js');
		await expect(api.requestApi('/unknown', 'GET')).rejects.toMatchObject({ status: 404 });
		const { addToast } = await import('$lib/toasts.svelte.js');
		expect(addToast).toHaveBeenCalled();
	});
	it('getStorageFileBlob returns blob with encoded path', async () => {
		const blob = new Blob(['data']);
		vi.mocked(fetch).mockResolvedValue(await _okBlob(blob));
		const { api } = await import('$lib/api.js');
		expect(await api.getStorageFileBlob('/some/file.csv')).toBe(blob);
		expect(fetch).toHaveBeenCalledWith('/api/storage/file?path=%2Fsome%2Ffile.csv', expect.any(Object));
	});
	it('updateGlobalSettings sends PATCH', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateGlobalSettings({ feature_x: true });
		expect(fetch).toHaveBeenCalledWith('/api/settings/global', expect.objectContaining({ method: 'PATCH' }));
	});
	it('getEnvironments fetches environment list', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ environments: [] }));
		const { api } = await import('$lib/api.js');
		expect(await api.getEnvironments()).toEqual({ environments: [] });
	});
	it('updateEnvironment sends PUT with id', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateEnvironment('e1', { name: 'Updated' });
		expect(fetch).toHaveBeenCalledWith('/api/settings/environments/e1', expect.objectContaining({ method: 'PUT' }));
	});
	it('testEnvironmentConnection sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.testEnvironmentConnection('e1');
		expect(fetch).toHaveBeenCalledWith('/api/settings/environments/e1/test', expect.objectContaining({ method: 'POST' }));
	});
	it('updateEnvironmentSchedule sends PUT with options', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateEnvironmentSchedule('e1', { cron: '0 9 * * *' });
		expect(fetch).toHaveBeenCalledWith('/api/environments/e1/schedule', expect.objectContaining({ method: 'PUT' }));
	});
	it('getStorageSettings fetches storage config', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ path: '/tmp' }));
		const { api } = await import('$lib/api.js');
		expect(await api.getStorageSettings()).toEqual({ path: '/tmp' });
	});
	it('updateStorageSettings sends PUT', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateStorageSettings({ path: '/new' });
		expect(fetch).toHaveBeenCalledWith('/api/settings/storage', expect.objectContaining({ method: 'PUT' }));
	});
	it('getEnvironmentsList fetches flat list with options', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson([{ id: 'e1' }]));
		const { api } = await import('$lib/api.js');
		expect(await api.getEnvironmentsList()).toEqual([{ id: 'e1' }]);
	});
	it('updateConsolidatedSettings sends PATCH', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateConsolidatedSettings({ llm: {} });
		expect(fetch).toHaveBeenCalledWith('/api/settings/consolidated', expect.objectContaining({ method: 'PATCH' }));
	});
	it('getValidationPolicies fetches policies', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ policies: [] }));
		const { api } = await import('$lib/api.js');
		expect(await api.getValidationPolicies()).toEqual({ policies: [] });
	});
	it('createValidationPolicy sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 'vp1' }));
		const { api } = await import('$lib/api.js');
		await api.createValidationPolicy({ name: 'Policy 1' });
		expect(fetch).toHaveBeenCalledWith('/api/settings/automation/policies', expect.objectContaining({ method: 'POST' }));
	});
	it('updateValidationPolicy sends PATCH', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateValidationPolicy('vp1', { is_active: false });
		expect(fetch).toHaveBeenCalledWith('/api/settings/automation/policies/vp1', expect.objectContaining({ method: 'PATCH' }));
	});
	it('deleteValidationPolicy sends DELETE', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.deleteValidationPolicy('vp1');
		expect(fetch).toHaveBeenCalledWith('/api/settings/automation/policies/vp1', expect.objectContaining({ method: 'DELETE' }));
	});
	it('getTranslationSchedules fetches schedule list', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ schedules: [] }));
		const { api } = await import('$lib/api.js');
		expect(await api.getTranslationSchedules()).toEqual({ schedules: [] });
	});
	it('getValidationTask fetches single task by id', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 'vt1' }));
		const { api } = await import('$lib/api.js');
		await api.getValidationTask('vt1');
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/vt1', expect.any(Object));
	});
	it('updateValidationTask sends PUT', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.updateValidationTask('vt1', { name: 'Updated' });
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/vt1', expect.objectContaining({ method: 'PUT' }));
	});
	it('triggerValidationRun sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ run_id: 'r1' }));
		const { api } = await import('$lib/api.js');
		await api.triggerValidationRun('vt1');
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/vt1/run', expect.objectContaining({ method: 'POST' }));
	});
	it('getValidationRunDetail fetches run detail', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ id: 'r1' }));
		const { api } = await import('$lib/api.js');
		await api.getValidationRunDetail('vt1', 'r1');
		expect(fetch).toHaveBeenCalledWith('/api/validation-tasks/vt1/runs/r1', expect.any(Object));
	});
	it('listApiKeys fetches API key list', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ api_keys: [] }));
		const { api } = await import('$lib/api.js');
		expect(await api.listApiKeys()).toEqual({ api_keys: [] });
	});
	it('revokeApiKey sends DELETE', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ success: true }));
		const { api } = await import('$lib/api.js');
		await api.revokeApiKey('ak-1');
		expect(fetch).toHaveBeenCalledWith('/api/admin/api-keys/ak-1', expect.objectContaining({ method: 'DELETE' }));
	});
	it('fetchLlmModels sends POST', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ models: [] }));
		const { api } = await import('$lib/api.js');
		await api.fetchLlmModels({ provider: 'openai' });
		expect(fetch).toHaveBeenCalledWith('/api/llm/providers/fetch-models', expect.objectContaining({ method: 'POST' }));
	});
	it('getEnvironmentDatabases fetches databases', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ databases: [] }));
		const { api } = await import('$lib/api.js');
		await api.getEnvironmentDatabases('env-1');
		expect(fetch).toHaveBeenCalledWith('/api/environments/env-1/databases', expect.any(Object));
	});
	it('getTasks builds task_type and completed_only params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ tasks: [], total: 0 }));
		const { api } = await import('$lib/api.js');
		await api.getTasks({ task_type: 'validation', completed_only: true, plugin_id: ['p1', 'p2'] });
		const calledUrl = vi.mocked(fetch).mock.calls[0][0];
		expect(calledUrl).toContain('task_type=validation');
		expect(calledUrl).toContain('completed_only=true');
		expect(calledUrl).toContain('plugin_id=p1');
		expect(calledUrl).toContain('plugin_id=p2');
	});
	it('getTaskLogs builds source, search, offset params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ logs: [], total: 0 }));
		const { api } = await import('$lib/api.js');
		await api.getTaskLogs('t1', { source: 'worker', search: 'error', offset: 10 });
		expect(fetch).toHaveBeenCalledWith('/api/tasks/t1/logs?source=worker&search=error&offset=10', expect.any(Object));
	});
	it('getDashboards builds all filter and option params', async () => {
		vi.mocked(fetch).mockResolvedValue(await _okJson({ dashboards: [], total: 0 }));
		const { api } = await import('$lib/api.js');
		await api.getDashboards('env-1', {
			page: '2', page_size: '50', search: 'sales', page_context: 'dashboard-list',
			apply_profile_default: true, override_show_all: false,
			filters: { llm_status: ['ready'], changed_on: ['today'], actor: ['user1'] },
		});
		const calledUrl = vi.mocked(fetch).mock.calls[0][0];
		expect(calledUrl).toContain('page=2');
		expect(calledUrl).toContain('page_size=50');
		expect(calledUrl).toContain('search=sales');
		expect(calledUrl).toContain('page_context=dashboard-list');
		expect(calledUrl).toContain('apply_profile_default=true');
		expect(calledUrl).toContain('override_show_all=false');
		expect(calledUrl).toContain('filter_llm_status=ready');
		expect(calledUrl).toContain('filter_changed_on=today');
		expect(calledUrl).toContain('filter_actor=user1');
	});
});
// #endregion ApiModuleTest
