import { describe, expect, it } from 'vitest'
import { searchEntities, type Hit, type SearchRegistry, type SearchWindow } from '@platform-modules/search'
import { buildSafeHeadlineHtml, HEADLINE_OPTS } from '@platform-modules/util/fts/safe-headline'
import { sanitizeTsquery } from '@platform-modules/util/fts/sanitize-tsquery'

type HarnessCtx = { tenantId: string; role: string }

function hit(entityType: string, id: string, rank: number): Hit {
  return { entityType, id, rank }
}

describe('search consumer fixture (Gate 3)', () => {
  it('merges cross-entity providers grouped by total desc', async () => {
    const registry: SearchRegistry<HarnessCtx> = {
      posts: async () => ({ hits: [hit('posts', 'p1', 0.9)], total: 12 }),
      vendors: async () => ({ hits: [hit('vendors', 'v1', 0.7)], total: 4 }),
      deals: async () => ({ hits: [hit('deals', 'd1', 0.6)], total: 7 }),
    }

    const result = await searchEntities(registry, {
      query: 'widget',
      ctx: { tenantId: 't-1', role: 'member' },
      limit: 5,
    })

    expect(result.nextCursor).toBeNull()
    expect(result.groups.map((g) => g.entityType)).toEqual(['posts', 'deals', 'vendors'])
  })

  it('neutralizes tsquery metacharacters via the util sanitizer', () => {
    const raw = "&|!():*<>'; DROP TABLE users;--"
    expect(() => sanitizeTsquery(raw)).not.toThrow()
    expect(sanitizeTsquery(raw)).not.toMatch(/['"|!()]/)
  })

  it('round-trips single-type cursor pagination', async () => {
    const all = Array.from({ length: 4 }, (_, i) => hit('posts', `p${i}`, 1))

    const registry: SearchRegistry<HarnessCtx> = {
      posts: async (_q, _ctx, window) => ({
        hits: all.slice(window.offset, window.offset + window.limit),
        total: all.length,
      }),
    }

    const page1 = await searchEntities(registry, {
      query: 'alpha',
      ctx: { tenantId: 't-1', role: 'member' },
      entityTypes: ['posts'],
      limit: 2,
    })

    expect(page1.nextCursor).not.toBeNull()

    const page2 = await searchEntities(registry, {
      query: 'alpha',
      ctx: { tenantId: 't-1', role: 'member' },
      entityTypes: ['posts'],
      limit: 2,
      cursor: page1.nextCursor,
    })

    expect(page2.groups[0]?.hits.map((h) => h.id)).toEqual(['p2', 'p3'])
    expect(page2.nextCursor).toBeNull()
  })

  it('forwards ctx and pushed-down window to providers intact', async () => {
    const ctx = { tenantId: 'tenant-forward', role: 'admin' }
    let seenCtx: HarnessCtx | undefined
    let seenWindow: SearchWindow | undefined

    const registry: SearchRegistry<HarnessCtx> = {
      posts: async (_q, forwardedCtx, window) => {
        seenCtx = forwardedCtx
        seenWindow = window
        return { hits: [hit('posts', 'p1', 1)], total: 1 }
      },
    }

    await searchEntities(registry, {
      query: 'needle',
      ctx,
      entityTypes: ['posts'],
      limit: 7,
    })

    expect(seenCtx).toBe(ctx)
    expect(seenWindow).toEqual({ limit: 7, offset: 0 })
  })

  it('escapes script terms in safe headline HTML', () => {
    const raw = `${HEADLINE_OPTS.StartSel}<script>alert(1)</script>${HEADLINE_OPTS.StopSel}`
    const html = buildSafeHeadlineHtml(raw)
    expect(html).toContain('&lt;script&gt;alert(1)&lt;/script&gt;')
    expect(html).not.toContain('<script>')
    expect(html).toContain('<mark>')
  })
})
