import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Pax8Client } from './pax8-client';

const SECRET = 'super-secret-value-should-never-leak';

function makeFetchMock(opts: {
  tokenOk?: boolean;
  tokenStatus?: number;
  tokenBody?: unknown;
  companiesBody?: unknown;
}) {
  const {
    tokenOk = true,
    tokenStatus = 200,
    tokenBody = { access_token: 'tok', token_type: 'Bearer', expires_in: 86400 },
    companiesBody = { content: [{ id: 'c1', name: 'Acme' }], page: { size: 10, totalElements: 1, totalPages: 1, number: 0 } },
  } = opts;

  const calls: Array<{ url: string; init?: RequestInit }> = [];

  const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
    calls.push({ url, init });

    if (url.includes('/token')) {
      return {
        ok: tokenOk,
        status: tokenStatus,
        json: async () => tokenBody,
        text: async () => `token error status ${tokenStatus}`,
      } as unknown as Response;
    }

    return {
      ok: true,
      status: 200,
      json: async () => companiesBody,
      text: async () => '',
    } as unknown as Response;
  });

  return { fetchMock, calls };
}

/**
 * Multi-page fetch mock for the listAll* pagination helpers. Keys page
 * bodies by URL substring (e.g. '/subscriptions') and by the `page=N` query
 * param, returning one page envelope per (resource, page) pair.
 */
function makeMultiPageFetchMock(opts: {
  resource: string; // e.g. 'subscriptions', 'products', 'companies'
  pages: Array<{ content: unknown[]; totalPages: number }>;
  tokenBody?: unknown;
}) {
  const {
    resource,
    pages,
    tokenBody = { access_token: 'tok', token_type: 'Bearer', expires_in: 86400 },
  } = opts;

  const calls: Array<{ url: string; init?: RequestInit }> = [];

  const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
    calls.push({ url, init });

    if (url.includes('/token')) {
      return {
        ok: true,
        status: 200,
        json: async () => tokenBody,
        text: async () => '',
      } as unknown as Response;
    }

    if (url.includes(`/${resource}`)) {
      const match = url.match(/page=(\d+)/);
      const pageNum = match ? parseInt(match[1], 10) : 0;
      const p = pages[pageNum];
      return {
        ok: true,
        status: 200,
        json: async () => ({
          content: p.content,
          page: { size: 200, totalElements: pages.reduce((n, pg) => n + pg.content.length, 0), totalPages: p.totalPages, number: pageNum },
        }),
        text: async () => '',
      } as unknown as Response;
    }

    return {
      ok: true,
      status: 200,
      json: async () => ({ content: [], page: { size: 200, totalElements: 0, totalPages: 1, number: 0 } }),
      text: async () => '',
    } as unknown as Response;
  });

  return { fetchMock, calls };
}

