import { describe, it, expect, vi } from 'vitest';
import { saveBlockWithConflictRetry } from './saveBlockWithConflictRetry';
import { BlockConflictResolvedError } from './blockConflictMerge';
import type { PersonMention } from '$lib/shared/types';

const DOC = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
const BLK = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';

const SERVER_BLOCK_AFTER_RENAME = {
	id: BLK,
	annotationId: 'cccccccc-cccc-cccc-cccc-cccccccccccc',
	documentId: DOC,
	text: 'old text from server',
	label: null,
	sortOrder: 0,
	version: 7,
	source: 'MANUAL' as const,
	reviewed: false,
	mentionedPersons: [{ personId: 'p-aug', displayName: 'Augusta Raddatz' }]
};

function mkResponse(status: number, body?: unknown): Response {
	return new Response(body === undefined ? null : JSON.stringify(body), {
		status,
		headers: { 'Content-Type': 'application/json' }
	});
}

describe('saveBlockWithConflictRetry', () => {
	it('returns the server-saved block on a successful PUT', async () => {
		const updated = { ...SERVER_BLOCK_AFTER_RENAME, text: 'persisted text' };
		const fetchImpl = vi.fn().mockResolvedValueOnce(mkResponse(200, updated));

		const result = await saveBlockWithConflictRetry({
			fetchImpl: fetchImpl as unknown as typeof fetch,
			documentId: DOC,
			blockId: BLK,
			text: 'persisted text',
			mentionedPersons: []
		});

		expect(result).toEqual(updated);
		expect(fetchImpl).toHaveBeenCalledTimes(1);
		expect(fetchImpl).toHaveBeenCalledWith(
			`/api/documents/${DOC}/transcription-blocks/${BLK}`,
			expect.objectContaining({
				method: 'PUT',
				body: JSON.stringify({ text: 'persisted text', mentionedPersons: [] })
			})
		);
	});

	it('throws BlockConflictResolvedError carrying the merged block on 409', async () => {
		const fetchImpl = vi
			.fn()
			.mockResolvedValueOnce(mkResponse(409))
			.mockResolvedValueOnce(mkResponse(200, SERVER_BLOCK_AFTER_RENAME));

		const localMentions: PersonMention[] = [{ personId: 'p-aug', displayName: 'Auguste Raddatz' }];

		await expect(
			saveBlockWithConflictRetry({
				fetchImpl: fetchImpl as unknown as typeof fetch,
				documentId: DOC,
				blockId: BLK,
				text: 'transcriber unsaved input',
				mentionedPersons: localMentions
			})
		).rejects.toThrow(BlockConflictResolvedError);

		expect(fetchImpl).toHaveBeenCalledTimes(2);
		// First call PUT, second is the GET refetch.
		expect(fetchImpl.mock.calls[0]?.[1]?.method).toBe('PUT');
		expect(fetchImpl.mock.calls[1]?.[1]).toBeUndefined();
	});

	it('attaches the merged block to err.merged so callers can update local state', async () => {
		const fetchImpl = vi
			.fn()
			.mockResolvedValueOnce(mkResponse(409))
			.mockResolvedValueOnce(mkResponse(200, SERVER_BLOCK_AFTER_RENAME));

		const localMentions: PersonMention[] = [
			{ personId: 'p-aug', displayName: 'Auguste Raddatz' } // stale displayName
		];
		try {
			await saveBlockWithConflictRetry({
				fetchImpl: fetchImpl as unknown as typeof fetch,
				documentId: DOC,
				blockId: BLK,
				text: 'transcriber unsaved input',
				mentionedPersons: localMentions
			});
			throw new Error('expected throw');
		} catch (err) {
			expect(err).toBeInstanceOf(BlockConflictResolvedError);
			const merged = (err as BlockConflictResolvedError).merged!;
			// Local text wins.
			expect(merged.text).toBe('transcriber unsaved input');
			// Server displayName wins for shared personId.
			expect(merged.mentionedPersons).toEqual([
				{ personId: 'p-aug', displayName: 'Augusta Raddatz' }
			]);
			// Server version carried forward.
			expect(merged.version).toBe(7);
		}
	});

	it('throws BlockConflictResolvedError without merged when refetch fails', async () => {
		const fetchImpl = vi
			.fn()
			.mockResolvedValueOnce(mkResponse(409))
			.mockResolvedValueOnce(mkResponse(500));

		await expect(
			saveBlockWithConflictRetry({
				fetchImpl: fetchImpl as unknown as typeof fetch,
				documentId: DOC,
				blockId: BLK,
				text: 'x',
				mentionedPersons: []
			})
		).rejects.toMatchObject({ code: 'CONFLICT_RESOLVED', merged: undefined });
	});

	it('throws Save failed for any other non-OK response', async () => {
		const fetchImpl = vi.fn().mockResolvedValueOnce(mkResponse(500));
		await expect(
			saveBlockWithConflictRetry({
				fetchImpl: fetchImpl as unknown as typeof fetch,
				documentId: DOC,
				blockId: BLK,
				text: 'x',
				mentionedPersons: []
			})
		).rejects.toThrow('Save failed');
	});

	it('rejects ids that are not UUIDs (path-injection guard)', async () => {
		const fetchImpl = vi.fn();
		await expect(
			saveBlockWithConflictRetry({
				fetchImpl: fetchImpl as unknown as typeof fetch,
				documentId: DOC,
				blockId: '../../etc/passwd',
				text: 'x',
				mentionedPersons: []
			})
		).rejects.toThrow(/Invalid id/);
		expect(fetchImpl).not.toHaveBeenCalled();
	});
});
