/**
 * S-series: global search (`search_in_collective`) — filtering, RLS isolation,
 * trash exclusion (RN-08), and by-type / by-creator / by-date-range filters.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createClientAs, createAdminClient } from '../src/supabase-clients.js';
import {
	ANA_ID,
	BORJA_ID,
	EVA_ID,
	COLLECTIVE_ID,
	SEED_LIST_ID
} from '../src/seed-constants.js';
import { closePool } from '../src/db-helpers.js';

const admin = createAdminClient();

// Fixtures use a shared lexeme-safe token that appears as a standalone word in
// every fixture (`simple` tsvector tokenises on whitespace — substrings don't match).
const UNIQUE = `needle${Date.now()}`;
const ITEM_TOKEN = `itm${Date.now()}`;
const TASK_TOKEN = `tsk${Date.now()}`;
const NOTE_TOKEN = `nt${Date.now()}`;
const TRASH_TOKEN = `trash${Date.now()}`;

const cleanup: { itemIds: string[]; taskListIds: string[]; noteIds: string[] } = {
	itemIds: [],
	taskListIds: [],
	noteIds: []
};

let taskListId: string;

beforeAll(async () => {
	// Shopping item in the seed list
	const { data: item } = await admin
		.from('shopping_items')
		.insert({
			list_id: SEED_LIST_ID,
			name: `${UNIQUE} ${ITEM_TOKEN}`,
			sort_order: 990,
			created_by: ANA_ID
		})
		.select('id')
		.single();
	cleanup.itemIds.push(item!.id);

	// Task list + task
	const { data: tl } = await admin
		.from('task_lists')
		.insert({ collective_id: COLLECTIVE_ID, name: 'Search test list', created_by: ANA_ID })
		.select('id')
		.single();
	taskListId = tl!.id;
	cleanup.taskListIds.push(taskListId);

	await admin
		.from('tasks')
		.insert({ list_id: taskListId, title: `${UNIQUE} ${TASK_TOKEN}`, created_by: BORJA_ID });

	// Note (active)
	const { data: note } = await admin
		.from('notes')
		.insert({
			collective_id: COLLECTIVE_ID,
			title: `${UNIQUE} ${NOTE_TOKEN}`,
			content: 'recipe ingredients',
			created_by: ANA_ID,
			updated_by: ANA_ID
		})
		.select('id')
		.single();
	cleanup.noteIds.push(note!.id);

	// Note in trash — must NOT appear in search (RN-08)
	const { data: trashedNote } = await admin
		.from('notes')
		.insert({
			collective_id: COLLECTIVE_ID,
			content: `secret ${TRASH_TOKEN} content`,
			created_by: ANA_ID,
			updated_by: ANA_ID,
			deleted_at: new Date().toISOString()
		})
		.select('id')
		.single();
	cleanup.noteIds.push(trashedNote!.id);
});

afterAll(async () => {
	if (cleanup.itemIds.length) {
		await admin.from('shopping_items').delete().in('id', cleanup.itemIds);
	}
	if (cleanup.noteIds.length) {
		await admin.from('notes').delete().in('id', cleanup.noteIds);
	}
	if (cleanup.taskListIds.length) {
		await admin.from('task_lists').delete().in('id', cleanup.taskListIds);
	}
	await closePool();
});

type SearchRow = {
	id: string;
	result_type: 'shopping_item' | 'task' | 'note';
	title: string | null;
	snippet: string;
	collective_id: string;
	list_id: string | null;
	created_by: string;
	created_at: string;
	rank: number;
};

async function search(
	client: Awaited<ReturnType<typeof createClientAs>>,
	query: string,
	opts: { types?: SearchRow['result_type'][]; creator?: string; from?: string; to?: string } = {}
): Promise<SearchRow[]> {
	const { data, error } = await client.rpc('search_in_collective' as never, {
		p_collective_id: COLLECTIVE_ID,
		p_query: query,
		p_types: opts.types ?? null,
		p_creator: opts.creator ?? null,
		p_from: opts.from ?? null,
		p_to: opts.to ?? null
	} as never);
	if (error) throw error;
	return (data ?? []) as SearchRow[];
}

describe('search_in_collective — base queries', () => {
	it('S-01: member (Borja) finds shopping items by name', async () => {
		const borja = await createClientAs(BORJA_ID);
		const rows = await search(borja, ITEM_TOKEN);
		expect(rows.length).toBeGreaterThan(0);
		expect(rows.some((r) => r.result_type === 'shopping_item' && r.title?.includes(ITEM_TOKEN))).toBe(true);
	});

	it('S-01b: member finds tasks by title', async () => {
		const borja = await createClientAs(BORJA_ID);
		const rows = await search(borja, TASK_TOKEN);
		expect(rows.some((r) => r.result_type === 'task' && r.title?.includes(TASK_TOKEN))).toBe(true);
	});

	it('S-01c: member finds notes by title or content', async () => {
		const borja = await createClientAs(BORJA_ID);
		const rows = await search(borja, NOTE_TOKEN);
		expect(rows.some((r) => r.result_type === 'note' && (r.title?.includes(NOTE_TOKEN) ?? false))).toBe(true);
	});
});

describe('search_in_collective — RLS isolation', () => {
	it('S-02: Eva (non-member of main collective) sees zero rows', async () => {
		const eva = await createClientAs(EVA_ID);
		const rows = await search(eva, ITEM_TOKEN);
		expect(rows).toHaveLength(0);
	});

	it('S-02b: passing a stranger collective_id yields zero rows (RLS blocks the joins)', async () => {
		// Ana querying for a completely unrelated UUID
		const ana = await createClientAs(ANA_ID);
		const { data } = await ana.rpc('search_in_collective' as never, {
			p_collective_id: '00000000-0000-0000-0000-000000000000',
			p_query: ITEM_TOKEN,
			p_types: null,
			p_creator: null,
			p_from: null,
			p_to: null
		} as never);
		expect(data ?? []).toHaveLength(0);
	});
});

describe('search_in_collective — trash exclusion (RN-08)', () => {
	it('S-03: a soft-deleted note does not appear in results', async () => {
		const borja = await createClientAs(BORJA_ID);
		const rows = await search(borja, TRASH_TOKEN);
		expect(rows).toHaveLength(0);
	});
});

describe('search_in_collective — filters', () => {
	it('S-04a: type filter restricts to shopping_item only', async () => {
		const borja = await createClientAs(BORJA_ID);
		const rows = await search(borja, UNIQUE, { types: ['shopping_item'] });
		expect(rows.length).toBeGreaterThan(0);
		expect(rows.every((r) => r.result_type === 'shopping_item')).toBe(true);
	});

	it('S-04b: creator filter restricts to a single user', async () => {
		const borja = await createClientAs(BORJA_ID);
		// Only the task is by BORJA; item + note are by ANA
		const rows = await search(borja, UNIQUE, { creator: BORJA_ID });
		expect(rows.length).toBeGreaterThan(0);
		expect(rows.every((r) => r.created_by === BORJA_ID)).toBe(true);
	});

	it('S-04c: date range in the far past returns no rows', async () => {
		const borja = await createClientAs(BORJA_ID);
		const rows = await search(borja, UNIQUE, {
			from: '1970-01-01T00:00:00Z',
			to: '1970-12-31T23:59:59Z'
		});
		expect(rows).toHaveLength(0);
	});

	it('S-04d: date range covering now returns at least one match', async () => {
		const borja = await createClientAs(BORJA_ID);
		const from = new Date(Date.now() - 1000 * 60 * 60).toISOString();
		const rows = await search(borja, UNIQUE, { from });
		expect(rows.length).toBeGreaterThan(0);
	});
});
