/**
 * Tests for the API service layer (src/lib/api.ts)
 *
 * All HTTP calls are intercepted by mocking global.fetch.
 * Tests verify correct URL construction, headers, methods, and error handling.
 */
import { describe, it, expect, vi, afterEach } from 'vitest'
import {
  registerUser,
  getUserByEmail,
  updateUserProfile,
  deleteUser,
  createEntry,
  getUserEntries,
  getEntry,
  updateEntry,
  deleteEntry,
  convertUTCToIST,
} from '../lib/api'

const TOKEN = 'firebase-id-token'
const USER_ID = '507f1f77bcf86cd799439011'
const ENTRY_ID = '507f1f77bcf86cd799439022'

// ---------------------------------------------------------------------------
// Fetch mock helpers
// ---------------------------------------------------------------------------

function mockFetch(body: unknown, status = 200) {
  vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
    ok: status >= 200 && status < 300,
    status,
    statusText: status === 200 ? 'OK' : 'Error',
    json: () => Promise.resolve(body),
  }))
}

function mockFetchError(detail: string, status: number) {
  vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
    ok: false,
    status,
    statusText: 'Error',
    json: () => Promise.resolve({ detail }),
  }))
}

function mockFetchNetworkError() {
  vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error')))
}

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

// ---------------------------------------------------------------------------
// User Endpoints
// ---------------------------------------------------------------------------

describe('registerUser', () => {
  it('sends POST to /users/register', async () => {
    mockFetch({ id: USER_ID, email: 'a@b.com', message: 'User registered successfully' })
    await registerUser({ email: 'a@b.com' }, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('/users/register'),
      expect.objectContaining({ method: 'POST' })
    )
  })

  it('includes Authorization Bearer token in headers', async () => {
    mockFetch({})
    await registerUser({ email: 'a@b.com' }, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        headers: expect.objectContaining({ Authorization: `Bearer ${TOKEN}` }),
      })
    )
  })

  it('sends displayName and photoURL in body', async () => {
    mockFetch({})
    await registerUser({ email: 'a@b.com', displayName: 'Alice', photoURL: 'https://pic.url' }, TOKEN)
    const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body)
    expect(body).toMatchObject({ email: 'a@b.com', displayName: 'Alice' })
  })

  it('returns the parsed response', async () => {
    const response = { id: USER_ID, email: 'a@b.com', message: 'User registered successfully' }
    mockFetch(response)
    const result = await registerUser({ email: 'a@b.com' }, TOKEN)
    expect(result).toEqual(response)
  })
})

describe('getUserByEmail', () => {
  it('sends GET to /users/by-email/{email}', async () => {
    mockFetch({ id: USER_ID, email: 'test@example.com' })
    await getUserByEmail('test@example.com', TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('/users/by-email/test@example.com'),
      expect.any(Object)
    )
  })

  it('throws "User not found" on 404', async () => {
    mockFetchError('User not found', 404)
    await expect(getUserByEmail('ghost@example.com', TOKEN)).rejects.toThrow('User not found')
  })
})

describe('updateUserProfile', () => {
  it('sends PUT to /users/{userId}', async () => {
    mockFetch({ id: USER_ID, theme: 'dark', message: 'User updated successfully' })
    await updateUserProfile(USER_ID, { theme: 'dark' }, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/users/${USER_ID}`),
      expect.objectContaining({ method: 'PUT' })
    )
  })

  it('sends only the provided fields', async () => {
    mockFetch({})
    await updateUserProfile(USER_ID, { displayName: 'New Name' }, TOKEN)
    const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body)
    expect(body).toMatchObject({ displayName: 'New Name' })
  })
})

describe('deleteUser', () => {
  it('sends DELETE to /users/{userId}', async () => {
    mockFetch({ message: 'User deleted successfully', user_deleted: 1, entries_deleted: 3 })
    await deleteUser(USER_ID, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/users/${USER_ID}`),
      expect.objectContaining({ method: 'DELETE' })
    )
  })

  it('returns deletion counts', async () => {
    mockFetch({ message: 'User deleted successfully', user_deleted: 1, entries_deleted: 5 })
    const result = await deleteUser(USER_ID, TOKEN)
    expect(result).toMatchObject({ user_deleted: 1, entries_deleted: 5 })
  })
})

// ---------------------------------------------------------------------------
// Entry Endpoints
// ---------------------------------------------------------------------------

