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

function jsonResponse(data: unknown, init: ResponseInit = {}) {
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    ...init,
  })
}

async function loadCsrfModule() {
  vi.resetModules()
  return import('./csrf')
}

function clearCsrfCookie() {
  if (typeof document === 'undefined') {
    return
  }
  document.cookie = 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
}

describe('csrf helpers', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.unstubAllGlobals()
    vi.unstubAllEnvs()
    clearCsrfCookie()
    vi.stubGlobal('fetch', vi.fn())
  })

  afterEach(() => {
    clearCsrfCookie()
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
    vi.unstubAllEnvs()
  })

  it('returns null when cookie lookup runs without a document', async () => {
    vi.stubGlobal('document', undefined)

    const { getCSRFTokenFromCookie, getCSRFHeaders } = await loadCsrfModule()

    expect(getCSRFTokenFromCookie()).toBeNull()
    expect(getCSRFHeaders()).toEqual({})
  })

  it('stores csrf tokens in memory and falls back to the cookie for headers', async () => {
    const {
      CSRF_HEADER_NAME,
      clearCSRFToken,
      getCSRFHeaders,
      getCSRFToken,
      setCSRFToken,
    } = await loadCsrfModule()

    setCSRFToken('memory-token')
    expect(getCSRFToken()).toBe('memory-token')
    expect(getCSRFHeaders()).toEqual({
      [CSRF_HEADER_NAME]: 'memory-token',
    })

    clearCSRFToken()
    document.cookie = 'csrftoken=cookie-token; path=/'

    expect(getCSRFToken()).toBeNull()
    expect(getCSRFHeaders()).toEqual({
      [CSRF_HEADER_NAME]: 'cookie-token',
    })
  })

  it('prefers an existing csrf cookie and skips the network bootstrap', async () => {
    const fetchMock = vi.mocked(fetch)
    document.cookie = 'csrftoken=cookie-token; path=/'

    const { getCSRFToken, initCSRFToken } = await loadCsrfModule()
    const token = await initCSRFToken()

    expect(token).toBe('cookie-token')
    expect(getCSRFToken()).toBe('cookie-token')
    expect(fetchMock).not.toHaveBeenCalled()
  })

  it('fetches and stores a csrf token from the default relative api base', async () => {
    const fetchMock = vi.mocked(fetch)
    fetchMock.mockResolvedValueOnce(
      jsonResponse({
        code: 0,
        data: {
          csrf_token: 'api-token',
        },
      }),
    )

    const { getCSRFToken, initCSRFToken } = await loadCsrfModule()
    const token = await initCSRFToken()

    expect(token).toBe('api-token')
    expect(getCSRFToken()).toBe('api-token')
    expect(fetchMock).toHaveBeenCalledWith(
      `${window.location.origin}/api/v1/auth/csrf-token`,
      {
        method: 'GET',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
      },
    )
  })

  it('supports api base urls without a leading slash', async () => {
    vi.stubEnv('VITE_API_BASE_URL', 'api/custom')
    const fetchMock = vi.mocked(fetch)
    fetchMock.mockResolvedValueOnce(
      jsonResponse({
        code: 0,
        data: {
          csrf_token: 'custom-token',
        },
      }),
    )

    const { initCSRFToken } = await loadCsrfModule()

    await initCSRFToken()

    expect(fetchMock).toHaveBeenCalledWith(
      `${window.location.origin}/api/custom/auth/csrf-token`,
      expect.any(Object),
    )
  })

  it('supports absolute api base urls', async () => {
    vi.stubEnv('VITE_API_BASE_URL', 'https://api.example.com/base')
    const fetchMock = vi.mocked(fetch)
    fetchMock.mockResolvedValueOnce(
      jsonResponse({
        code: 0,
        data: {
          csrf_token: 'absolute-token',
        },
      }),
    )

    const { initCSRFToken } = await loadCsrfModule()

    await initCSRFToken()

    expect(fetchMock).toHaveBeenCalledWith(
      'https://api.example.com/base/auth/csrf-token',
      expect.any(Object),
    )
  })

  it('falls back to a cookie exposed after the csrf bootstrap request fails', async () => {
    const fetchMock = vi.mocked(fetch)
    fetchMock.mockRejectedValueOnce(new Error('network failed'))

    const cookieSpy = vi
      .spyOn(document, 'cookie', 'get')
      .mockReturnValueOnce('')
      .mockReturnValueOnce('csrftoken=fallback-token')

    const { getCSRFToken, initCSRFToken } = await loadCsrfModule()
    const token = await initCSRFToken()

    expect(token).toBe('fallback-token')
    expect(getCSRFToken()).toBe('fallback-token')
    expect(fetchMock).toHaveBeenCalledTimes(1)
    cookieSpy.mockRestore()
  })

  it('returns null when the bootstrap response does not contain a csrf token', async () => {
    const fetchMock = vi.mocked(fetch)
    fetchMock.mockResolvedValueOnce(
      jsonResponse({
        code: 1,
        data: {},
      }),
    )

    const { getCSRFHeaders, getCSRFToken, initCSRFToken } = await loadCsrfModule()
    const token = await initCSRFToken()

    expect(token).toBeNull()
    expect(getCSRFToken()).toBeNull()
    expect(getCSRFHeaders()).toEqual({})
  })
})
