import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
  addTraktList,
  deleteTraktList,
  getTraktListById,
  getTraktLists,
  getTraktStatus,
  matchTraktList,
  updateTraktList
} from './trakt';

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

function accepted(): Response {
  return new Response(null, { headers: { 'Content-Length': '0' }, status: 202 });
}

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

  it('getTraktLists fetches the default page without query params', async () => {
    const fetchMock = vi
      .spyOn(window, 'fetch')
      .mockResolvedValue(jsonResponse({ page: [], totalCount: 0 }));

    await getTraktLists();

    expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists', expect.objectContaining({ method: 'GET' }));
  });

  it('getTraktLists forwards pageNum/pageSize as query params', async () => {
    const fetchMock = vi
      .spyOn(window, 'fetch')
      .mockResolvedValue(jsonResponse({ page: [], totalCount: 0 }));

    await getTraktLists({ pageNum: 2, pageSize: 25 });

    const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
    expect(url.pathname).toBe('/api/v1/trakt/lists');
    expect(url.searchParams.get('pageNum')).toBe('2');
    expect(url.searchParams.get('pageSize')).toBe('25');
  });

  it('getTraktListById fetches a single list by id', async () => {
    const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(
      jsonResponse({
        autoRefresh: true,
        generatePlaylist: false,
        id: 1,
        itemCount: 10,
        matchCount: 8,
        name: 'My List',
        slug: 'my-list',
        traktId: 100
      })
    );

    await expect(getTraktListById(1)).resolves.toMatchObject({ id: 1, slug: 'my-list' });
    expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists/1', expect.objectContaining({ method: 'GET' }));
  });

  it('addTraktList POSTs the url and resolves on 202', async () => {
    const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(accepted());

    await expect(addTraktList('https://trakt.tv/users/someuser/lists/some-list')).resolves.toBeUndefined();

    const [url, init] = fetchMock.mock.calls[0];
    expect(url).toBe('/api/v1/trakt/lists');
    expect(init).toMatchObject({ method: 'POST' });
    expect(JSON.parse(String(init?.body))).toEqual({ url: 'https://trakt.tv/users/someuser/lists/some-list' });
  });

  it('addTraktList rethrows a 422 for an invalid url', async () => {
    vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ detail: 'Invalid Trakt list url', status: 422 }, 422));

    await expect(addTraktList('not-a-url')).rejects.toMatchObject({ status: 422 });
  });

  it('matchTraktList POSTs to the match sub-route', async () => {
    const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(accepted());

    await expect(matchTraktList(5)).resolves.toBeUndefined();
    expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists/5/match', expect.objectContaining({ method: 'POST' }));
  });

  it('deleteTraktList issues a DELETE and resolves on 202', async () => {
    const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(accepted());

    await expect(deleteTraktList(9)).resolves.toBeUndefined();
    expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists/9', expect.objectContaining({ method: 'DELETE' }));
  });

  it('updateTraktList PUTs autoRefresh/generatePlaylist', async () => {
    const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(
      jsonResponse({
        autoRefresh: true,
        generatePlaylist: true,
        id: 3,
        itemCount: 10,
        matchCount: 8,
        name: 'My List',
        slug: 'my-list',
        traktId: 100
      })
    );

    await updateTraktList(3, { autoRefresh: true, generatePlaylist: true });

    const [url, init] = fetchMock.mock.calls[0];
    expect(url).toBe('/api/v1/trakt/lists/3');
    expect(init).toMatchObject({ method: 'PUT' });
    expect(JSON.parse(String(init?.body))).toEqual({ autoRefresh: true, generatePlaylist: true });
  });

  it('getTraktStatus fetches the busy flag', async () => {
    const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ busy: true }));

    await expect(getTraktStatus()).resolves.toEqual({ busy: true });
    expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/status', expect.objectContaining({ method: 'GET' }));
  });
});
