import { describe, expect, it, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import DocumentList from './DocumentList.svelte';
import type { components } from '$lib/generated/api';

vi.mock('$app/navigation', () => ({ goto: vi.fn() }));

afterEach(() => cleanup());

type DocumentListItem = components['schemas']['DocumentListItem'];

function makeItem(overrides: Partial<DocumentListItem> = {}): DocumentListItem {
	return {
		id: '1',
		title: 'Testbrief',
		originalFilename: 'testbrief.pdf',
		documentDate: '2024-03-15',
		metaDatePrecision: 'DAY',
		sender: undefined,
		receivers: [],
		tags: [],
		matchData: {
			titleOffsets: [],
			senderMatched: false,
			matchedReceiverIds: [],
			matchedTagIds: [],
			snippetOffsets: [],
			summaryOffsets: []
		},
		completionPercentage: 0,
		contributors: [],
		...overrides
	};
}

const baseProps = { items: [], canWrite: false, error: null, total: 0, q: '' };

// ─── Result count ─────────────────────────────────────────────────────────────

describe('DocumentList – result count', () => {
	it('shows result count when total > 0', async () => {
		render(DocumentList, { ...baseProps, items: [makeItem()], total: 1, q: 'test' });
		await expect.element(page.getByText('1 Dokumente')).toBeInTheDocument();
	});

	it('does not show result count when total is 0', async () => {
		render(DocumentList, { ...baseProps, total: 0, q: '' });
		await expect.element(page.getByText(/\d+ Dokumente/)).not.toBeInTheDocument();
	});
});

// ─── Empty state ──────────────────────────────────────────────────────────────

describe('DocumentList – empty state', () => {
	it('shows generic empty heading when q is empty', async () => {
		render(DocumentList, { ...baseProps });
		await expect.element(page.getByText(/Keine Dokumente/)).toBeInTheDocument();
	});

	it('shows search term in empty state when q is set', async () => {
		render(DocumentList, { ...baseProps, q: 'Urlaub' });
		await expect.element(page.getByText(/"Urlaub"/)).toBeInTheDocument();
	});
});

// ─── Year grouping ────────────────────────────────────────────────────────────

describe('DocumentList – year grouping', () => {
	it('groups documents by year into separate cards', async () => {
		const items = [
			makeItem({ id: '1', documentDate: '1923-04-12' }),
			makeItem({ id: '2', documentDate: '1965-08-03' })
		];
		render(DocumentList, { ...baseProps, items, total: 2 });
		const groupCards = page.getByTestId('group-card');
		await expect.element(groupCards.first()).toBeInTheDocument();
		await expect.element(groupCards.nth(1)).toBeInTheDocument();
	});

	it('uses undated label for items with no documentDate', async () => {
		const items = [makeItem({ id: '1', documentDate: undefined })];
		render(DocumentList, { ...baseProps, items, total: 1 });
		await expect.element(page.getByText('Undatiert')).toBeInTheDocument();
	});

	it('single year renders one group-card', async () => {
		const items = [
			makeItem({ id: '1', documentDate: '1938-01-01' }),
			makeItem({ id: '2', documentDate: '1938-06-15' })
		];
		render(DocumentList, { ...baseProps, items, total: 2 });
		const groupCards = page.getByTestId('group-card');
		await expect.element(groupCards.first()).toBeInTheDocument();
		await expect.element(groupCards.nth(1)).not.toBeInTheDocument();
	});
});

// ─── Sort fallback ────────────────────────────────────────────────────────────

describe('DocumentList – sort fallback', () => {
	it('falls back to year grouping when sort is not SENDER or RECEIVER', async () => {
		const items = [makeItem({ id: '1', documentDate: '2024-03-15' })];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'TITLE' });
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: '2024' }))
			.toBeInTheDocument();
	});
});

// ─── Sender grouping ─────────────────────────────────────────────────────────

