import { QueryClient } from '@tanstack/react-query'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
  clearPersistedCache,
  installPersistence,
  persistKeyForUser,
  type SyncStorageLike,
} from './persist'

class MemoryStorage implements SyncStorageLike {
  private readonly values = new Map<string, string>()

  get length(): number {
    return this.values.size
  }

  clear(): void {
    this.values.clear()
  }

  getItem(key: string): string | null {
    return this.values.get(key) ?? null
  }

  key(index: number): string | null {
    return [...this.values.keys()][index] ?? null
  }

  removeItem(key: string): void {
    this.values.delete(key)
  }

  setItem(key: string, value: string): void {
    this.values.set(key, value)
  }
}

function createPersistedBlob(queryKey: readonly unknown[], data: unknown): string {
  return JSON.stringify({
    buster: 'build-1',
    timestamp: Date.now(),
    clientState: {
      mutations: [],
      queries: [
        {
          queryKey,
          queryHash: JSON.stringify(queryKey),
          state: {
            data,
            dataUpdatedAt: Date.now(),
            error: null,
            errorUpdateCount: 0,
            errorUpdatedAt: 0,
            fetchFailureCount: 0,
            fetchFailureReason: null,
            fetchMeta: null,
            isInvalidated: false,
            status: 'success',
            fetchStatus: 'idle',
          },
        },
      ],
    },
  })
}

describe('persist', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
    vi.restoreAllMocks()
  })

  it('builds a per-user key and falls back to anon', () => {
    expect(persistKeyForUser('user-1')).toBe('pq:user-1')
    expect(persistKeyForUser(null)).toBe('pq:anon')
  })

  it('hydrates synchronously during install so first render sees cached data', () => {
    const storage = new MemoryStorage()
    const queryClient = new QueryClient()
    const key = ['dashboard']

    storage.setItem(persistKeyForUser('user-1'), createPersistedBlob(key, 'cached'))

    installPersistence(queryClient, {
      userId: 'user-1',
      buster: 'build-1',
      storage,
      throttleMs: 10,
    })

    expect(queryClient.getQueryData(key)).toBe('cached')
  })

  it('cancels a pending throttled save after uninstall', async () => {
    const storage = new MemoryStorage()
    const queryClient = new QueryClient()
    const unsubscribe = installPersistence(queryClient, {
      userId: 'user-1',
      buster: 'build-1',
      storage,
      throttleMs: 50,
    })

    queryClient.setQueryData(['todos'], ['a'])
    unsubscribe()
    await vi.advanceTimersByTimeAsync(60)

    expect(storage.getItem(persistKeyForUser('user-1'))).toBeNull()
  })

  it('ignores a corrupt blob and keeps the client usable', () => {
    const storage = new MemoryStorage()
    const queryClient = new QueryClient()

    storage.setItem(persistKeyForUser('user-1'), '{not-json')

    expect(() =>
      installPersistence(queryClient, {
        userId: 'user-1',
        buster: 'build-1',
        storage,
      }),
    ).not.toThrow()
    expect(queryClient.getQueryCache().getAll()).toHaveLength(0)
  })

  it('clears all user cache entries by default', () => {
    const storage = new MemoryStorage()
    storage.setItem('pq:user-a', 'a')
    storage.setItem('pq:user-b', 'b')
    storage.setItem('other:key', 'c')

    clearPersistedCache({ storage })

    expect(storage.getItem('pq:user-a')).toBeNull()
    expect(storage.getItem('pq:user-b')).toBeNull()
    expect(storage.getItem('other:key')).toBe('c')
  })
})
