import { describe, it, expect, vi, beforeEach } from 'vitest';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { createApiClient } from '../client.js';
import { ApiError, AuthenticationError, NetworkError, RefreshTokenExpiredError } from '../errors.js';
import type { TokenProvider } from '../token-provider.js';

function makeTokenProvider(overrides: Partial<TokenProvider> = {}): TokenProvider {
  return {
    getAccessToken: vi.fn().mockResolvedValue('access-token'),
    getRefreshToken: vi.fn().mockResolvedValue('refresh-token'),
    saveTokens: vi.fn().mockResolvedValue(undefined),
    clearTokens: vi.fn().mockResolvedValue(undefined),
    ...overrides,
  };
}

describe('Auth interceptor', () => {
  it('attaches Authorization header to requests', async () => {
    const tokenProvider = makeTokenProvider();
    const client = createApiClient({}, tokenProvider);
    const mock = new MockAdapter(client);

    mock.onGet('/test').reply(200, { ok: true });

    await client.get('/test');

    expect(mock.history['get']?.[0]?.headers?.['Authorization']).toBe('Bearer access-token');
  });

  it('does not attach header when no token is stored', async () => {
    const tokenProvider = makeTokenProvider({
      getAccessToken: vi.fn().mockResolvedValue(null),
    });
    const client = createApiClient({}, tokenProvider);
    const mock = new MockAdapter(client);

    mock.onGet('/test').reply(200, {});

    await client.get('/test');

    expect(mock.history['get']?.[0]?.headers?.['Authorization']).toBeUndefined();
  });
});

describe('Error interceptor', () => {
  it('converts 401 to AuthenticationError', async () => {
    const client = createApiClient({}, makeTokenProvider({
      getRefreshToken: vi.fn().mockResolvedValue(null),
    }));
    const mock = new MockAdapter(client);

    mock.onGet('/secure').reply(401, { message: 'Unauthorized' });

    await expect(client.get('/secure')).rejects.toBeInstanceOf(AuthenticationError);
  });

  it('converts 404 to ApiError with correct status', async () => {
    const client = createApiClient({}, makeTokenProvider());
    const mock = new MockAdapter(client);

    mock.onGet('/missing').reply(404, { message: 'Not found' });

    await expect(client.get('/missing')).rejects.toMatchObject({
      status: 404,
      message: 'Not found',
    });
  });

  it('converts network timeout to NetworkError with isTimeout=true', async () => {
    const client = createApiClient({ timeoutMs: 100 }, makeTokenProvider());
    const mock = new MockAdapter(client);

    mock.onGet('/slow').timeout();

    const error = await client.get('/slow').catch((e: unknown) => e);
    expect(error).toBeInstanceOf(NetworkError);
    expect((error as NetworkError).isTimeout).toBe(true);
  });

  it('converts network error to NetworkError', async () => {
    const client = createApiClient({}, makeTokenProvider());
    const mock = new MockAdapter(client);

    mock.onGet('/down').networkError();

    await expect(client.get('/down')).rejects.toBeInstanceOf(NetworkError);
  });
});

describe('Refresh interceptor', () => {
  it('retries request with new token after successful refresh', async () => {
    const tokenProvider = makeTokenProvider();
    const client = createApiClient({}, tokenProvider);
    const mock = new MockAdapter(client);

    // First call returns 401, then after refresh returns 200
    let callCount = 0;
    mock.onGet('/api/data').reply(() => {
      callCount++;
      return callCount === 1 ? [401, { message: 'Unauthorized' }] : [200, { data: 'ok' }];
    });

    mock.onPost('/api/auth/refresh').reply(200, {
      accessToken: 'new-access-token',
      refreshToken: 'new-refresh-token',
      expiresAt: '2026-02-17T15:00:00Z',
    });

    const response = await client.get('/api/data');
    expect(response.data).toEqual({ data: 'ok' });
    expect(tokenProvider.saveTokens).toHaveBeenCalledWith(
      'new-access-token',
      'new-refresh-token',
      '2026-02-17T15:00:00Z',
    );
  });

  it('throws RefreshTokenExpiredError when no refresh token is stored', async () => {
    const tokenProvider = makeTokenProvider({
      getRefreshToken: vi.fn().mockResolvedValue(null),
    });
    const client = createApiClient({}, tokenProvider);
    const mock = new MockAdapter(client);

    mock.onGet('/api/data').reply(401, { message: 'Unauthorized' });

    await expect(client.get('/api/data')).rejects.toBeInstanceOf(RefreshTokenExpiredError);
    expect(tokenProvider.clearTokens).toHaveBeenCalled();
  });

  it('throws RefreshTokenExpiredError when refresh request fails', async () => {
    const tokenProvider = makeTokenProvider();
    const client = createApiClient({}, tokenProvider);
    const mock = new MockAdapter(client);

    mock.onGet('/api/data').reply(401, { message: 'Unauthorized' });
    mock.onPost('/api/auth/refresh').reply(401, { message: 'Refresh token expired' });

    await expect(client.get('/api/data')).rejects.toBeInstanceOf(RefreshTokenExpiredError);
    expect(tokenProvider.clearTokens).toHaveBeenCalled();
  });

  it('does not retry login endpoint on 401', async () => {
    const tokenProvider = makeTokenProvider();
    const client = createApiClient({}, tokenProvider);
    const mock = new MockAdapter(client);

    mock.onPost('/api/auth/login').reply(401, { message: 'Invalid credentials' });

    await expect(client.post('/api/auth/login', {})).rejects.toBeInstanceOf(AuthenticationError);
    expect(tokenProvider.saveTokens).not.toHaveBeenCalled();
  });
});
