/**
 * Tests for the marketplaceApi client (web-ui/src/services/marketplaceApi.js).
 *
 * Lives under src/__tests__ (the unit project) instead of web-ui/ because the
 * service uses ESM + import.meta.env, which the esbuild-jest transform in the
 * react project can't process under --experimental-vm-modules. Running it
 * under the unit project (node env, native ESM) avoids that issue entirely.
 *
 * We provide a minimal localStorage stub since node env doesn't have one.
 */

import { jest, describe, test, expect, beforeEach, afterEach, beforeAll } from '@jest/globals';

// ── localStorage stub ─────────────────────────────────────────────
class LocalStorageStub {
  constructor() { this._store = new Map(); }
  getItem(key) { return this._store.has(key) ? this._store.get(key) : null; }
  setItem(key, value) { this._store.set(key, String(value)); }
  removeItem(key) { this._store.delete(key); }
  clear() { this._store.clear(); }
}
global.localStorage = new LocalStorageStub();

let marketplaceApi;

beforeAll(async () => {
  ({ marketplaceApi } = await import('../../web-ui/src/services/marketplaceApi.js'));
});

const BASE = 'https://autopilot-marketplace.azurewebsites.net';

function mockFetchResponse(body, ok = true, status = 200) {
  return Promise.resolve({
    ok,
    status,
    json: () => Promise.resolve(body)
  });
}