describe('DocumentList – sender grouping', () => {
	it('groups by sender displayName when sort is SENDER', async () => {
		const items = [
			makeItem({
				id: '1',
				sender: {
					id: 's1',
					lastName: 'Mustermann',
					displayName: 'Max Mustermann',
					personType: 'PERSON',
					familyMember: false
				}
			}),
			makeItem({
				id: '2',
				sender: {
					id: 's2',
					lastName: 'Musterfrau',
					displayName: 'Anna Musterfrau',
					personType: 'PERSON',
					familyMember: false
				}
			})
		];
		render(DocumentList, { ...baseProps, items, total: 2, sort: 'SENDER' });
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: 'Max Mustermann' }))
			.toBeInTheDocument();
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: 'Anna Musterfrau' }))
			.toBeInTheDocument();
	});

	it('groups documents with the same sender into one card', async () => {
		const sender = {
			id: 's1',
			lastName: 'Mustermann',
			displayName: 'Max Mustermann',
			personType: 'PERSON' as const,
			familyMember: false
		};
		const items = [makeItem({ id: '1', sender }), makeItem({ id: '2', sender })];
		render(DocumentList, { ...baseProps, items, total: 2, sort: 'SENDER' });
		const cards = page.getByTestId('group-card');
		await expect.element(cards.first()).toBeInTheDocument();
		await expect.element(cards.nth(1)).not.toBeInTheDocument();
	});

	it('places items with no sender under fallback label', async () => {
		const items = [makeItem({ id: '1', sender: undefined })];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'SENDER' });
		await expect.element(page.getByText('Unbekannter Absender')).toBeInTheDocument();
	});
});

// ─── Receiver grouping ────────────────────────────────────────────────────────

describe('DocumentList – receiver grouping', () => {
	it('groups by receiver displayName when sort is RECEIVER', async () => {
		const items = [
			makeItem({
				id: '1',
				receivers: [
					{
						id: 'r1',
						lastName: 'Brandt',
						displayName: 'Felix Brandt',
						personType: 'PERSON',
						familyMember: false
					}
				]
			})
		];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' });
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: 'Felix Brandt' }))
			.toBeInTheDocument();
	});

	it('duplicates a document into each receiver group', async () => {
		const items = [
			makeItem({
				id: '1',
				title: 'Rundbriefchen',
				receivers: [
					{
						id: 'r1',
						lastName: 'Brandt',
						displayName: 'Felix Brandt',
						personType: 'PERSON',
						familyMember: false
					},
					{
						id: 'r2',
						lastName: 'Meier',
						displayName: 'Hans Meier',
						personType: 'PERSON',
						familyMember: false
					}
				]
			})
		];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' });
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: 'Felix Brandt' }))
			.toBeInTheDocument();
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: 'Hans Meier' }))
			.toBeInTheDocument();
		const cards = page.getByTestId('group-card');
		await expect.element(cards.nth(1)).toBeInTheDocument();
	});

	it('places items with no receivers under fallback label', async () => {
		const items = [makeItem({ id: '1', receivers: [] })];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' });
		await expect.element(page.getByText('Unbekannter Empfänger')).toBeInTheDocument();
	});
});

// ─── DocumentRow rendering (delegated) ───────────────────────────────────────

describe('DocumentList – DocumentRow delegation', () => {
	it('shows transcription snippet when matchData has one', async () => {
		const items = [
			makeItem({
				id: 'doc1',
				matchData: {
					transcriptionSnippet: 'Er schrieb einen langen Brief',
					titleOffsets: [],
					senderMatched: false,
					matchedReceiverIds: [],
					matchedTagIds: [],
					snippetOffsets: [],
					summaryOffsets: []
				}
			})
		];
		render(DocumentList, { ...baseProps, items, total: 1 });
		await expect.element(page.getByText('Er schrieb einen langen Brief')).toBeInTheDocument();
	});

	it('does not render snippet when matchData has no transcription snippet', async () => {
		const items = [makeItem({ id: 'doc1' })];
		render(DocumentList, { ...baseProps, items, total: 1 });
		await expect.element(page.getByTestId('search-snippet')).not.toBeInTheDocument();
	});

	it('renders mark for title highlight when titleOffsets present', async () => {
		const items = [
			makeItem({
				id: 'doc1',
				title: 'Brief an Anna',
				matchData: {
					titleOffsets: [{ start: 0, length: 5 }], // "Brief"
					senderMatched: false,
					matchedReceiverIds: [],
					matchedTagIds: [],
					snippetOffsets: [],
					summaryOffsets: []
				}
			})
		];
		render(DocumentList, { ...baseProps, items, total: 1 });
		const mark = page.getByRole('mark');
		await expect.element(mark).toBeInTheDocument();
		await expect.element(mark).toHaveTextContent('Brief');
	});
});

