/**
 * Component: Audiobookshelf API Client Tests
 * Documentation: documentation/features/audiobookshelf-integration.md
 */

import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
  absRequest,
  deleteABSItem,
  getABSLibraries,
  getABSLibraryItems,
  getABSRecentItems,
  getABSServerInfo,
  searchABSItems,
  triggerABSItemMatch,
  triggerABSScan,
} from '@/lib/services/audiobookshelf/api';

const configServiceMock = vi.hoisted(() => ({
  get: vi.fn(),
  getAudibleRegion: vi.fn(),
}));

const fetchMock = vi.hoisted(() => vi.fn());

vi.mock('@/lib/services/config.service', () => ({
  getConfigService: () => configServiceMock,
}));

describe('Audiobookshelf API client', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    configServiceMock.get.mockReset();
    fetchMock.mockReset();
    vi.stubGlobal('fetch', fetchMock);
  });

  it('throws when Audiobookshelf config is missing', async () => {
    configServiceMock.get.mockResolvedValue(null);

    await expect(absRequest('/status')).rejects.toThrow('Audiobookshelf not configured');
  });

  it('returns parsed JSON for successful requests', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({ version: '2.0.0', name: 'ABS' }),
    });

    const info = await getABSServerInfo();

    expect(info).toEqual({ version: '2.0.0', name: 'ABS' });
    expect(fetchMock).toHaveBeenCalledWith('http://abs/api/status', expect.any(Object));
  });

  it('throws when ABS responds with an error status', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    fetchMock.mockResolvedValue({
      ok: false,
      status: 401,
      statusText: 'Unauthorized',
    });

    await expect(absRequest('/status')).rejects.toThrow('ABS API error: 401 Unauthorized');
  });

  it('maps library responses and search queries', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });

    fetchMock
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ libraries: [{ id: 'lib-1' }] }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ results: [{ id: 'item-1' }] }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ results: [{ id: 'recent-1' }] }),
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ book: [{ id: 'result-1' }] }),
      });

    expect(await getABSLibraries()).toEqual([{ id: 'lib-1' }]);
    expect(await getABSLibraryItems('lib-1')).toEqual([{ id: 'item-1' }]);
    expect(await getABSRecentItems('lib-1', 5)).toEqual([{ id: 'recent-1' }]);
    expect(await searchABSItems('lib-1', 'hello world')).toEqual([{ id: 'result-1' }]);

    expect(fetchMock.mock.calls[1][0]).toBe('http://abs/api/libraries/lib-1/items');
    expect(fetchMock.mock.calls[2][0]).toBe('http://abs/api/libraries/lib-1/items?sort=addedAt&desc=1&limit=5');
    expect(fetchMock.mock.calls[3][0]).toBe('http://abs/api/libraries/lib-1/search?q=hello%20world');
  });

  it('returns an empty array when search results are missing', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({}),
    });

    expect(await searchABSItems('lib-1', 'missing')).toEqual([]);
  });

  it('triggers library scan using plain text responses', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    fetchMock.mockResolvedValue({
      ok: true,
      text: async () => 'OK',
    });

    await triggerABSScan('lib-1');

    expect(fetchMock).toHaveBeenCalledWith('http://abs/api/libraries/lib-1/scan', expect.objectContaining({
      method: 'POST',
    }));
  });

  it('includes ASIN overrides in metadata match requests with US region', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    configServiceMock.getAudibleRegion.mockResolvedValue('us');
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({}),
    });

    await triggerABSItemMatch('item-1', 'ASIN123');

    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body).toEqual({
      provider: 'audible', // US uses 'audible'
      asin: 'ASIN123',
      overrideDefaults: true,
    });
  });

  it('uses region-specific provider for Canada', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    configServiceMock.getAudibleRegion.mockResolvedValue('ca');
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({}),
    });

    await triggerABSItemMatch('item-1', 'ASIN123');

    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body).toEqual({
      provider: 'audible.ca',
      asin: 'ASIN123',
      overrideDefaults: true,
    });
  });

  it('uses region-specific provider for UK', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    configServiceMock.getAudibleRegion.mockResolvedValue('uk');
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({}),
    });

    await triggerABSItemMatch('item-1');

    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body).toEqual({
      provider: 'audible.uk',
    });
  });

  it('uses region-specific provider for Australia', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    configServiceMock.getAudibleRegion.mockResolvedValue('au');
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({}),
    });

    await triggerABSItemMatch('item-1', 'ASIN456');

    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body).toEqual({
      provider: 'audible.au',
      asin: 'ASIN456',
      overrideDefaults: true,
    });
  });

  it('uses region-specific provider for India', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    configServiceMock.getAudibleRegion.mockResolvedValue('in');
    fetchMock.mockResolvedValue({
      ok: true,
      json: async () => ({}),
    });

    await triggerABSItemMatch('item-1', 'ASIN789');

    const body = JSON.parse(fetchMock.mock.calls[0][1].body);
    expect(body).toEqual({
      provider: 'audible.in',
      asin: 'ASIN789',
      overrideDefaults: true,
    });
  });

  it('suppresses errors when metadata match fails', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    configServiceMock.getAudibleRegion.mockResolvedValue('us');
    fetchMock.mockResolvedValue({
      ok: false,
      status: 500,
      statusText: 'Boom',
    });

    await expect(triggerABSItemMatch('item-1', 'ASIN123')).resolves.toBeUndefined();
  });

  it('deletes a library item successfully', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    fetchMock.mockResolvedValue({
      ok: true,
    });

    await expect(deleteABSItem('item-1')).resolves.toBeUndefined();
    expect(fetchMock).toHaveBeenCalledWith('http://abs/api/items/item-1?hard=1', {
      method: 'DELETE',
      headers: {
        'Authorization': 'Bearer token',
      },
    });
  });

  it('throws when delete fails', async () => {
    configServiceMock.get.mockImplementation(async (key: string) => {
      if (key === 'audiobookshelf.server_url') return 'http://abs';
      if (key === 'audiobookshelf.api_token') return 'token';
      return null;
    });
    fetchMock.mockResolvedValue({
      ok: false,
      status: 500,
      statusText: 'Boom',
    });

    await expect(deleteABSItem('item-1')).rejects.toThrow('ABS API error: 500 Boom');
  });
});
