import { describe, it, expect, vi } from 'vitest';
import { SearchService } from './search.service';
import type { MessageSearchPort, SearchHit } from './message-search.port';
import type { ActorResolver, MessagePrincipal } from '../identity/actor.resolver';
import type { PrismaService } from '../prisma/prisma.service';

const principal: MessagePrincipal = { userId: 'u1', orgId: 'org', appId: 'app' };

function make(opts: { ready?: boolean; threadIds?: string[]; hits?: SearchHit[]; scope?: { id: string } | null } = {}) {
  const engine: MessageSearchPort = {
    ready: () => opts.ready ?? true,
    index: vi.fn(async () => undefined),
    remove: vi.fn(async () => undefined),
    search: vi.fn(async () => opts.hits ?? []),
  };
  const actors = {
    findScope: vi.fn(async () => (opts.scope === undefined ? { id: 'scope_1' } : opts.scope)),
    resolveActor: vi.fn(async () => ({ id: 'actor_1' })),
  } as unknown as ActorResolver;
  const prisma = {
    iiosThreadParticipant: { findMany: vi.fn(async () => (opts.threadIds ?? ['t1', 't2']).map((threadId) => ({ threadId }))) },
  } as unknown as PrismaService;
  return { svc: new SearchService(engine, prisma, actors), engine };
}

describe('SearchService (permission fence)', () => {
  it('searches only within the caller scope + their thread ids', async () => {
    const { svc, engine } = make({ threadIds: ['t1', 't2', 't3'], hits: [{ id: 'i1', threadId: 't2', text: 'hello world', subject: 'General', source: 'crm-messenger', at: 1, snippet: '<em>hello</em>' }] });
    const res = await svc.search(principal, { query: 'hello' });
    expect(res[0]!.id).toBe('i1');
    expect(engine.search).toHaveBeenCalledWith('scope_1', ['t1', 't2', 't3'], 'hello', 20);
  });

  it('returns empty (no engine hit) for a blank query', async () => {
    const { svc, engine } = make();
    expect(await svc.search(principal, { query: '   ' })).toEqual([]);
    expect(engine.search).not.toHaveBeenCalled();
  });

  it('returns empty when the caller belongs to no threads', async () => {
    const { svc, engine } = make({ threadIds: [] });
    expect(await svc.search(principal, { query: 'hello' })).toEqual([]);
    expect(engine.search).not.toHaveBeenCalled();
  });

  it('returns empty when search is not configured (no-op engine)', async () => {
    const { svc, engine } = make({ ready: false });
    expect(await svc.search(principal, { query: 'hello' })).toEqual([]);
    expect(engine.search).not.toHaveBeenCalled();
  });

  it('caps the limit at 50', async () => {
    const { svc, engine } = make();
    await svc.search(principal, { query: 'x', limit: 999 });
    expect((engine.search as ReturnType<typeof vi.fn>).mock.calls[0]![3]).toBe(50);
  });
});
