// Tests for the cache adapter layer: MemoryAdapter, RedisAdapter, and getCacheAdapter selector.

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { MemoryAdapter } from '@/lib/cache/memory'
import { RedisAdapter } from '@/lib/cache/redis'

// ─── MemoryAdapter ────────────────────────────────────────────────────────────

describe('MemoryAdapter', () => {
  let adapter: MemoryAdapter

  beforeEach(() => {
    adapter = new MemoryAdapter()
  })

  it('returns null on cache miss', async () => {
    expect(await adapter.get('missing')).toBeNull()
  })

  it('stores and retrieves a plain object', async () => {
    await adapter.set('key', { foo: 'bar' }, 60_000)
    expect(await adapter.get<{ foo: string }>('key')).toEqual({ foo: 'bar' })
  })

  it('stores and retrieves a primitive value', async () => {
    await adapter.set('num', 42, 60_000)
    expect(await adapter.get<number>('num')).toBe(42)
  })

  it('returns null when TTL has expired (negative TTL)', async () => {
    // Set with negative TTL — expires immediately in the past
    await adapter.set('expired', 'value', -1)
    expect(await adapter.get('expired')).toBeNull()
  })

  it('deletes a key explicitly', async () => {
    await adapter.set('key', 'value', 60_000)
    await adapter.delete('key')
    expect(await adapter.get('key')).toBeNull()
  })

  it('delete is a no-op for missing keys', async () => {
    await expect(adapter.delete('nonexistent')).resolves.toBeUndefined()
  })

  it('clear removes all stored entries', async () => {
    await adapter.set('a', 1, 60_000)
    await adapter.set('b', 2, 60_000)
    adapter.clear()
    expect(await adapter.get('a')).toBeNull()
    expect(await adapter.get('b')).toBeNull()
  })

  it('different keys do not interfere with each other', async () => {
    await adapter.set('x', 'hello', 60_000)
    await adapter.set('y', 'world', 60_000)
    expect(await adapter.get('x')).toBe('hello')
    expect(await adapter.get('y')).toBe('world')
    await adapter.delete('x')
    expect(await adapter.get('x')).toBeNull()
    expect(await adapter.get('y')).toBe('world')
  })

  it('overwriting a key replaces the value', async () => {
    await adapter.set('key', 'first', 60_000)
    await adapter.set('key', 'second', 60_000)
    expect(await adapter.get('key')).toBe('second')
  })
})

// ─── RedisAdapter ─────────────────────────────────────────────────────────────

