import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { refreshOAuthTokenIfExpired, forceRefreshOAuthToken, _getInflightRefreshCount } from '../refresh'
import type { OAuthToken } from '../../types'

describe('concurrent OAuth token refresh deduplication', () => {
  const originalFetch = global.fetch

  beforeEach(() => {
    process.env.DROPBOX_APP_KEY = 'test-dropbox-app-key'
    process.env.DROPBOX_APP_SECRET = 'test-dropbox-app-secret'
    process.env.GOOGLE_OAUTH_CLIENT_ID = 'test-google-client-id'
    process.env.GOOGLE_OAUTH_CLIENT_SECRET = 'test-google-client-secret'
  })

  afterEach(() => {
    global.fetch = originalFetch
    vi.restoreAllMocks()
  })

  function makeExpiredToken(overrides?: Partial<OAuthToken>): OAuthToken {
    return {
      provider: 'dropbox',
      accessToken: 'expired-access-token',
      refreshToken: 'shared-refresh-token',
      expiresAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(),
      tokenType: 'Bearer',
      ...overrides
    }
  }

  it('should make only one fetch call when two concurrent refreshes use the same token', async () => {
    let resolveRefresh!: (value: Response) => void
    const mockFetch = vi.fn().mockReturnValue(
      new Promise<Response>((resolve) => {
        resolveRefresh = resolve
      })
    )
    global.fetch = mockFetch

    const token = makeExpiredToken()

    // Fire two concurrent refreshes
    const promise1 = refreshOAuthTokenIfExpired(token)
    const promise2 = refreshOAuthTokenIfExpired(token)

    // While in-flight, there should be exactly one tracked refresh
    expect(_getInflightRefreshCount()).toBe(1)

    // Resolve the single fetch call
    resolveRefresh(
      new Response(
        JSON.stringify({
          access_token: 'new-access-token',
          refresh_token: 'new-refresh-token',
          expires_in: 3600
        }),
        { status: 200 }
      )
    )

    const [result1, result2] = await Promise.all([promise1, promise2])

    // Both get the same result
    expect(result1.accessToken).toBe('new-access-token')
    expect(result2.accessToken).toBe('new-access-token')

    // Only one actual API call was made
    expect(mockFetch).toHaveBeenCalledTimes(1)

    // Map is cleaned up
    expect(_getInflightRefreshCount()).toBe(0)
  })

  it('should make separate fetch calls for different providers', async () => {
    const mockFetch = vi.fn().mockImplementation(() =>
      Promise.resolve(
        new Response(
          JSON.stringify({
            access_token: 'new-token',
            refresh_token: 'new-refresh',
            expires_in: 3600
          }),
          { status: 200 }
        )
      )
    )
    global.fetch = mockFetch

    const dropboxToken = makeExpiredToken()
    const googleToken = makeExpiredToken({
      provider: 'google-sheets',
      refreshToken: 'google-refresh-token'
    })

    await Promise.all([refreshOAuthTokenIfExpired(dropboxToken), refreshOAuthTokenIfExpired(googleToken)])

    // Different providers = separate API calls
    expect(mockFetch).toHaveBeenCalledTimes(2)
  })

  it('should make separate fetch calls for different refresh tokens on the same provider', async () => {
    const mockFetch = vi.fn().mockImplementation(() =>
      Promise.resolve(
        new Response(
          JSON.stringify({
            access_token: 'new-token',
            refresh_token: 'new-refresh',
            expires_in: 3600
          }),
          { status: 200 }
        )
      )
    )
    global.fetch = mockFetch

    const token1 = makeExpiredToken({ refreshToken: 'user-a-refresh' })
    const token2 = makeExpiredToken({ refreshToken: 'user-b-refresh' })

    await Promise.all([refreshOAuthTokenIfExpired(token1), refreshOAuthTokenIfExpired(token2)])

    // Different refresh tokens = different users = separate API calls
    expect(mockFetch).toHaveBeenCalledTimes(2)
  })

  it('should propagate errors to all waiters when the refresh fails', async () => {
    const mockFetch = vi.fn().mockResolvedValue(
      new Response(
        JSON.stringify({
          error: 'invalid_grant',
          error_description: 'Token revoked'
        }),
        { status: 400 }
      )
    )
    global.fetch = mockFetch

    const token = makeExpiredToken()

    const promise1 = forceRefreshOAuthToken(token)
    const promise2 = forceRefreshOAuthToken(token)

    // Both should reject with the same error
    await expect(promise1).rejects.toThrow('invalid_grant')
    await expect(promise2).rejects.toThrow('invalid_grant')

    // Only one API call was made
    expect(mockFetch).toHaveBeenCalledTimes(1)

    // Map is cleaned up even on failure
    expect(_getInflightRefreshCount()).toBe(0)
  })

  it('should allow a new refresh after the previous one completes', async () => {
    let callCount = 0
    const mockFetch = vi.fn().mockImplementation(() => {
      callCount++
      return Promise.resolve(
        new Response(
          JSON.stringify({
            access_token: `token-${callCount}`,
            refresh_token: 'new-refresh',
            expires_in: 3600
          }),
          { status: 200 }
        )
      )
    })
    global.fetch = mockFetch

    const token = makeExpiredToken()

    // First refresh
    const result1 = await forceRefreshOAuthToken(token)
    expect(result1.accessToken).toBe('token-1')

    // Second refresh (after first completed)
    const result2 = await forceRefreshOAuthToken(token)
    expect(result2.accessToken).toBe('token-2')

    // Two separate API calls (not deduplicated since they were sequential)
    expect(mockFetch).toHaveBeenCalledTimes(2)
  })
})
