import { getPayload, Payload } from 'payload'
import config from '@/payload.config'
import { describe, it, beforeAll, afterAll, expect } from 'vitest'
import {
  searchPosts,
  getSearchSuggestions,
  getPostsByCategory,
  searchCache,
  suggestionCache,
  extractTextFromLexical,
} from '@/lib/search'
import { searchLimiter } from '@/lib/security'

let payload: Payload

// Test data IDs for cleanup
const testIds: { posts: number[]; categories: number[]; tenants: number[] } = {
  posts: [],
  categories: [],
  tenants: [],
}

describe('Search Library', () => {
  beforeAll(async () => {
    const payloadConfig = await config
    payload = await getPayload({ config: payloadConfig })

    // Clear caches before tests
    searchCache.clear()
    suggestionCache.clear()
  })

  afterAll(async () => {
    // Cleanup test data
    for (const postId of testIds.posts) {
      try {
        await payload.delete({ collection: 'posts', id: postId })
      } catch {
        // Ignore cleanup errors
      }
    }
    for (const catId of testIds.categories) {
      try {
        await payload.delete({ collection: 'categories', id: catId })
      } catch {
        // Ignore cleanup errors
      }
    }
  })

  describe('extractTextFromLexical', () => {
    it('extracts text from Lexical JSON structure', () => {
      const lexicalContent = {
        root: {
          children: [
            {
              type: 'paragraph',
              children: [{ type: 'text', text: 'Hello' }, { type: 'text', text: ' World' }],
            },
            {
              type: 'paragraph',
              children: [{ type: 'text', text: 'Another paragraph' }],
            },
          ],
        },
      }

      const result = extractTextFromLexical(lexicalContent)
      expect(result).toContain('Hello')
      expect(result).toContain('World')
      expect(result).toContain('Another paragraph')
    })

    it('returns empty string for null/undefined content', () => {
      expect(extractTextFromLexical(null)).toBe('')
      expect(extractTextFromLexical(undefined)).toBe('')
      expect(extractTextFromLexical({})).toBe('')
    })
  })

  describe('searchLimiter (central rate limiter)', () => {
    it('allows requests within limit', async () => {
      const testIp = `test-${Date.now()}-1`
      const result = await searchLimiter.check(testIp)

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(29) // 30 - 1
    })

    it('blocks requests exceeding limit', async () => {
      const testIp = `test-${Date.now()}-2`

      // Use up all requests
      for (let i = 0; i < 30; i++) {
        await searchLimiter.check(testIp)
      }

      const result = await searchLimiter.check(testIp)
      expect(result.allowed).toBe(false)
      expect(result.remaining).toBe(0)
      expect(result.retryAfter).toBeGreaterThan(0)
    })
  })

  describe('searchPosts', () => {
    it('returns empty results for non-matching query', async () => {
      const result = await searchPosts(payload, {
        query: 'xyznonexistent12345',
        limit: 10,
        offset: 0,
      })

      expect(result.results).toEqual([])
      expect(result.total).toBe(0)
      expect(result.query).toBe('xyznonexistent12345')
    })

    it('respects limit parameter', async () => {
      const result = await searchPosts(payload, {
        query: '',
        limit: 5,
        offset: 0,
      })

      expect(result.pagination.limit).toBe(5)
      expect(result.results.length).toBeLessThanOrEqual(5)
    })

    it('caches search results', async () => {
      // Clear cache first
      searchCache.clear()

      const params = { query: 'test-cache', limit: 10, offset: 0 }

      // First call
      const result1 = await searchPosts(payload, params)

      // Second call should use cache
      const result2 = await searchPosts(payload, params)

      expect(result1).toEqual(result2)
    })
  })

  describe('getSearchSuggestions', () => {
    it('returns empty array for short query', async () => {
      const result = await getSearchSuggestions(payload, {
        query: 'a',
        limit: 5,
      })

      expect(result).toEqual([])
    })

    it('respects limit parameter', async () => {
      const result = await getSearchSuggestions(payload, {
        query: 'test',
        limit: 3,
      })

      expect(result.length).toBeLessThanOrEqual(3)
    })
  })

  describe('getPostsByCategory', () => {
    it('returns paginated results', async () => {
      const result = await getPostsByCategory(payload, {
        page: 1,
        limit: 10,
      })

      expect(result).toHaveProperty('docs')
      expect(result).toHaveProperty('totalDocs')
      expect(result).toHaveProperty('page')
      expect(result).toHaveProperty('totalPages')
      expect(result).toHaveProperty('hasNextPage')
      expect(result).toHaveProperty('hasPrevPage')
    })

    it('filters by type', async () => {
      const result = await getPostsByCategory(payload, {
        type: 'blog',
        page: 1,
        limit: 10,
      })

      // All returned posts should be of type 'blog' (or empty if none exist)
      for (const post of result.docs) {
        const postWithType = post as typeof post & { type?: string }
        if (postWithType.type) {
          expect(postWithType.type).toBe('blog')
        }
      }
    })
  })
})