describe('RedisAdapter', () => {
  const BASE_URL = 'https://redis.example.upstash.io'
  const TOKEN = 'test-redis-token'
  let adapter: RedisAdapter

  beforeEach(() => {
    adapter = new RedisAdapter(BASE_URL, TOKEN)
    vi.stubGlobal('fetch', vi.fn())
  })

  afterEach(() => {
    vi.unstubAllGlobals()
  })

  function mockFetchOk(result: unknown) {
    vi.mocked(fetch).mockResolvedValueOnce(
      new Response(JSON.stringify({ result }), { status: 200 }),
    )
  }

  it('sends GET command and returns deserialized value', async () => {
    mockFetchOk(JSON.stringify({ foo: 'bar' }))

    const result = await adapter.get<{ foo: string }>('mykey')

    expect(result).toEqual({ foo: 'bar' })
    expect(fetch).toHaveBeenCalledWith(BASE_URL, expect.objectContaining({
      method: 'POST',
      body: JSON.stringify(['GET', 'mykey']),
    }))
  })

  it('returns null on cache miss (null result from Redis)', async () => {
    mockFetchOk(null)
    expect(await adapter.get('missing')).toBeNull()
  })

  it('returns null on non-OK response', async () => {
    vi.mocked(fetch).mockResolvedValueOnce(
      new Response('Unauthorized', { status: 401 }),
    )
    expect(await adapter.get('key')).toBeNull()
  })

  it('returns null when fetch throws (network error)', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('ECONNREFUSED'))
    expect(await adapter.get('key')).toBeNull()
  })

  it('returns null when stored value is not valid JSON', async () => {
    mockFetchOk('not-json-{{{')
    expect(await adapter.get('key')).toBeNull()
  })

  it('sends SET command with JSON-serialized value and PX TTL', async () => {
    mockFetchOk('OK')

    await adapter.set('mykey', { value: 42 }, 300_000)

    expect(fetch).toHaveBeenCalledWith(BASE_URL, expect.objectContaining({
      method: 'POST',
      body: JSON.stringify(['SET', 'mykey', JSON.stringify({ value: 42 }), 'PX', 300_000]),
    }))
  })

  it('sends DEL command on delete', async () => {
    mockFetchOk(1)

    await adapter.delete('mykey')

    expect(fetch).toHaveBeenCalledWith(BASE_URL, expect.objectContaining({
      method: 'POST',
      body: JSON.stringify(['DEL', 'mykey']),
    }))
  })

  it('includes correct Authorization and Content-Type headers', async () => {
    mockFetchOk(null)

    await adapter.get('key')

    expect(fetch).toHaveBeenCalledWith(BASE_URL, expect.objectContaining({
      headers: expect.objectContaining({
        Authorization: `Bearer ${TOKEN}`,
        'Content-Type': 'application/json',
      }),
    }))
  })

  it('strips trailing slash from base URL', async () => {
    adapter = new RedisAdapter(`${BASE_URL}/`, TOKEN)
    mockFetchOk(null)

    await adapter.get('key')

    expect(fetch).toHaveBeenCalledWith(BASE_URL, expect.anything())
  })

  it('silences fetch errors in set (fire-and-forget safe)', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('network down'))
    await expect(adapter.set('key', 'value', 60_000)).resolves.toBeUndefined()
  })

  it('silences fetch errors in delete', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('network down'))
    await expect(adapter.delete('key')).resolves.toBeUndefined()
  })
})

// ─── getCacheAdapter selector ─────────────────────────────────────────────────

describe('getCacheAdapter', () => {
  beforeEach(() => {
    vi.resetModules()
    vi.unstubAllEnvs()
  })

  it('returns MemoryAdapter when Redis env vars are absent', async () => {
    vi.stubEnv('UPSTASH_REDIS_REST_URL', '')
    vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', '')

    const { getCacheAdapter } = await import('@/lib/cache/index')
    const { MemoryAdapter: MA } = await import('@/lib/cache/memory')

    expect(getCacheAdapter()).toBeInstanceOf(MA)
  })

  it('returns RedisAdapter when both Redis env vars are set', async () => {
    vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.upstash.io')
    vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', 'secret')

    const { getCacheAdapter } = await import('@/lib/cache/index')
    const { RedisAdapter: RA } = await import('@/lib/cache/redis')

    expect(getCacheAdapter()).toBeInstanceOf(RA)
  })

  it('returns the same instance on repeated calls (singleton)', async () => {
    vi.stubEnv('UPSTASH_REDIS_REST_URL', '')

    const { getCacheAdapter } = await import('@/lib/cache/index')

    expect(getCacheAdapter()).toBe(getCacheAdapter())
  })

  it('_resetCacheAdapter allows adapter re-selection', async () => {
    vi.stubEnv('UPSTASH_REDIS_REST_URL', '')
    vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', '')

    const { getCacheAdapter, _resetCacheAdapter } = await import('@/lib/cache/index')
    const { MemoryAdapter: MA } = await import('@/lib/cache/memory')
    const { RedisAdapter: RA } = await import('@/lib/cache/redis')

    expect(getCacheAdapter()).toBeInstanceOf(MA)

    _resetCacheAdapter()
    vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.upstash.io')
    vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', 'secret')

    expect(getCacheAdapter()).toBeInstanceOf(RA)
  })
})
