import { describe, expect, it, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import SearchFilterBar from './SearchFilterBar.svelte';

afterEach(() => cleanup());

const defaultProps = {
	onSearch: vi.fn()
};

describe('SearchFilterBar – sort controls', () => {
	it('renders a sort select when sort and dir are provided', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc' });
		const select = page.getByRole('combobox');
		await expect.element(select).toBeInTheDocument();
	});

	it('reflects the active sort value in the select', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'TITLE', dir: 'asc' });
		const select = page.getByRole('combobox');
		await expect.element(select).toHaveValue('TITLE');
	});

	it('renders direction toggle button', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'asc' });
		const btn = page.getByRole('button', { name: /sortieren/i });
		await expect.element(btn).toBeInTheDocument();
	});
});

describe('SearchFilterBar – loading spinner', () => {
	it('shows search icon when isLoading is false', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', isLoading: false });
		const spinner = page.getByRole('status');
		await expect.element(spinner).not.toBeInTheDocument();
	});

	it('shows spinner and hides search icon when isLoading is true', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', isLoading: true });
		const spinner = page.getByRole('status');
		await expect.element(spinner).toBeInTheDocument();
	});
});

describe('SearchFilterBar – AND/OR tag operator toggle', () => {
	async function openAdvanced() {
		const filterBtn = page.getByRole('button', { name: 'Filter', exact: true });
		await filterBtn.click();
		// Wait for slide transition to finish before interacting with contents —
		// clicking during the transition triggers track_reactivity_loss in Svelte 5 async.js
		// (same guard as the undated-only describe below; this block flaked in CI run 2208).
		await expect.element(page.getByTestId('undated-only-toggle')).toBeVisible();
	}

	it('hides AND/OR toggle when fewer than 2 tags are selected', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
		);
		render(SearchFilterBar, {
			...defaultProps,
			sort: 'DATE',
			dir: 'desc',
			tagNames: [{ name: 'Tag1' }]
		});
		await openAdvanced();
		await expect.element(page.getByTestId('operator-and')).not.toBeInTheDocument();
		vi.unstubAllGlobals();
	});

	it('shows AND/OR toggle when 2+ tags are selected', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
		);
		render(SearchFilterBar, {
			...defaultProps,
			sort: 'DATE',
			dir: 'desc',
			tagNames: [{ name: 'Tag1' }, { name: 'Tag2' }]
		});
		await openAdvanced();
		const toggle = page.getByTestId('and-or-toggle');
		await expect.element(toggle).toBeInTheDocument();
		await expect.element(toggle.getByTestId('operator-and')).toBeInTheDocument();
		await expect.element(toggle.getByTestId('operator-or')).toBeInTheDocument();
		vi.unstubAllGlobals();
	});

	it('calls onSearch when operator is toggled', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
		);
		const onSearch = vi.fn();
		render(SearchFilterBar, {
			...defaultProps,
			onSearch,
			sort: 'DATE',
			dir: 'desc',
			tagNames: [{ name: 'Tag1' }, { name: 'Tag2' }]
		});
		await openAdvanced();
		const toggle = page.getByTestId('and-or-toggle');
		await toggle.getByTestId('operator-or').click();
		await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0);
		vi.unstubAllGlobals();
	});

	it('calls onSearchImmediate instead of onSearch when operator is toggled and onSearchImmediate is provided', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
		);
		const onSearch = vi.fn();
		const onSearchImmediate = vi.fn();
		render(SearchFilterBar, {
			...defaultProps,
			onSearch,
			onSearchImmediate,
			sort: 'DATE',
			dir: 'desc',
			tagNames: [{ name: 'Tag1' }, { name: 'Tag2' }]
		});
		await openAdvanced();
		const toggle = page.getByTestId('and-or-toggle');
		await toggle.getByTestId('operator-or').click();
		await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0);
		expect(onSearch).not.toHaveBeenCalled();
		vi.unstubAllGlobals();
	});
});

describe('SearchFilterBar – undated-only toggle (#668)', () => {
	async function openAdvanced() {
		const filterBtn = page.getByRole('button', { name: 'Filter', exact: true });
		await filterBtn.click();
		// Wait for slide transition to finish before interacting with contents —
		// clicking during the transition triggers track_reactivity_loss in Svelte 5 async.js
		await expect.element(page.getByTestId('undated-only-toggle')).toBeVisible();
	}

	it('renders the "Nur undatierte" toggle in the advanced row', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc' });
		await openAdvanced();
		await expect.element(page.getByTestId('undated-only-toggle')).toBeInTheDocument();
	});

	it('reflects the active undated state via aria-pressed', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undated: true });
		await openAdvanced();
		await expect
			.element(page.getByTestId('undated-only-toggle'))
			.toHaveAttribute('aria-pressed', 'true');
	});

	it('calls onSearchImmediate when the undated toggle is clicked', async () => {
		const onSearch = vi.fn();
		const onSearchImmediate = vi.fn();
		render(SearchFilterBar, {
			...defaultProps,
			onSearch,
			onSearchImmediate,
			sort: 'DATE',
			dir: 'desc'
		});
		await openAdvanced();
		await page.getByTestId('undated-only-toggle').click();
		await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0);
	});

	it('shows the global undated count chip when undatedCount > 0', async () => {
		// The count is the backend's global filtered total (#668), passed straight
		// through — the chip must render it verbatim, not a page-derived number.
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undatedCount: 42 });
		await openAdvanced();
		await expect.element(page.getByTestId('undated-count')).toHaveTextContent('42');
	});

	it('hides the undated count chip when undatedCount is 0', async () => {
		render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undatedCount: 0 });
		await openAdvanced();
		await expect.element(page.getByTestId('undated-count')).not.toBeInTheDocument();
	});
});

describe('SearchFilterBar – tagQ live filter', () => {
	it('calls onSearch when tag text changes in TagInput', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
		);
		const onSearch = vi.fn();
		render(SearchFilterBar, { ...defaultProps, onSearch, sort: 'DATE', dir: 'desc' });
		// TagInput is only visible in advanced panel — open it first
		const filterBtn = page.getByRole('button', { name: 'Filter', exact: true });
		await filterBtn.click();
		const tagTextbox = page.getByPlaceholder('Nach Schlagworten filtern...');
		await tagTextbox.fill('fam');
		await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0);
		vi.unstubAllGlobals();
	});
});
