import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
  getAllSearchItemIds,
  getSearchAllItems,
  getSearchFields,
  getSearchResults,
  toAddItemsRequestFromSearch
} from './search';

// Builds an all-items page response body. `totals` fills unspecified counts with 0.
function allItemsPage(
  buckets: Record<string, number[]>,
  totals: Record<string, number> = {}
): Record<string, unknown> {
  return {
    ...buckets,
    totals: {
      movieCount: 0,
      showCount: 0,
      seasonCount: 0,
      episodeCount: 0,
      artistCount: 0,
      musicVideoCount: 0,
      otherVideoCount: 0,
      songCount: 0,
      imageCount: 0,
      remoteStreamCount: 0,
      ...totals
    }
  };
}

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

const emptyGroup = { totalCount: 0, items: [] };
const sampleResults = {
  movies: emptyGroup,
  shows: emptyGroup,
  seasons: emptyGroup,
  artists: emptyGroup,
  episodes: emptyGroup,
  musicVideos: emptyGroup,
  songs: emptyGroup,
  otherVideos: emptyGroup,
  images: emptyGroup,
  remoteStreams: emptyGroup
};

describe('getSearchResults', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('GETs /api/v1/search with the query', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify(sampleResults), {
        headers: { 'Content-Type': 'application/json' },
        status: 200
      })
    );

    await getSearchResults({ query: 'star' });

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/search?query=star');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
  });

  it('includes pageSize when provided', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify(sampleResults), {
        headers: { 'Content-Type': 'application/json' },
        status: 200
      })
    );

    await getSearchResults({ query: 'star wars', pageSize: 50 });

    const [url] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/search?query=star+wars&pageSize=50');
  });

  it('rejects with the ApiError status on failure', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ status: 422, title: 'Validation failed' }), {
        headers: { 'Content-Type': 'application/json' },
        status: 422
      })
    );

    await expect(getSearchResults({ query: '' })).rejects.toMatchObject({ status: 422 });
  });
});

describe('getSearchAllItems', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('GETs /api/v1/search/all-items with the query', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({ movieIds: [1], showIds: [2] }), {
        headers: { 'Content-Type': 'application/json' },
        status: 200
      })
    );

    await getSearchAllItems('star wars');

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/search/all-items?query=star+wars');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
  });
});

describe('getSearchFields', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('GETs /api/v1/search/fields and returns the parsed array', async () => {
    const sampleFields = [
      { name: 'title', label: 'Title', type: 'text', group: 'General', values: null },
      { name: 'year', label: 'Year', type: 'number', group: 'General', values: null }
    ];
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
      new Response(JSON.stringify(sampleFields), {
        headers: { 'Content-Type': 'application/json' },
        status: 200
      })
    );

    const result = await getSearchFields();

    const [url, init] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/search/fields');
    expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
    expect(result).toEqual(sampleFields);
  });
});

describe('getSearchAllItems paging params', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('includes pageNum and pageSize when provided', async () => {
    const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(allItemsPage({})));

    await getSearchAllItems('star', 2, 1000);

    const [url] = fetchSpy.mock.calls[0];
    expect(url).toBe('/api/v1/search/all-items?query=star&pageNum=2&pageSize=1000');
  });
});

describe('getAllSearchItemIds', () => {
  beforeEach(() => {
    window.localStorage.clear();
    vi.restoreAllMocks();
  });

  it('returns a single page when every bucket is already complete', async () => {
    const fetchSpy = vi
      .spyOn(window, 'fetch')
      .mockResolvedValue(jsonResponse(allItemsPage({ movieIds: [1, 2], showIds: [9] }, { movieCount: 2, showCount: 1 })));

    const result = await getAllSearchItemIds('star');

    expect(fetchSpy).toHaveBeenCalledTimes(1);
    expect(result.movieIds).toEqual([1, 2]);
    expect(result.showIds).toEqual([9]);
    expect(result.seasonIds).toEqual([]);
  });

  it('pages to completeness and merges every bucket', async () => {
    const fetchSpy = vi
      .spyOn(window, 'fetch')
      .mockResolvedValueOnce(jsonResponse(allItemsPage({ movieIds: [1, 2] }, { movieCount: 3 })))
      .mockResolvedValueOnce(jsonResponse(allItemsPage({ movieIds: [3] }, { movieCount: 3 })));

    const result = await getAllSearchItemIds('star');

    expect(fetchSpy).toHaveBeenCalledTimes(2);
    expect(result.movieIds).toEqual([1, 2, 3]);
    // The second request advances the page.
    expect(fetchSpy.mock.calls[1][0]).toBe('/api/v1/search/all-items?query=star&pageNum=1&pageSize=1000');
  });

  it('pages until an empty page when the server omits totals', async () => {
    // Without `totals` the fast completeness check can't fire, so it must keep paging until empty
    // rather than silently truncating to the first page.
    const fetchSpy = vi
      .spyOn(window, 'fetch')
      .mockResolvedValueOnce(jsonResponse({ movieIds: [1, 2] }))
      .mockResolvedValueOnce(jsonResponse({ movieIds: [3] }))
      .mockResolvedValueOnce(jsonResponse({ movieIds: [] }));

    const result = await getAllSearchItemIds('star');

    expect(fetchSpy).toHaveBeenCalledTimes(3);
    expect(result.movieIds).toEqual([1, 2, 3]);
  });

  it('stops on an empty page even if a total claims more (drift safety)', async () => {
    const fetchSpy = vi
      .spyOn(window, 'fetch')
      .mockResolvedValue(jsonResponse(allItemsPage({}, { movieCount: 5 })));

    const result = await getAllSearchItemIds('star');

    expect(fetchSpy).toHaveBeenCalledTimes(1);
    expect(result.movieIds).toEqual([]);
  });
});

describe('toAddItemsRequestFromSearch', () => {
  it('fills every bucket, defaulting null arrays to []', () => {
    expect(
      toAddItemsRequestFromSearch({
        artistIds: [],
        episodeIds: [],
        imageIds: [],
        movieIds: [1, 2],
        musicVideoIds: [],
        otherVideoIds: [],
        remoteStreamIds: [],
        seasonIds: [],
        showIds: [],
        songIds: []
      })
    ).toEqual({
      artistIds: [],
      episodeIds: [],
      imageIds: [],
      movieIds: [1, 2],
      musicVideoIds: [],
      otherVideoIds: [],
      remoteStreamIds: [],
      seasonIds: [],
      showIds: [],
      songIds: []
    });
  });
});