describe('Pax8Client', () => {
  beforeEach(() => {
    vi.unstubAllGlobals();
  });

  it('getToken() POSTs a JSON body with grant_type, client_id, client_secret, audience', async () => {
    const { fetchMock, calls } = makeFetchMock({});
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    await client.listCompanies();

    const tokenCall = calls.find(c => c.url.includes('/token'));
    expect(tokenCall).toBeDefined();
    expect((tokenCall!.init!.headers as Record<string, string>)['Content-Type']).toBe('application/json');

    const parsedBody = JSON.parse(tokenCall!.init!.body as string);
    expect(parsedBody.grant_type).toBe('client_credentials');
    expect(parsedBody.client_id).toBe('id1');
    expect(parsedBody.client_secret).toBe(SECRET);
    expect(parsedBody.audience).toBe('https://api.pax8.com');
  });

  it('reuses the cached token without a second fetch within the cache window', async () => {
    const { fetchMock, calls } = makeFetchMock({});
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    await client.listCompanies();
    await client.listCompanies();

    const tokenCalls = calls.filter(c => c.url.includes('/token'));
    expect(tokenCalls).toHaveLength(1);
    expect(fetchMock).toHaveBeenCalledTimes(3); // 1 token + 2 companies
  });

  it('throws on a not-ok token response, including the status but never the secret', async () => {
    const { fetchMock } = makeFetchMock({ tokenOk: false, tokenStatus: 401 });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });

    await expect(client.listCompanies()).rejects.toThrow(/401/);
    await expect(client.listCompanies()).rejects.not.toThrow(new RegExp(SECRET));
  });

  it('listCompanies() sends Authorization: Bearer <token> and returns the parsed envelope', async () => {
    const companiesBody = {
      content: [{ id: 'c1', name: 'Acme Co' }],
      page: { size: 10, totalElements: 1, totalPages: 1, number: 0 },
    };
    const { fetchMock, calls } = makeFetchMock({ companiesBody });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listCompanies();

    const companiesCall = calls.find(c => c.url.includes('/companies'));
    expect(companiesCall).toBeDefined();
    const authHeader = (companiesCall!.init!.headers as Record<string, string>)['Authorization'];
    expect(authHeader).toMatch(/^Bearer /);

    expect(result).toEqual(companiesBody);
  });

  it('listAllSubscriptions() concatenates content across all pages in order', async () => {
    const pages = [
      { content: [{ id: 's1' }, { id: 's2' }], totalPages: 2 },
      { content: [{ id: 's3' }], totalPages: 2 },
    ];
    const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'subscriptions', pages });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listAllSubscriptions();

    expect(result).toEqual([{ id: 's1' }, { id: 's2' }, { id: 's3' }]);

    const subCalls = calls.filter(c => c.url.includes('/subscriptions'));
    expect(subCalls).toHaveLength(2);
    for (const c of subCalls) {
      expect(c.url).toContain('size=200');
    }
  });

  it('listAllSubscriptions() stops after a single page when totalPages is 1 (no infinite loop)', async () => {
    const pages = [{ content: [{ id: 's1' }], totalPages: 1 }];
    const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'subscriptions', pages });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listAllSubscriptions();

    expect(result).toEqual([{ id: 's1' }]);
    expect(calls.filter(c => c.url.includes('/subscriptions'))).toHaveLength(1);
  });

  it('listAllProducts() concatenates content across all pages in order', async () => {
    const pages = [
      { content: [{ id: 'p1' }], totalPages: 2 },
      { content: [{ id: 'p2' }], totalPages: 2 },
    ];
    const { fetchMock } = makeMultiPageFetchMock({ resource: 'products', pages });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listAllProducts();

    expect(result).toEqual([{ id: 'p1' }, { id: 'p2' }]);
  });

  it('listAllCompanies() concatenates content across all pages in order', async () => {
    const pages = [
      { content: [{ id: 'c1' }], totalPages: 2 },
      { content: [{ id: 'c2' }], totalPages: 2 },
    ];
    const { fetchMock } = makeMultiPageFetchMock({ resource: 'companies', pages });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listAllCompanies();

    expect(result).toEqual([{ id: 'c1' }, { id: 'c2' }]);
  });

  it('every request the listAll* helpers issue is a GET with an Authorization: Bearer header; none use a mutating method', async () => {
    const pages = [{ content: [{ id: 's1' }], totalPages: 1 }];
    const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'subscriptions', pages });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    await client.listAllSubscriptions();

    const dataCalls = calls.filter(c => !c.url.includes('/token'));
    expect(dataCalls.length).toBeGreaterThan(0);
    for (const c of dataCalls) {
      expect(c.init?.method === undefined || c.init?.method === 'GET').toBe(true);
      const authHeader = (c.init!.headers as Record<string, string>)['Authorization'];
      expect(authHeader).toMatch(/^Bearer /);
    }
  });

  it('listAllInvoices() concatenates content across all pages in order', async () => {
    const pages = [
      { content: [{ id: 'inv-1' }, { id: 'inv-2' }], totalPages: 2 },
      { content: [{ id: 'inv-3' }], totalPages: 2 },
    ];
    const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'invoices', pages });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listAllInvoices();

    expect(result).toEqual([{ id: 'inv-1' }, { id: 'inv-2' }, { id: 'inv-3' }]);

    const invoiceCalls = calls.filter(c => c.url.includes('/invoices') && !c.url.includes('/items'));
    expect(invoiceCalls).toHaveLength(2);
    for (const c of invoiceCalls) {
      expect(c.url).toContain('size=200');
    }
  });

  it('listAllInvoiceItems(invoiceId) requests the nested per-invoice path and concatenates its pages', async () => {
    const invoiceId = 'inv-123';
    const pages = [
      { content: [{ id: 'item-1' }, { id: 'item-2' }], totalPages: 2 },
      { content: [{ id: 'item-3' }], totalPages: 2 },
    ];
    const calls: Array<{ url: string; init?: RequestInit }> = [];
    const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
      calls.push({ url, init });

      if (url.includes('/token')) {
        return {
          ok: true,
          status: 200,
          json: async () => ({ access_token: 'tok', token_type: 'Bearer', expires_in: 86400 }),
          text: async () => '',
        } as unknown as Response;
      }

      if (url.includes(`/invoices/${invoiceId}/items`)) {
        const match = url.match(/page=(\d+)/);
        const pageNum = match ? parseInt(match[1], 10) : 0;
        const p = pages[pageNum];
        return {
          ok: true,
          status: 200,
          json: async () => ({
            content: p.content,
            page: { size: 200, totalElements: pages.reduce((n, pg) => n + pg.content.length, 0), totalPages: p.totalPages, number: pageNum },
          }),
          text: async () => '',
        } as unknown as Response;
      }

      return {
        ok: true,
        status: 200,
        json: async () => ({ content: [], page: { size: 200, totalElements: 0, totalPages: 1, number: 0 } }),
        text: async () => '',
      } as unknown as Response;
    });
    vi.stubGlobal('fetch', fetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    const result = await client.listAllInvoiceItems(invoiceId);

    expect(result).toEqual([{ id: 'item-1' }, { id: 'item-2' }, { id: 'item-3' }]);

    const itemCalls = calls.filter(c => c.url.includes(`/invoices/${invoiceId}/items`));
    expect(itemCalls).toHaveLength(2);
    expect(itemCalls.some(c => c.url.includes('/invoices/inv-123/items'))).toBe(true);
    for (const c of itemCalls) {
      expect(c.url).toContain('size=200');
    }
  });

  it('listAllInvoices() and listAllInvoiceItems() only issue GET requests with an Authorization: Bearer header', async () => {
    const invoicePages = [{ content: [{ id: 'inv-1' }], totalPages: 1 }];
    const { fetchMock: invoiceFetchMock, calls: invoiceCalls } = makeMultiPageFetchMock({
      resource: 'invoices',
      pages: invoicePages,
    });
    vi.stubGlobal('fetch', invoiceFetchMock);

    const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET });
    await client.listAllInvoices();
    await client.listAllInvoiceItems('inv-123');

    const dataCalls = invoiceCalls.filter(c => !c.url.includes('/token'));
    expect(dataCalls.length).toBeGreaterThan(0);
    for (const c of dataCalls) {
      expect(c.init?.method === undefined || c.init?.method === 'GET').toBe(true);
      const authHeader = (c.init!.headers as Record<string, string>)['Authorization'];
      expect(authHeader).toMatch(/^Bearer /);
    }
  });
});
