import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GitHubHost } from '../github';
import { GiteaHost } from '../gitea';
import { BLOB_THRESHOLD_BYTES } from '../git-host';

const REPO = 'alee/test-vault';
const TOKEN = 'ghp_TEST';

function setupFetch(): ReturnType<typeof vi.fn> {
  const fetchMock = vi.fn();
  vi.stubGlobal('fetch', fetchMock);
  return fetchMock;
}

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

  it('uses Contents API when content is at threshold', async () => {
    const fetchMock = setupFetch();
    // First fetch: GET existing file → 404 (create path)
    fetchMock.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' } as Response);
    // Second fetch: PUT contents → 201 created
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({}) } as Response);

    const host = new GitHubHost(REPO, TOKEN);
    const small = new Uint8Array(BLOB_THRESHOLD_BYTES);  // exactly threshold
    await host.putBlob('attachments/abc.bin', small, 'add abc');

    expect(fetchMock).toHaveBeenCalledTimes(2);
    const lastCall = fetchMock.mock.calls[1];
    expect(lastCall[0]).toContain('/contents/');  // Contents API
    expect(lastCall[1]?.method).toBe('PUT');
  });

  it('uses Git Data API fallback when content exceeds threshold', async () => {
    const fetchMock = setupFetch();
    // 1. POST /git/blobs → blob SHA
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'blobsha111' }) } as Response);
    // 2. GET /git/refs/heads/main → ref { object: { sha: 'commitsha' } }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ object: { sha: 'commitsha111' } }) } as Response);
    // 3. GET /git/commits/commitsha111 → commit { tree: { sha: 'treesha' } }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ tree: { sha: 'treesha111' } }) } as Response);
    // 4. POST /git/trees → new tree { sha: 'newtreesha' }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'newtreesha111' }) } as Response);
    // 5. POST /git/commits → new commit { sha: 'newcommitsha' }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'newcommitsha111' }) } as Response);
    // 6. PATCH /git/refs/heads/main → updated
    fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({}) } as Response);

    const host = new GitHubHost(REPO, TOKEN);
    const big = new Uint8Array(BLOB_THRESHOLD_BYTES + 1);  // one byte over
    await host.putBlob('attachments/big.bin', big, 'add big');

    expect(fetchMock).toHaveBeenCalledTimes(6);
    expect(fetchMock.mock.calls[0][0]).toContain('/git/blobs');
    expect(fetchMock.mock.calls[1][0]).toContain('/git/refs/heads/');
    expect(fetchMock.mock.calls[2][0]).toContain('/git/commits/');
    expect(fetchMock.mock.calls[3][0]).toContain('/git/trees');
    expect(fetchMock.mock.calls[4][0]).toContain('/git/commits');
    expect(fetchMock.mock.calls[5][1]?.method).toBe('PATCH');
  });

  it('throws when blob creation fails in fallback path', async () => {
    const fetchMock = setupFetch();
    fetchMock.mockResolvedValueOnce({ ok: false, status: 422, statusText: 'Unprocessable', text: async () => 'too big' } as Response);

    const host = new GitHubHost(REPO, TOKEN);
    const big = new Uint8Array(BLOB_THRESHOLD_BYTES + 1);
    await expect(host.putBlob('attachments/big.bin', big, 'add big')).rejects.toThrow(/422|too big/);
  });
});

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

  it('uses Contents API when content is at threshold', async () => {
    const fetchMock = setupFetch();
    // First fetch: GET existing file → 404 (create path)
    fetchMock.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' } as Response);
    // Second fetch: POST contents → 201 created
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({}) } as Response);

    const host = new GiteaHost('https://git.example.com', REPO, TOKEN);
    const small = new Uint8Array(BLOB_THRESHOLD_BYTES);  // exactly threshold
    await host.putBlob('attachments/abc.bin', small, 'add abc');

    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(fetchMock.mock.calls[1][0]).toContain('/contents/');
    expect(fetchMock.mock.calls[1][1]?.method).toBe('POST');
  });

  it('uses Git Data API fallback when content exceeds threshold', async () => {
    const fetchMock = setupFetch();
    // 1. POST /api/v1/.../git/blobs → blob SHA
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'blobsha111' }) } as Response);
    // 2. GET /api/v1/.../git/refs/heads/main → ref { object: { sha: 'commitsha' } }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ object: { sha: 'commitsha111' } }) } as Response);
    // 3. GET /api/v1/.../git/commits/commitsha111 → commit { tree: { sha: 'treesha' } }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ tree: { sha: 'treesha111' } }) } as Response);
    // 4. POST /api/v1/.../git/trees → new tree { sha: 'newtreesha' }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'newtreesha111' }) } as Response);
    // 5. POST /api/v1/.../git/commits → new commit { sha: 'newcommitsha' }
    fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'newcommitsha111' }) } as Response);
    // 6. PATCH /api/v1/.../git/refs/heads/main → updated
    fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({}) } as Response);

    const host = new GiteaHost('https://git.example.com', REPO, TOKEN);
    const big = new Uint8Array(BLOB_THRESHOLD_BYTES + 1);  // one byte over
    await host.putBlob('attachments/big.bin', big, 'add big');

    expect(fetchMock).toHaveBeenCalledTimes(6);
    // Gitea uses /api/v1/ prefix
    expect(fetchMock.mock.calls[0][0]).toContain('/api/v1/repos/');
    expect(fetchMock.mock.calls[0][0]).toContain('/git/blobs');
    expect(fetchMock.mock.calls[1][0]).toContain('/git/refs/heads/');
    expect(fetchMock.mock.calls[2][0]).toContain('/git/commits/');
    expect(fetchMock.mock.calls[3][0]).toContain('/git/trees');
    expect(fetchMock.mock.calls[4][0]).toContain('/git/commits');
    expect(fetchMock.mock.calls[5][1]?.method).toBe('PATCH');
  });
});