describe('Search API Integration', () => {
  let testCategoryId: number | null = null
  let testPostId: number | null = null
  let testTenantId: number | null = null

  beforeAll(async () => {
    const payloadConfig = await config
    payload = await getPayload({ config: payloadConfig })

    // Get existing tenant (required by multi-tenant plugin)
    try {
      const tenants = await payload.find({
        collection: 'tenants',
        limit: 1,
      })
      if (tenants.docs.length > 0) {
        testTenantId = tenants.docs[0].id
      }
    } catch {
      // No tenants available
    }

    if (!testTenantId) {
      console.warn('No tenant available for integration tests')
      return
    }

    // Create test category
    try {
      const category = await payload.create({
        collection: 'categories',
        data: {
          name: 'Test Search Category',
          slug: `test-search-category-${Date.now()}`,
          tenant: testTenantId,
        },
      })
      testCategoryId = category.id
      testIds.categories.push(category.id)
    } catch {
      // Category might already exist
    }

    // Create test post
    try {
      const post = await payload.create({
        collection: 'posts',
        draft: false,
        data: {
          title: 'Searchable Test Post Title',
          slug: `searchable-test-post-${Date.now()}`,
          excerpt: 'This is a searchable excerpt for testing',
          type: 'blog',
          status: 'published',
          publishedAt: new Date().toISOString(),
          tenant: testTenantId,
          content: {
            root: {
              type: 'root',
              children: [
                {
                  type: 'paragraph',
                  children: [{ type: 'text', text: 'Test content for search' }],
                  direction: null,
                  format: '',
                  indent: 0,
                  version: 1,
                },
              ],
              direction: null,
              format: '',
              indent: 0,
              version: 1,
            },
          },
          ...(testCategoryId ? { categories: [testCategoryId] } : {}),
        },
      })
      testPostId = post.id
      testIds.posts.push(post.id)
    } catch (error) {
      console.error('Failed to create test post:', error)
    }
  })

  it('finds posts by title search', async () => {
    if (!testPostId) {
      console.warn('Test post not created, skipping test')
      return
    }

    // Clear cache to ensure fresh search
    searchCache.clear()

    const result = await searchPosts(payload, {
      query: 'Searchable Test Post',
      limit: 10,
      offset: 0,
    })

    expect(result.results.length).toBeGreaterThan(0)
    expect(result.results.some((r) => r.id === testPostId)).toBe(true)
  })

  it('finds posts by excerpt search', async () => {
    if (!testPostId) {
      console.warn('Test post not created, skipping test')
      return
    }

    searchCache.clear()

    const result = await searchPosts(payload, {
      query: 'searchable excerpt',
      limit: 10,
      offset: 0,
    })

    expect(result.results.length).toBeGreaterThan(0)
    expect(result.results.some((r) => r.id === testPostId)).toBe(true)
  })

  it('returns suggestions for valid prefix', async () => {
    if (!testPostId) {
      console.warn('Test post not created, skipping test')
      return
    }

    suggestionCache.clear()

    const suggestions = await getSearchSuggestions(payload, {
      query: 'Searchable',
      limit: 5,
    })

    expect(suggestions.length).toBeGreaterThan(0)
    expect(suggestions.some((s) => s.title.includes('Searchable'))).toBe(true)
  })
})