// ─── Undated badge in person-grouped modes (#668) ────────────────────────────

describe('DocumentList – undated badge in person grouping', () => {
	const sender = {
		id: 's1',
		lastName: 'Mustermann',
		displayName: 'Max Mustermann',
		personType: 'PERSON' as const,
		birthDatePrecision: 'UNKNOWN' as const,
		deathDatePrecision: 'UNKNOWN' as const,
		familyMember: false,
		provisional: false
	};
	const receiver = {
		id: 'r1',
		lastName: 'Brandt',
		displayName: 'Felix Brandt',
		personType: 'PERSON' as const,
		birthDatePrecision: 'UNKNOWN' as const,
		deathDatePrecision: 'UNKNOWN' as const,
		familyMember: false,
		provisional: false
	};

	it('shows the undated badge on a row under SENDER grouping', async () => {
		const items = [
			makeItem({ id: '1', documentDate: undefined, metaDatePrecision: 'UNKNOWN', sender })
		];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'SENDER' });
		await expect.element(page.getByTestId('undated-badge').first()).toBeInTheDocument();
	});

	it('shows the undated badge on a row under RECEIVER grouping', async () => {
		const items = [
			makeItem({
				id: '1',
				documentDate: undefined,
				metaDatePrecision: 'UNKNOWN',
				receivers: [receiver]
			})
		];
		render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' });
		await expect.element(page.getByTestId('undated-badge').first()).toBeInTheDocument();
	});

	it('keeps an undated letter under its sender, not in a synthetic undated sub-group', async () => {
		const items = [
			makeItem({ id: '1', documentDate: '1916-06-15', metaDatePrecision: 'DAY', sender }),
			makeItem({ id: '2', documentDate: undefined, metaDatePrecision: 'UNKNOWN', sender })
		];
		render(DocumentList, { ...baseProps, items, total: 2, sort: 'SENDER' });
		// One sender card; no "Undatiert" group header inside person-grouped mode.
		await expect
			.element(page.getByTestId('group-header').filter({ hasText: 'Max Mustermann' }))
			.toBeInTheDocument();
		await expect.element(page.getByText('Undatiert')).not.toBeInTheDocument();
		const cards = page.getByTestId('group-card');
		await expect.element(cards.nth(1)).not.toBeInTheDocument();
	});
});

// ─── Date-range / undated empty state (#668) ─────────────────────────────────

describe('DocumentList – range-excludes-undated empty state', () => {
	it('explains that a date range excludes undated documents when from/to active and no results', async () => {
		render(DocumentList, {
			...baseProps,
			items: [],
			total: 0,
			from: '1920-01-01',
			to: '1930-12-31'
		});
		await expect
			.element(page.getByText(/Datumsfilter schließt undatierte Dokumente aus/))
			.toBeInTheDocument();
	});

	it('shows the generic empty state when no date range is active', async () => {
		render(DocumentList, { ...baseProps, items: [], total: 0 });
		await expect.element(page.getByText(/Keine Dokumente/)).toBeInTheDocument();
		await expect
			.element(page.getByText(/Datumsfilter schließt undatierte Dokumente aus/))
			.not.toBeInTheDocument();
	});
});
