/**
 * Unit tests for github-tags helper.
 *
 * External HTTP calls are mocked via vitest's global fetch stub so the tests
 * run fully offline.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { listGitHubTags, findGitHubTag, GitHubApiError } from './github-tags';

const MOCK_TAGS = [
	{ name: 'v18.3.0', commit: { sha: 'abc123' } },
	{ name: 'v17.0.2', commit: { sha: 'def456' } }
];

function mockFetch(status: number, body: unknown) {
	return vi.fn().mockResolvedValue({
		ok: status >= 200 && status < 300,
		status,
		json: () => Promise.resolve(body)
	});
}

beforeEach(() => {
	vi.stubGlobal('fetch', mockFetch(200, MOCK_TAGS));
});

afterEach(() => {
	vi.restoreAllMocks();
});

// ---------------------------------------------------------------------------
// listGitHubTags()
// ---------------------------------------------------------------------------

describe('listGitHubTags()', () => {
	it('returns parsed tag list on success', async () => {
		const tags = await listGitHubTags('facebook', 'react');
		expect(tags).toHaveLength(2);
		expect(tags[0].name).toBe('v18.3.0');
		expect(tags[0].commit.sha).toBe('abc123');
	});

	it('calls the correct GitHub API URL', async () => {
		await listGitHubTags('facebook', 'react');
		const [url] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
		expect(url).toBe('https://api.github.com/repos/facebook/react/tags?per_page=100');
	});

	it('includes Accept and User-Agent headers', async () => {
		await listGitHubTags('facebook', 'react');
		const [, init] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
		const headers = init.headers as Record<string, string>;
		expect(headers['Accept']).toBe('application/vnd.github.v3+json');
		expect(headers['User-Agent']).toBe('TrueRef/1.0');
	});

	it('includes Authorization header when token is provided', async () => {
		await listGitHubTags('facebook', 'react', 'mytoken');
		const [, init] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
		const headers = init.headers as Record<string, string>;
		expect(headers['Authorization']).toBe('Bearer mytoken');
	});

	it('does not include Authorization header when token is absent', async () => {
		await listGitHubTags('facebook', 'react');
		const [, init] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
		const headers = init.headers as Record<string, string>;
		expect(headers['Authorization']).toBeUndefined();
	});

	it('throws GitHubApiError on non-2xx response', async () => {
		vi.stubGlobal('fetch', mockFetch(404, { message: 'Not Found' }));
		await expect(listGitHubTags('facebook', 'react')).rejects.toThrow(GitHubApiError);
	});

	it('GitHubApiError carries the HTTP status code', async () => {
		vi.stubGlobal('fetch', mockFetch(403, {}));
		try {
			await listGitHubTags('facebook', 'react');
		} catch (err) {
			expect(err).toBeInstanceOf(GitHubApiError);
			expect((err as GitHubApiError).status).toBe(403);
		}
	});
});

// ---------------------------------------------------------------------------
// findGitHubTag()
// ---------------------------------------------------------------------------

describe('findGitHubTag()', () => {
	it('returns the tag record when it exists', async () => {
		const tag = await findGitHubTag('facebook', 'react', 'v18.3.0');
		expect(tag).not.toBeNull();
		expect(tag?.name).toBe('v18.3.0');
	});

	it('returns null when the tag does not exist', async () => {
		const tag = await findGitHubTag('facebook', 'react', 'v99.0.0');
		expect(tag).toBeNull();
	});

	it('passes the token to listGitHubTags', async () => {
		await findGitHubTag('facebook', 'react', 'v18.3.0', 'mytoken');
		const [, init] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
		const headers = init.headers as Record<string, string>;
		expect(headers['Authorization']).toBe('Bearer mytoken');
	});
});
