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

// Mock node:child_process before importing the SUT. The SUT uses
// promisify(execFile); without the Node-internal special handling, promisify
// would only forward the first callback arg. We sidestep that by returning a
// pre-promisified function tagged with util.promisify.custom that resolves
// to {stdout, stderr}.
import * as util from 'node:util';
const execFileMock = vi.fn();
vi.mock('node:child_process', () => {
	const execFile: any = () => {
		throw new Error('callback form not used in tests');
	};
	execFile[util.promisify.custom] = (cmd: string, args: string[], opts: any) =>
		execFileMock(cmd, args, opts);
	return { execFile };
});

const existsSyncMock = vi.fn();
vi.mock('node:fs', () => ({
	existsSync: (p: string) => existsSyncMock(p)
}));

import { extractTextAndThumbnail } from '../lib/server/instagram-extractor';

describe('instagram-extractor (yt-dlp backend)', () => {
	const originalFetch = globalThis.fetch;

	beforeEach(() => {
		execFileMock.mockReset();
		existsSyncMock.mockReset();
		existsSyncMock.mockReturnValue(false);
	});

	afterEach(() => {
		globalThis.fetch = originalFetch;
	});

	it('parses yt-dlp JSON and returns bodyText + thumbnail data URI', async () => {
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({
				description: 'Pasta carbonara: 200g spaghetti, 100g pancetta, 2 eggs.',
				thumbnail: 'https://example.com/thumb.jpg'
			}),
			stderr: ''
		});

		globalThis.fetch = vi.fn().mockResolvedValue({
			status: 200,
			headers: { get: () => 'image/jpeg' },
			arrayBuffer: () => Promise.resolve(new Uint8Array([1, 2, 3]).buffer)
		}) as unknown as typeof fetch;

		const result = await extractTextAndThumbnail('https://www.instagram.com/reel/abc123/');

		expect(result.bodyText).toContain('carbonara');
		expect(result.thumbnail).toMatch(/^data:image\/jpeg;base64,/);
	});

	it('falls back to first thumbnails entry when top-level thumbnail is absent', async () => {
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({
				description: 'Recipe text',
				thumbnails: [{ url: 'https://example.com/alt-thumb.jpg' }]
			}),
			stderr: ''
		});

		globalThis.fetch = vi.fn().mockResolvedValue({
			status: 200,
			headers: { get: () => 'image/png' },
			arrayBuffer: () => Promise.resolve(new Uint8Array([4, 5, 6]).buffer)
		}) as unknown as typeof fetch;

		const result = await extractTextAndThumbnail('https://www.instagram.com/reel/abc/');
		expect(result.thumbnail).toMatch(/^data:image\/png;base64,/);
	});

	it('returns null thumbnail when fetch fails', async () => {
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({
				description: 'Recipe text',
				thumbnail: 'https://example.com/missing.jpg'
			}),
			stderr: ''
		});
		globalThis.fetch = vi.fn().mockResolvedValue({
			status: 404,
			headers: { get: () => 'text/html' },
			arrayBuffer: () => Promise.resolve(new ArrayBuffer(0))
		}) as unknown as typeof fetch;

		const result = await extractTextAndThumbnail('https://www.instagram.com/reel/abc/');
		expect(result.bodyText).toBe('Recipe text');
		expect(result.thumbnail).toBeNull();
	});

	it('passes --cookies flag when secrets/cookies.txt exists', async () => {
		existsSyncMock.mockImplementation((p: string) => p.endsWith('cookies.txt'));
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({ description: 'x', thumbnail: null }),
			stderr: ''
		});

		await extractTextAndThumbnail('https://www.instagram.com/reel/abc/');

		const [, args] = execFileMock.mock.calls[0];
		expect(args).toContain('--cookies');
		const idx = (args as string[]).indexOf('--cookies');
		expect((args as string[])[idx + 1]).toMatch(/cookies\.txt$/);
	});

	it('omits --cookies flag when no cookie file is present', async () => {
		existsSyncMock.mockReturnValue(false);
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({ description: 'x', thumbnail: null }),
			stderr: ''
		});

		await extractTextAndThumbnail('https://www.instagram.com/reel/abc/');

		const [, args] = execFileMock.mock.calls[0];
		expect(args).not.toContain('--cookies');
	});

	it('throws non-recoverable error on "Login required" stderr', async () => {
		const err: any = new Error('yt-dlp failed');
		err.stderr = 'ERROR: [Instagram] xyz: Login required to access this post.';
		execFileMock.mockRejectedValue(err);

		await expect(
			extractTextAndThumbnail('https://www.instagram.com/reel/private/')
		).rejects.toThrow(/authentication/);
	});

	it('throws clear error when yt-dlp binary is missing (ENOENT)', async () => {
		const err: any = new Error('not found');
		err.code = 'ENOENT';
		execFileMock.mockRejectedValue(err);

		await expect(
			extractTextAndThumbnail('https://www.instagram.com/reel/abc/')
		).rejects.toThrow(/yt-dlp is not installed/);
	});

	it('throws when description is empty', async () => {
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({ description: '', thumbnail: null }),
			stderr: ''
		});

		await expect(
			extractTextAndThumbnail('https://www.instagram.com/reel/empty/')
		).rejects.toThrow(/no description/);
	});

	it('emits progress events through the callback', async () => {
		execFileMock.mockResolvedValue({
			stdout: JSON.stringify({ description: 'x', thumbnail: null }),
			stderr: ''
		});

		const events: any[] = [];
		await extractTextAndThumbnail('https://www.instagram.com/reel/abc/', (e) =>
			events.push(e)
		);

		expect(events.some((e) => e.type === 'status' && e.message.includes('yt-dlp'))).toBe(true);
		expect(events.some((e) => e.type === 'complete')).toBe(true);
	});
});
