import { beforeEach, describe, expect, it, vi } from 'vitest'

const cmsApiMock = vi.fn()
vi.mock('../../lib/api.js', () => ({
  cmsApi: (...args: unknown[]) => cmsApiMock(...args),
  getApiBase: () => '/api/cms',
}))

const { detectMentionContext, insertMentionToken, searchMentionUsers } =
  await import('../../lib/mention-picker.js')

beforeEach(() => {
  cmsApiMock.mockReset()
})

describe('detectMentionContext', () => {
  it('returns null when there is no @ before the caret', () => {
    expect(detectMentionContext('hello world', 5)).toBeNull()
  })

  it('matches a fresh @ at the start of the field', () => {
    expect(detectMentionContext('@ali', 4)).toEqual({ start: 0, end: 4, query: 'ali' })
  })

  it('matches @ after a leading space', () => {
    expect(detectMentionContext('hey @bob', 8)).toEqual({ start: 4, end: 8, query: 'bob' })
  })

  it('opens immediately with an empty query the moment @ is typed', () => {
    expect(detectMentionContext('@', 1)).toEqual({ start: 0, end: 1, query: '' })
  })

  it('does NOT open mid-word for email-like input (`foo@bar`)', () => {
    expect(detectMentionContext('foo@bar', 7)).toBeNull()
  })

  it('terminates on a whitespace character between @ and caret', () => {
    expect(detectMentionContext('@ali ce', 7)).toBeNull()
  })

  it('terminates on a non-mention punctuation character', () => {
    expect(detectMentionContext('@ali.ce', 7)).toBeNull()
  })

  it('supports underscores and hyphens (id-shape charset)', () => {
    expect(detectMentionContext('@user_id-42', 11)).toEqual({
      start: 0,
      end: 11,
      query: 'user_id-42',
    })
  })

  it('matches when the caret is mid-query (not at end of value)', () => {
    // value: "@ali rest", caret after the `i`
    expect(detectMentionContext('@ali rest', 4)).toEqual({ start: 0, end: 4, query: 'ali' })
  })

  it('handles newline as a boundary', () => {
    expect(detectMentionContext('line\n@bob', 9)).toEqual({ start: 5, end: 9, query: 'bob' })
  })

  it('returns null when the caret is at offset 0', () => {
    expect(detectMentionContext('@ali', 0)).toBeNull()
  })

  it('returns null when the caret is past the end of value', () => {
    expect(detectMentionContext('@ali', 99)).toBeNull()
  })

  it('rejects queries that exceed the 64-char server cap', () => {
    const long = 'a'.repeat(65)
    expect(detectMentionContext(`@${long}`, 1 + long.length)).toBeNull()
  })

  it('returns null for non-string input (defensive)', () => {
    // @ts-expect-error — runtime guard against caller misuse
    expect(detectMentionContext(undefined, 0)).toBeNull()
  })
})

describe('insertMentionToken', () => {
  it('replaces the @<query> slice with @<userId> + trailing space', () => {
    const ctx = { start: 0, end: 4, query: 'ali' }
    expect(insertMentionToken('@ali rest', ctx, 'user_123')).toEqual({
      value: '@user_123 rest',
      caret: '@user_123 '.length,
    })
  })

  it('does not duplicate a trailing space when one is already present', () => {
    const ctx = { start: 0, end: 4, query: 'ali' }
    const out = insertMentionToken('@ali extra', ctx, 'u1')
    expect(out.value).toBe('@u1 extra')
    expect(out.value.slice(out.caret)).toBe('extra')
  })

  it('preserves text before the @ token', () => {
    const ctx = { start: 4, end: 8, query: 'bob' }
    expect(insertMentionToken('hey @bob', ctx, 'u_99').value).toBe('hey @u_99 ')
  })

  it('places the caret immediately past the inserted token + space', () => {
    const ctx = { start: 0, end: 1, query: '' }
    const out = insertMentionToken('@', ctx, 'u_xyz')
    expect(out.value).toBe('@u_xyz ')
    expect(out.caret).toBe(out.value.length)
  })
})

describe('searchMentionUsers', () => {
  it('short-circuits an empty query without hitting the wire', async () => {
    const out = await searchMentionUsers('')
    expect(out).toEqual({ ok: true, result: [] })
    expect(cmsApiMock).not.toHaveBeenCalled()
  })

  it('short-circuits a whitespace-only query', async () => {
    const out = await searchMentionUsers('   ')
    expect(out).toEqual({ ok: true, result: [] })
    expect(cmsApiMock).not.toHaveBeenCalled()
  })

  it('URL-encodes the query and forwards the result array', async () => {
    cmsApiMock.mockResolvedValueOnce({
      data: [{ id: 'u-1', name: 'Alice', email: 'a@x.com' }],
      status: 200,
    })
    const out = await searchMentionUsers('a li ce')
    expect(cmsApiMock).toHaveBeenCalledWith(`/users/search?q=${encodeURIComponent('a li ce')}`, {
      method: 'GET',
    })
    expect(out.ok).toBe(true)
    if (!out.ok) return
    expect(out.result.map((u) => u.id)).toEqual(['u-1'])
  })

  it('propagates server errors as { ok: false }', async () => {
    cmsApiMock.mockResolvedValueOnce({ error: 'CSRF', status: 403 })
    const out = await searchMentionUsers('ali')
    expect(out.ok).toBe(false)
    if (out.ok) return
    expect(out.error).toBe('CSRF')
    expect(out.status).toBe(403)
  })
})
