import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import CommentThread from './CommentThread.svelte';
import type { Comment } from '$lib/shared/types';

afterEach(cleanup);

const baseComment = (overrides: Partial<Comment> = {}): Comment =>
	({
		id: 'c-1',
		documentId: 'doc-1',
		content: 'Hello world',
		authorId: 'u-1',
		authorName: 'Anna',
		createdAt: '2026-01-01T00:00:00Z',
		updatedAt: null,
		replies: [],
		...overrides
	}) as Comment;

describe('CommentThread', () => {
	let fetchSpy: ReturnType<typeof vi.spyOn>;

	beforeEach(() => {
		fetchSpy = vi
			.spyOn(globalThis, 'fetch')
			.mockResolvedValue(
				new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } })
			);
	});

	afterEach(() => {
		fetchSpy?.mockRestore();
	});

	it('renders the empty hint when there are no comments', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: []
			}
		});

		await expect.element(page.getByText(/noch keine kommentare/i)).toBeVisible();
	});

	it('renders the comment list when initialComments is non-empty', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [baseComment({ id: 'c-1', content: 'First comment' })]
			}
		});

		await expect.element(page.getByText('First comment')).toBeVisible();
		const header = document.querySelector('.font-sans.font-semibold');
		expect(header?.textContent).toMatch(/1\s+Kommentar(?!e)/);
	});

	it('renders the plural label when there are 2 or more', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [
					baseComment({ id: 'c-1', content: 'First comment' }),
					baseComment({ id: 'c-2', content: 'Second comment' })
				]
			}
		});

		const header = document.querySelector('.font-sans.font-semibold');
		expect(header?.textContent).toMatch(/2\s+Kommentare/);
	});

	it('does not render the compose textarea when canComment is false', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: []
			}
		});

		const ta = document.querySelector('textarea');
		expect(ta).toBeNull();
	});

	it('renders the compose textarea when canComment is true', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: 'u-1',
				initialComments: []
			}
		});

		const ta = document.querySelector('textarea');
		expect(ta).not.toBeNull();
	});

	it('hides the compose textarea when showCompose is false and there are no comments', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: 'u-1',
				initialComments: [],
				showCompose: false
			}
		});

		const ta = document.querySelector('textarea');
		expect(ta).toBeNull();
	});

	it('shows the compose textarea when showCompose is false but flatMessages is non-empty', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: 'u-1',
				initialComments: [baseComment({ id: 'c-1' })],
				showCompose: false
			}
		});

		const ta = document.querySelector('textarea');
		expect(ta).not.toBeNull();
	});

	it('seeds the textarea with a quote when quotedText is set', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: 'u-1',
				initialComments: [],
				quotedText: 'die wichtige Stelle'
			}
		});

		await vi.waitFor(() => {
			const ta = document.querySelector('textarea') as HTMLTextAreaElement;
			expect(ta?.value).toContain('die wichtige Stelle');
		});
	});

	it('calls onCountChange on mount with the initial total when loadOnMount=false', async () => {
		const onCountChange = vi.fn();
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [
					baseComment({
						id: 'c-1',
						replies: [
							baseComment({ id: 'r-1' }) as Comment,
							baseComment({ id: 'r-2' }) as Comment
						] as Comment[]
					})
				],
				loadOnMount: false,
				onCountChange
			}
		});

		// 1 thread + 2 replies = 3.
		await vi.waitFor(() => expect(onCountChange).toHaveBeenCalledWith(3));
	});

	it('uses the annotation comments URL when annotationId is provided', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-X',
				annotationId: 'ann-Y',
				canComment: false,
				currentUserId: null,
				initialComments: [],
				loadOnMount: true
			}
		});

		await vi.waitFor(() => {
			const calls = fetchSpy.mock.calls.map((c) => c[0].toString());
			expect(calls.some((c) => c.includes('/api/documents/doc-X/annotations/ann-Y/comments'))).toBe(
				true
			);
		});
	});

	it('uses the block comments URL when blockId is provided', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-X',
				blockId: 'block-Z',
				canComment: false,
				currentUserId: null,
				initialComments: [],
				loadOnMount: true
			}
		});

		await vi.waitFor(() => {
			const calls = fetchSpy.mock.calls.map((c) => c[0].toString());
			expect(
				calls.some((c) => c.includes('/api/documents/doc-X/transcription-blocks/block-Z/comments'))
			).toBe(true);
		});
	});

	it('uses the document comments URL when neither annotationId nor blockId is provided', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-X',
				canComment: false,
				currentUserId: null,
				initialComments: [],
				loadOnMount: true
			}
		});

		await vi.waitFor(() => {
			const calls = fetchSpy.mock.calls.map((c) => c[0].toString());
			expect(calls.some((c) => c.endsWith('/api/documents/doc-X/comments'))).toBe(true);
		});
	});

	it('fires onCountChange with the loaded comment count after a successful reload', async () => {
		const onCountChange = vi.fn();
		fetchSpy.mockResolvedValueOnce(
			new Response(
				JSON.stringify([
					{
						id: 'c-1',
						documentId: 'doc-1',
						content: 'Loaded',
						authorId: 'u-1',
						authorName: 'Anna',
						createdAt: '2026-01-01T00:00:00Z',
						updatedAt: null,
						replies: []
					}
				]),
				{ status: 200, headers: { 'Content-Type': 'application/json' } }
			)
		);

		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [],
				loadOnMount: true,
				onCountChange
			}
		});

		await vi.waitFor(() => expect(onCountChange).toHaveBeenCalledWith(1));
	});

	it('treats currentUserId=null as never owning a comment', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: null,
				initialComments: [baseComment({ id: 'c-1', authorId: 'u-1' })]
			}
		});

		// No edit/delete buttons because no comment is "own".
		const editBtns = Array.from(document.querySelectorAll('button')).filter((b) =>
			/bearbeiten/i.test(b.textContent ?? '')
		);
		expect(editBtns.length).toBe(0);
	});

	it('flat-messages flattens replies into the rendered list', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [
					{
						...baseComment({ id: 'c-1', content: 'Top' }),
						replies: [
							baseComment({ id: 'r-1', content: 'Reply 1' }),
							baseComment({ id: 'r-2', content: 'Reply 2' })
						]
					} as Comment
				]
			}
		});

		expect(document.body.textContent).toContain('Top');
		expect(document.body.textContent).toContain('Reply 1');
		expect(document.body.textContent).toContain('Reply 2');
	});

	it('does not seed quotedText when it is whitespace-only', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: 'u-1',
				initialComments: [],
				quotedText: '   '
			}
		});

		await vi.waitFor(() => {
			expect(document.querySelector('textarea')).not.toBeNull();
		});
		const ta = document.querySelector('textarea') as HTMLTextAreaElement;
		expect(ta.value).toBe('');
	});

	it('renders the initial comment when onCountChange is not provided', async () => {
		// Component must not assume the callback is wired up; verify content still renders.
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [baseComment()],
				loadOnMount: false
			}
		});

		await expect.element(page.getByText('Hello world')).toBeVisible();
	});

	it('keeps the empty-hint state when reload fetch rejects (network error)', async () => {
		fetchSpy.mockRejectedValueOnce(new Error('network down'));

		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [],
				loadOnMount: true
			}
		});

		// On rejection the component swallows the error and falls back to empty state.
		await expect.element(page.getByText(/noch keine kommentare/i)).toBeVisible();
	});

	it('keeps the empty-hint state when reload returns non-OK status', async () => {
		fetchSpy.mockResolvedValueOnce(new Response('error', { status: 500 }));

		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: false,
				currentUserId: null,
				initialComments: [],
				loadOnMount: true
			}
		});

		await expect.element(page.getByText(/noch keine kommentare/i)).toBeVisible();
	});

	it('renders own comment when authorId matches currentUserId', async () => {
		render(CommentThread, {
			props: {
				documentId: 'doc-1',
				canComment: true,
				currentUserId: 'u-self',
				initialComments: [baseComment({ id: 'c-mine', authorId: 'u-self', content: 'mine' })]
			}
		});

		await expect.element(page.getByText('mine')).toBeVisible();
	});
});