describe('marketplaceApi', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
    global.localStorage.clear();
  });

  afterEach(() => {
    delete global.fetch;
  });

  describe('_getHeaders / auth', () => {
    test('omits Authorization header when no token in localStorage', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.searchItems();
      const headers = global.fetch.mock.calls[0][1].headers;
      expect(headers['Authorization']).toBeUndefined();
      expect(headers['Content-Type']).toBe('application/json');
    });

    test('includes Bearer token when present in localStorage', async () => {
      global.localStorage.setItem('loxia-auth-token', 'jwt-abc-123');
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.searchItems();
      const headers = global.fetch.mock.calls[0][1].headers;
      expect(headers['Authorization']).toBe('Bearer jwt-abc-123');
    });
  });

  describe('searchItems', () => {
    test('builds query string with all params', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.searchItems({ query: 'test', type: 'skill', sort: 'rating', page: 2, limit: 10 });

      const url = global.fetch.mock.calls[0][0];
      expect(url).toContain('q=test');
      expect(url).toContain('type=skill');
      expect(url).toContain('sort=rating');
      expect(url).toContain('page=2');
      expect(url).toContain('limit=10');
      expect(url).toMatch(new RegExp(`^${BASE}/api/items\\?`));
    });

    test('uses sensible defaults when called with no args', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.searchItems();
      const url = global.fetch.mock.calls[0][0];
      expect(url).toContain('sort=downloads');
      expect(url).toContain('page=1');
      expect(url).toContain('limit=20');
      expect(url).not.toMatch(/[?&]q=/);
      expect(url).not.toMatch(/[?&]type=/);
    });

    test('omits empty query and type from URL', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.searchItems({ query: '', type: '' });
      const url = global.fetch.mock.calls[0][0];
      expect(url).not.toMatch(/[?&]q=/);
      expect(url).not.toMatch(/[?&]type=/);
    });
  });

  describe('getItem', () => {
    test('fetches /api/items/:id', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, item: { id: 'abc' } }));
      const result = await marketplaceApi.getItem('abc-123');
      expect(global.fetch.mock.calls[0][0]).toBe(`${BASE}/api/items/abc-123`);
      expect(result.item.id).toBe('abc');
    });
  });

  describe('downloadItem', () => {
    test('fetches /api/items/:id/download with redirect follow', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ name: 'My Skill', sections: [] }));
      const data = await marketplaceApi.downloadItem('id-1');
      expect(global.fetch.mock.calls[0][0]).toBe(`${BASE}/api/items/id-1/download`);
      expect(global.fetch.mock.calls[0][1].redirect).toBe('follow');
      expect(data.name).toBe('My Skill');
    });

    test('throws on non-OK download', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({}, false, 404));
      await expect(marketplaceApi.downloadItem('missing')).rejects.toThrow('Download failed: 404');
    });
  });

  describe('getFeatured', () => {
    test('fetches /api/items/featured', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.getFeatured();
      expect(global.fetch.mock.calls[0][0]).toBe(`${BASE}/api/items/featured`);
    });
  });

  describe('publishItem', () => {
    test('POSTs to /api/items with full body', async () => {
      global.localStorage.setItem('loxia-auth-token', 'jwt');
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, item: { id: 'new' } }));

      await marketplaceApi.publishItem({
        type: 'skill',
        name: 'My Skill',
        description: 'A test skill',
        tags: ['ai', 'utility'],
        content: { sections: [] },
        metadata: { difficulty: 'easy' },
        authorName: 'Test User'
      });

      const call = global.fetch.mock.calls[0];
      expect(call[0]).toBe(`${BASE}/api/items`);
      expect(call[1].method).toBe('POST');
      expect(call[1].headers['Authorization']).toBe('Bearer jwt');

      const body = JSON.parse(call[1].body);
      expect(body.type).toBe('skill');
      expect(body.name).toBe('My Skill');
      expect(body.tags).toEqual(['ai', 'utility']);
      expect(body.metadata.difficulty).toBe('easy');
      expect(body.authorName).toBe('Test User');
    });

    test('throws with server error message on failed publish', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ error: 'name is required' }, false, 400));
      await expect(marketplaceApi.publishItem({ type: 'skill' }))
        .rejects.toThrow('name is required');
    });

    test('falls back to HTTP status when response has no error field', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({}, false, 500));
      await expect(marketplaceApi.publishItem({})).rejects.toThrow('HTTP 500');
    });
  });

  describe('updateItem', () => {
    test('PUTs to /api/items/:id with body', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, item: {} }));
      await marketplaceApi.updateItem('item-1', { name: 'New name' });
      const call = global.fetch.mock.calls[0];
      expect(call[0]).toBe(`${BASE}/api/items/item-1`);
      expect(call[1].method).toBe('PUT');
      expect(JSON.parse(call[1].body).name).toBe('New name');
    });
  });

  describe('deleteItem', () => {
    test('DELETEs /api/items/:id', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true }));
      await marketplaceApi.deleteItem('item-1');
      const call = global.fetch.mock.calls[0];
      expect(call[0]).toBe(`${BASE}/api/items/item-1`);
      expect(call[1].method).toBe('DELETE');
    });
  });

  describe('rateItem', () => {
    test('POSTs to /api/items/:id/rate with rating body', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, rating_avg: 4.5, rating_count: 2 }));
      await marketplaceApi.rateItem('item-1', { rating: 5, review: 'Great!', userName: 'Bob' });

      const call = global.fetch.mock.calls[0];
      expect(call[0]).toBe(`${BASE}/api/items/item-1/rate`);
      expect(call[1].method).toBe('POST');
      const body = JSON.parse(call[1].body);
      expect(body.rating).toBe(5);
      expect(body.review).toBe('Great!');
      expect(body.userName).toBe('Bob');
    });
  });

  describe('getMyItems', () => {
    test('fetches /api/my-items', async () => {
      global.fetch.mockReturnValue(mockFetchResponse({ success: true, items: [] }));
      await marketplaceApi.getMyItems();
      expect(global.fetch.mock.calls[0][0]).toBe(`${BASE}/api/my-items`);
    });
  });

  describe('isAvailable', () => {
    test('returns true when health endpoint reports OK', async () => {
      global.fetch.mockReturnValue(Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ status: 'OK' })
      }));
      const available = await marketplaceApi.isAvailable();
      expect(available).toBe(true);
      expect(global.fetch.mock.calls[0][0]).toBe(`${BASE}/health`);
    });

    test('returns false when health endpoint reports non-OK status', async () => {
      global.fetch.mockReturnValue(Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ status: 'DEGRADED' })
      }));
      expect(await marketplaceApi.isAvailable()).toBe(false);
    });

    test('returns false when fetch throws (network error)', async () => {
      global.fetch.mockImplementation(() => { throw new Error('Network error'); });
      expect(await marketplaceApi.isAvailable()).toBe(false);
    });
  });
});
