// #region TranslateCorrectionsApiTest [C:3] [TYPE Module] [SEMANTICS test, translate, corrections, api]
// @BRIEF Unit tests for translation corrections API — inline edits, dictionary submission,
//   bulk find-and-replace, CSV downloads, and error normalization.
// @LAYER Tests
// @RELATION BINDS_TO -> [TranslateCorrectionsApi]
// @TEST_CONTRACT: submitCorrection -> POST /translate/corrections with payload
// @TEST_CONTRACT: inlineEditCorrection -> PUT to run/record/language endpoint
// @TEST_CONTRACT: bulkFindReplace -> POST with find/replace patterns
// @TEST_CONTRACT: bulkReplacePreview -> POST with preview:true
// @TEST_CONTRACT: csv downloads -> Blob fetch with DOM manipulation
// @TEST_EDGE: api_error -> normalizeTranslateError wraps errors
// @TEST_EDGE: non_error_error -> string errors handled gracefully
// @TEST_EDGE: empty_payload -> Empty payload sent as-is

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

const mockApi = {
	postApi: vi.fn(),
	requestApi: vi.fn(),
	fetchApiBlob: vi.fn(),
};

vi.mock('$lib/api.js', () => ({
	api: mockApi,
}));

describe('submitCorrection', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('sends POST to /translate/corrections with payload', async () => {
		mockApi.postApi.mockResolvedValue({ success: true });
		const { submitCorrection } = await import('$lib/api/translate/corrections.js');
		const payload = { record_id: 'rec-1', language_code: 'fr', corrected_text: 'Bonjour' };
		const result = await submitCorrection(payload);
		expect(result).toEqual({ success: true });
		expect(mockApi.postApi).toHaveBeenCalledWith('/translate/corrections', payload);
	});

	it('normalizes API error', async () => {
		mockApi.postApi.mockRejectedValue(new Error('Server error'));
		const { submitCorrection } = await import('$lib/api/translate/corrections.js');
		await expect(submitCorrection({})).rejects.toMatchObject({
			message: 'Server error',
			code: 'TRANSLATE_API_ERROR',
			retryable: true,
		});
	});

	it('throws normalized error for string rejection', async () => {
		mockApi.postApi.mockRejectedValue('string error');
		const { submitCorrection } = await import('$lib/api/translate/corrections.js');
		await expect(submitCorrection({})).rejects.toMatchObject({
			message: 'string error',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});

describe('submitBulkCorrections', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('sends POST to /translate/corrections/bulk', async () => {
		mockApi.postApi.mockResolvedValue({ results: [] });
		const { submitBulkCorrections } = await import('$lib/api/translate/corrections.js');
		const payload = { corrections: [{ record_id: 'r1' }] };
		const result = await submitBulkCorrections(payload);
		expect(result).toEqual({ results: [] });
		expect(mockApi.postApi).toHaveBeenCalledWith('/translate/corrections/bulk', payload);
	});
});

describe('inlineEditCorrection', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('sends PUT to run/record/language endpoint', async () => {
		mockApi.requestApi.mockResolvedValue({ updated: true });
		const { inlineEditCorrection } = await import('$lib/api/translate/corrections.js');
		const data = { translated_text: 'Hola' };
		const result = await inlineEditCorrection('run-1', 'rec-1', 'es', data);
		expect(result).toEqual({ updated: true });
		expect(mockApi.requestApi).toHaveBeenCalledWith(
			'/translate/runs/run-1/records/rec-1/languages/es',
			'PUT',
			data,
		);
	});

	it('normalizes error on inline edit failure', async () => {
		mockApi.requestApi.mockRejectedValue(new Error('Timeout'));
		const { inlineEditCorrection } = await import('$lib/api/translate/corrections.js');
		await expect(inlineEditCorrection('run-1', 'rec-1', 'de', {})).rejects.toMatchObject({
			message: 'Timeout',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});

describe('submitCorrectionToDict', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('sends POST to submit-to-dict endpoint', async () => {
		mockApi.requestApi.mockResolvedValue({ entry_id: 'dict-entry-1' });
		const { submitCorrectionToDict } = await import('$lib/api/translate/corrections.js');
		const result = await submitCorrectionToDict('run-1', 'rec-1', 'fr', 'dict-1');
		expect(result).toEqual({ entry_id: 'dict-entry-1' });
		expect(mockApi.requestApi).toHaveBeenCalledWith(
			'/translate/runs/run-1/records/rec-1/languages/fr/submit-to-dict',
			'POST',
			{ dictionary_id: 'dict-1' },
		);
	});

	it('normalizes error on submit to dict failure', async () => {
		mockApi.requestApi.mockRejectedValue(new Error('Dict error'));
		const { submitCorrectionToDict } = await import('$lib/api/translate/corrections.js');
		await expect(submitCorrectionToDict('run-1', 'rec-1', 'fr', 'dict-1')).rejects.toMatchObject({
			message: 'Dict error',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});

describe('bulkFindReplace', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('sends POST to bulk-replace endpoint', async () => {
		mockApi.postApi.mockResolvedValue({ affected: 5 });
		const { bulkFindReplace } = await import('$lib/api/translate/corrections.js');
		const data = { find: 'foo', replace: 'bar' };
		const result = await bulkFindReplace('run-1', data);
		expect(result).toEqual({ affected: 5 });
		expect(mockApi.postApi).toHaveBeenCalledWith('/translate/runs/run-1/bulk-replace', data);
	});

	it('normalizes error on bulk find-replace failure', async () => {
		mockApi.postApi.mockRejectedValue(new Error('Bulk replace failed'));
		const { bulkFindReplace } = await import('$lib/api/translate/corrections.js');
		await expect(bulkFindReplace('run-1', {})).rejects.toMatchObject({
			message: 'Bulk replace failed',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});

describe('bulkReplacePreview', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it('sends POST with preview:true flag', async () => {
		mockApi.postApi.mockResolvedValue({ affected: 0, preview: true });
		const { bulkReplacePreview } = await import('$lib/api/translate/corrections.js');
		const data = { find: 'foo', replace: 'bar' };
		const result = await bulkReplacePreview('run-1', data);
		expect(result).toEqual({ affected: 0, preview: true });
		expect(mockApi.postApi).toHaveBeenCalledWith('/translate/runs/run-1/bulk-replace', {
			...data,
			preview: true,
		});
	});

	it('normalizes error on preview failure', async () => {
		mockApi.postApi.mockRejectedValue(new Error('Preview error'));
		const { bulkReplacePreview } = await import('$lib/api/translate/corrections.js');
		await expect(bulkReplacePreview('run-1', {})).rejects.toMatchObject({
			message: 'Preview error',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});

describe('downloadSkippedCsv', () => {
	beforeEach(() => {
		vi.clearAllMocks();
		const mockAnchor = {
			href: '',
			download: '',
			click: vi.fn(),
		};
		vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor as unknown as HTMLAnchorElement);
		vi.spyOn(document.body, 'appendChild').mockReturnValue(mockAnchor as unknown as HTMLElement);
		vi.spyOn(document.body, 'removeChild').mockReturnValue(mockAnchor as unknown as HTMLElement);
		vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
		vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
	});

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

	it('fetches blob and triggers browser download', async () => {
		const blob = new Blob(['a,b,c\n1,2,3'], { type: 'text/csv' });
		mockApi.fetchApiBlob.mockResolvedValue(blob);
		const { downloadSkippedCsv } = await import('$lib/api/translate/corrections.js');
		await downloadSkippedCsv('run-abc12345');
		expect(mockApi.fetchApiBlob).toHaveBeenCalledWith('/translate/runs/run-abc12345/skipped.csv');
		expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
		expect(document.createElement).toHaveBeenCalledWith('a');
		expect(document.body.appendChild).toHaveBeenCalled();
	});

	it('normalizes error on CSV download failure', async () => {
		mockApi.fetchApiBlob.mockRejectedValue(new Error('Network error'));
		const { downloadSkippedCsv } = await import('$lib/api/translate/corrections.js');
		await expect(downloadSkippedCsv('run-1')).rejects.toMatchObject({
			message: 'Network error',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});

describe('downloadFailedCsv', () => {
	beforeEach(() => {
		vi.clearAllMocks();
		const mockAnchor = {
			href: '',
			download: '',
			click: vi.fn(),
		};
		vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor as unknown as HTMLAnchorElement);
		vi.spyOn(document.body, 'appendChild').mockReturnValue(mockAnchor as unknown as HTMLElement);
		vi.spyOn(document.body, 'removeChild').mockReturnValue(mockAnchor as unknown as HTMLElement);
		vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-failed');
		vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
	});

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

	it('fetches failed CSV blob and triggers download', async () => {
		const blob = new Blob(['failed data'], { type: 'text/csv' });
		mockApi.fetchApiBlob.mockResolvedValue(blob);
		const { downloadFailedCsv } = await import('$lib/api/translate/corrections.js');
		await downloadFailedCsv('run-xyz987');
		expect(mockApi.fetchApiBlob).toHaveBeenCalledWith('/translate/runs/run-xyz987/failed.csv');
		expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
	});

	it('sets correct download filename (first 8 chars of runId)', async () => {
		const blob = new Blob(['data'], { type: 'text/csv' });
		mockApi.fetchApiBlob.mockResolvedValue(blob);
		const mockAnchor = document.createElement('a') as HTMLAnchorElement;
		vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor);
		const { downloadFailedCsv } = await import('$lib/api/translate/corrections.js');
		await downloadFailedCsv('run-long-id-12345678');
		// runId.substring(0, 8) = 'run-long' → 'failed-run-long.csv'
		expect(mockAnchor.download).toBe('failed-run-long.csv');
	});

	it('normalizes error on failed CSV download failure', async () => {
		mockApi.fetchApiBlob.mockRejectedValue(new Error('Failed CSV error'));
		const { downloadFailedCsv } = await import('$lib/api/translate/corrections.js');
		await expect(downloadFailedCsv('run-1')).rejects.toMatchObject({
			message: 'Failed CSV error',
			code: 'TRANSLATE_API_ERROR',
		});
	});

	it('normalizes error for empty object rejection', async () => {
		mockApi.postApi.mockRejectedValue({});
		const { submitCorrection } = await import('$lib/api/translate/corrections.js');
		await expect(submitCorrection({})).rejects.toMatchObject({
			message: 'Failed to submit correction',
			code: 'TRANSLATE_API_ERROR',
		});
	});

	it('submitBulkCorrections normalizes error', async () => {
		mockApi.postApi.mockRejectedValue(new Error('Bulk failed'));
		const { submitBulkCorrections } = await import('$lib/api/translate/corrections.js');
		await expect(submitBulkCorrections({})).rejects.toMatchObject({
			message: 'Bulk failed',
			code: 'TRANSLATE_API_ERROR',
		});
	});
});
// #endregion TranslateCorrectionsApiTest
