import { describe, expect, it, vi } from 'vitest';

import { PicloudClient, type RealtimeEvent } from '../src/index.js';
import { dataFrame, emptyResponse, lastUrl, sseResponse, type FetchArgs } from './helpers.js';

/** A fetch mock that plays through a queue of response factories. */
function queuedFetch(responders: Array<() => Promise<Response>>) {
  let i = 0;
  return vi.fn(async (_u: FetchArgs[0], _i?: FetchArgs[1]) => {
    const idx = Math.min(i, responders.length - 1);
    i += 1;
    const r = responders[idx];
    if (!r) throw new Error('no responder');
    return r();
  });
}

describe('subscribe', () => {
  it('connects to the SSE endpoint and delivers events', async () => {
    const fetchMock = queuedFetch([async () => sseResponse([dataFrame('chat', { hi: 1 })])]);
    const client = new PicloudClient({ baseURL: 'https://api.test', fetch: fetchMock });

    const received: Array<RealtimeEvent<{ hi: number }>> = [];
    const unsubscribe = client.subscribe<{ hi: number }>('chat', (e) => received.push(e));

    await vi.waitFor(() => expect(received.length).toBe(1));
    unsubscribe();

    expect(received[0]?.topic).toBe('chat');
    expect(received[0]?.message).toEqual({ hi: 1 });
    expect(lastUrl(fetchMock)).toBe('https://api.test/realtime/topics/chat');
  });

  it('passes a token via the query string', async () => {
    const fetchMock = queuedFetch([async () => sseResponse([dataFrame('chat', 1)])]);
    const client = new PicloudClient({ baseURL: 'https://api.test', fetch: fetchMock });
    const unsubscribe = client.subscribe('chat', () => {}, { token: 'abc.def' });
    await vi.waitFor(() => expect(fetchMock).toHaveBeenCalled());
    unsubscribe();
    expect(lastUrl(fetchMock)).toBe('https://api.test/realtime/topics/chat?token=abc.def');
  });

  it('reconnects with backoff after an initial connection failure', async () => {
    const fetchMock = queuedFetch([
      async () => {
        throw new Error('network down');
      },
      async () => sseResponse([dataFrame('chat', { ok: true })])
    ]);
    const client = new PicloudClient({ baseURL: 'https://api.test', fetch: fetchMock });

    const received: unknown[] = [];
    const unsubscribe = client.subscribe('chat', (e) => received.push(e.message), {
      baseBackoffMs: 5,
      maxBackoffMs: 20
    });

    await vi.waitFor(() => expect(received.length).toBeGreaterThanOrEqual(1), { timeout: 1000 });
    unsubscribe();
    expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(2);
    expect(received[0]).toEqual({ ok: true });
  });

  it('refreshes the token after a 401 and reconnects', async () => {
    const fetchMock = queuedFetch([
      async () => emptyResponse(401),
      async () => sseResponse([dataFrame('chat', { v: 2 })])
    ]);
    const client = new PicloudClient({ baseURL: 'https://api.test', fetch: fetchMock });

    const onTokenExpired = vi.fn(() => 'fresh-token');
    const received: unknown[] = [];
    const unsubscribe = client.subscribe('chat', (e) => received.push(e.message), {
      token: 'stale',
      onTokenExpired,
      baseBackoffMs: 5
    });

    await vi.waitFor(() => expect(received.length).toBeGreaterThanOrEqual(1), { timeout: 1000 });
    unsubscribe();

    expect(onTokenExpired).toHaveBeenCalled();
    // Second connect carries the refreshed token.
    expect(lastUrl(fetchMock, 1)).toContain('token=fresh-token');
    expect(received[0]).toEqual({ v: 2 });
  });

  it('stops and reports when a 401 cannot be refreshed', async () => {
    const fetchMock = queuedFetch([async () => emptyResponse(401)]);
    const client = new PicloudClient({ baseURL: 'https://api.test', fetch: fetchMock });
    const onError = vi.fn();
    const unsubscribe = client.subscribe('chat', () => {}, {
      onTokenExpired: () => null,
      onError
    });
    await vi.waitFor(() => expect(onError).toHaveBeenCalled());
    unsubscribe();
  });

  it('caps the 401-refresh loop after consecutive failures', async () => {
    // onTokenExpired keeps returning a fresh-looking-but-still-rejected
    // token. Without the cap the loop would reconnect forever.
    const fetchMock = queuedFetch([
      async () => emptyResponse(401),
      async () => emptyResponse(401),
      async () => emptyResponse(401),
      async () => emptyResponse(401),
      async () => emptyResponse(401)
    ]);
    const client = new PicloudClient({ baseURL: 'https://api.test', fetch: fetchMock });
    const onError = vi.fn();
    const onTokenExpired = vi.fn(() => 'never-good-enough');
    const unsubscribe = client.subscribe('chat', () => {}, {
      onTokenExpired,
      onError
    });
    await vi.waitFor(() => expect(onError).toHaveBeenCalled(), { timeout: 1000 });
    unsubscribe();
    // The cap is 3 consecutive 401s — at most 3 fetches should have
    // been issued before the loop bailed.
    expect(fetchMock.mock.calls.length).toBeLessThanOrEqual(3);
    const errMsg = String(onError.mock.calls[0]?.[0]);
    expect(errMsg).toContain('401-refresh loop');
  });
});