describe('createEntry', () => {
  const encryptedEntry = {
    encryption: {
      encrypted: true,
      ciphertext: 'dGVzdA==',
      nonce: 'bm9uY2U=',
      algorithm: 'XSalsa20-Poly1305',
    },
  }

  it('sends POST to /entries/{userId}', async () => {
    mockFetch({ id: ENTRY_ID, message: 'Entry created successfully' })
    await createEntry(USER_ID, encryptedEntry, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/entries/${USER_ID}`),
      expect.objectContaining({ method: 'POST' })
    )
  })

  it('returns entry id and message', async () => {
    mockFetch({ id: ENTRY_ID, message: 'Entry created successfully' })
    const result = await createEntry(USER_ID, encryptedEntry, TOKEN)
    expect(result).toMatchObject({ id: ENTRY_ID })
  })

  it('throws on 404 when user not found', async () => {
    mockFetchError('User not found', 404)
    await expect(createEntry('nonexistent-user', encryptedEntry, TOKEN)).rejects.toThrow('User not found')
  })
})

describe('getUserEntries', () => {
  it('sends GET to /entries/{userId} with default pagination', async () => {
    mockFetch({ entries: [], total: 0 })
    await getUserEntries(USER_ID, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/entries/${USER_ID}?limit=50&skip=0`),
      expect.any(Object)
    )
  })

  it('respects custom limit and skip', async () => {
    mockFetch({ entries: [], total: 0 })
    await getUserEntries(USER_ID, TOKEN, 10, 20)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('limit=10&skip=20'),
      expect.any(Object)
    )
  })

  it('returns entries and total', async () => {
    mockFetch({ entries: [{ id: ENTRY_ID }], total: 1 })
    const result = await getUserEntries(USER_ID, TOKEN)
    expect(result).toMatchObject({ total: 1 })
  })
})

describe('getEntry', () => {
  it('sends GET to /entries/{userId}/{entryId}', async () => {
    mockFetch({ id: ENTRY_ID, userId: USER_ID, createdAt: '2024-01-01', updatedAt: '2024-01-01' })
    await getEntry(USER_ID, ENTRY_ID, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/entries/${USER_ID}/${ENTRY_ID}`),
      expect.any(Object)
    )
  })

  it('throws "Entry not found" on 404', async () => {
    mockFetchError('Entry not found', 404)
    await expect(getEntry(USER_ID, 'bad-id', TOKEN)).rejects.toThrow('Entry not found')
  })
})

describe('updateEntry', () => {
  it('sends PUT to /entries/{userId}/{entryId}', async () => {
    mockFetch({ id: ENTRY_ID })
    await updateEntry(USER_ID, ENTRY_ID, { mood: 'happy' }, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/entries/${USER_ID}/${ENTRY_ID}`),
      expect.objectContaining({ method: 'PUT' })
    )
  })
})

describe('deleteEntry', () => {
  it('sends DELETE to /entries/{userId}/{entryId}', async () => {
    mockFetch({ message: 'Entry deleted successfully' })
    await deleteEntry(USER_ID, ENTRY_ID, TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining(`/entries/${USER_ID}/${ENTRY_ID}`),
      expect.objectContaining({ method: 'DELETE' })
    )
  })
})

describe('convertUTCToIST', () => {
  it('sends POST to /entries/convert-timestamp/utc-to-ist', async () => {
    const utc = '2024-01-01T00:00:00Z'
    mockFetch({ utc, ist: '2024-01-01T05:30:00+05:30' })
    await convertUTCToIST(utc)
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('/convert-timestamp/utc-to-ist'),
      expect.objectContaining({ method: 'POST' })
    )
  })

  it('returns both utc and ist fields', async () => {
    const utc = '2024-01-01T00:00:00Z'
    mockFetch({ utc, ist: '2024-01-01T05:30:00+05:30' })
    const result = await convertUTCToIST(utc)
    expect(result).toMatchObject({ utc, ist: expect.stringContaining('+05:30') })
  })
})

// ---------------------------------------------------------------------------
// Generic Error Handling
// ---------------------------------------------------------------------------

describe('API error handling', () => {
  it('throws the error detail from response body', async () => {
    mockFetchError('Specific backend error message', 400)
    await expect(getUserByEmail('x@x.com', TOKEN)).rejects.toThrow('Specific backend error message')
  })

  it('falls back to "API error: {statusText}" when body has no detail', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      status: 500,
      statusText: 'Internal Server Error',
      json: () => Promise.reject(new Error('no JSON')),
    }))
    await expect(getUserByEmail('x@x.com', TOKEN)).rejects.toThrow('API error: Internal Server Error')
  })

  it('propagates network errors', async () => {
    mockFetchNetworkError()
    await expect(getUserByEmail('x@x.com', TOKEN)).rejects.toThrow('Network error')
  })

  it('includes credentials: include in all requests', async () => {
    mockFetch({})
    await getUserByEmail('x@x.com', TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({ credentials: 'include' })
    )
  })

  it('sets Content-Type: application/json on all requests', async () => {
    mockFetch({})
    await getUserByEmail('x@x.com', TOKEN)
    expect(fetch).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
      })
    )
  })
})
