/**
 * Tests for src/lib/tracker-client — request path/header construction.
 *
 * The shared @bytelyst/api-client is mocked so we can assert exactly which
 * path + options the tracker client forwards, without any real network.
 */

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

const { fetchSpy } = vi.hoisted(() => ({ fetchSpy: vi.fn() }));

vi.mock('@bytelyst/api-client', () => ({
  createApiClient: () => ({ fetch: fetchSpy }),
}));

import {
  listItems,
  getItem,
  createItem,
  updateItem,
  updateItemStatus,
  deleteItem,
  getStats,
  listComments,
  addComment,
  toggleVote,
  getRoadmapItems,
  getRoadmapStats,
  getPublicItem,
  submitPublicItem,
  publicVote,
} from '@/lib/tracker-client';

describe('tracker-client (authenticated API)', () => {
  beforeEach(() => {
    fetchSpy.mockReset();
    fetchSpy.mockResolvedValue({});
  });

  it('builds a query string for listItems', async () => {
    await listItems({ status: 'open', limit: '10' });
    const [path] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items?status=open&limit=10');
  });

  it('omits the query string when no params are passed', async () => {
    await listItems();
    expect(fetchSpy.mock.calls[0][0]).toBe('/items');
  });

  it('requests a single item by id', async () => {
    await getItem('item_42');
    expect(fetchSpy.mock.calls[0][0]).toBe('/items/item_42');
  });

  it('POSTs a serialized body when creating an item', async () => {
    await createItem({ title: 'Bug', type: 'bug' });
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items');
    expect(options.method).toBe('POST');
    expect(JSON.parse(options.body)).toEqual({ title: 'Bug', type: 'bug' });
  });

  it('PATCHes the status sub-resource', async () => {
    await updateItemStatus('item_1', 'done');
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items/item_1/status');
    expect(options.method).toBe('PATCH');
    expect(JSON.parse(options.body)).toEqual({ status: 'done' });
  });

  it('DELETEs an item by id', async () => {
    await deleteItem('item_9');
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items/item_9');
    expect(options.method).toBe('DELETE');
  });

  it('PUTs an updated item', async () => {
    await updateItem('item_1', { title: 'Renamed' });
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items/item_1');
    expect(options.method).toBe('PUT');
    expect(JSON.parse(options.body)).toEqual({ title: 'Renamed' });
  });

  it('requests stats with an optional productId query', async () => {
    await getStats('chronomind');
    expect(fetchSpy.mock.calls[0][0]).toBe('/items/stats?productId=chronomind');
  });

  it('requests stats without a query when no productId is given', async () => {
    await getStats();
    expect(fetchSpy.mock.calls[0][0]).toBe('/items/stats');
  });

  it('lists comments for an item', async () => {
    await listComments('item_1');
    expect(fetchSpy.mock.calls[0][0]).toBe('/items/item_1/comments');
  });

  it('POSTs a new comment body', async () => {
    await addComment('item_1', 'Looks good');
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items/item_1/comments');
    expect(options.method).toBe('POST');
    expect(JSON.parse(options.body)).toEqual({ body: 'Looks good' });
  });

  it('POSTs a vote toggle', async () => {
    await toggleVote('item_1');
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/items/item_1/vote');
    expect(options.method).toBe('POST');
  });
});

describe('tracker-client (public roadmap API)', () => {
  beforeEach(() => {
    fetchSpy.mockReset();
    fetchSpy.mockResolvedValue({});
  });

  it('builds the public roadmap path with query params', async () => {
    await getRoadmapItems({ sortBy: 'voteCount', limit: '100' });
    expect(fetchSpy.mock.calls[0][0]).toBe('/public/roadmap?sortBy=voteCount&limit=100');
  });

  it('POSTs to the public submit endpoint', async () => {
    await submitPublicItem({ title: 'Idea', email: 'a@b.com', name: 'A' });
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/public/submit');
    expect(options.method).toBe('POST');
    expect(JSON.parse(options.body).title).toBe('Idea');
  });

  it('requests public roadmap stats with optional productId', async () => {
    await getRoadmapStats('lysnrai');
    expect(fetchSpy.mock.calls[0][0]).toBe('/public/roadmap/stats?productId=lysnrai');
  });

  it('requests a single public item by id', async () => {
    await getPublicItem('item_7');
    expect(fetchSpy.mock.calls[0][0]).toBe('/public/items/item_7');
  });

  it('POSTs a public vote with the voter email', async () => {
    await publicVote('item_7', 'voter@example.com');
    const [path, options] = fetchSpy.mock.calls[0];
    expect(path).toBe('/public/items/item_7/vote');
    expect(options.method).toBe('POST');
    expect(JSON.parse(options.body)).toEqual({ email: 'voter@example.com' });
  });
});

describe('tracker-client product header injection', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  beforeEach(() => {
    fetchSpy.mockReset();
    fetchSpy.mockResolvedValue({});
  });

  it('adds x-product-id from localStorage when running in the browser', async () => {
    vi.stubGlobal('window', {});
    vi.stubGlobal('localStorage', {
      getItem: (key: string) => (key === 'tracker_selected_product' ? 'chronomind' : null),
    });

    await listItems();

    const options = fetchSpy.mock.calls[0][1];
    expect(options.headers['x-product-id']).toBe('chronomind');
  });

  it('does not add x-product-id when no product is selected', async () => {
    vi.stubGlobal('window', {});
    vi.stubGlobal('localStorage', { getItem: () => null });

    await listItems();

    const options = fetchSpy.mock.calls[0][1];
    expect(options.headers['x-product-id']).toBeUndefined();
  });
});
