import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { runTranslationJob } from '../app/dashboard/translate/translationRunner';

/**
 * Dedicated tests for the multi-file queue runner — the piece that drives
 * N sequential translations. Fetch is fully mocked; no network, no React.
 */

const TEST_CONFIG = {
  sourceLang: 'fr',
  targetLang: 'en',
  mode: 'classic' as const,
  provider: 'google',
  glossaryId: null,
  translateImages: false,
};

function makeFile(name = 'rapport_q3.docx'): File {
  return new File(['dummy content'], name, {
    type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  });
}

function jsonResponse(body: unknown, ok = true, status = 200): Response {
  return {
    ok,
    status,
    json: async () => body,
  } as unknown as Response;
}

describe('runTranslationJob', () => {
  beforeEach(() => {
    vi.stubGlobal('fetch', vi.fn());
    localStorage.clear();
  });

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

  it('submits, polls, and reports completion with progress callbacks', async () => {
    const fetchMock = vi.mocked(fetch);
    fetchMock
      // POST /translate
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_1', file_name: 'rapport_q3.docx' } }))
      // poll 1: processing at 45%
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_1', status: 'processing', progress_percent: 45, current_step: 'Translating' } }))
      // poll 2: completed
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_1', status: 'completed', progress_percent: 100, file_name: 'rapport_q3.docx' } }));

    const onProgress = vi.fn();
    const onJobId = vi.fn();
    const result = await runTranslationJob(makeFile(), TEST_CONFIG, { onProgress, onJobId, pollIntervalMs: 10 });

    expect(result.status).toBe('completed');
    expect(result.jobId).toBe('job_1');
    expect(result.fileName).toBe('rapport_q3.docx');
    expect(onJobId).toHaveBeenCalledWith('job_1');
    expect(onProgress).toHaveBeenCalledTimes(2);
    expect(onProgress).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: 'processing', progress: 45 }));

    // the multipart submit carried the core fields
    const submitCall = fetchMock.mock.calls[0];
    expect(submitCall[0]).toContain('/api/v1/translate');
    const body = submitCall[1]?.body as FormData;
    expect(body.get('source_lang')).toBe('fr');
    expect(body.get('target_lang')).toBe('en');
    expect(body.get('mode')).toBe('classic');
  });

  it('surfaces a failed job with its backend error message', async () => {
    const fetchMock = vi.mocked(fetch);
    fetchMock
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_2' } }))
      .mockResolvedValueOnce(
        jsonResponse({ data: { id: 'job_2', status: 'failed', error_message: 'quota exceeded', progress_percent: 20 } })
      );

    const result = await runTranslationJob(makeFile(), TEST_CONFIG, { pollIntervalMs: 10 });

    expect(result.status).toBe('failed');
    expect(result.error).toBe('quota exceeded');
    expect(result.jobId).toBe('job_2');
  });

  it('reports a submit-time HTTP error without polling', async () => {
    const fetchMock = vi.mocked(fetch);
    fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'Insufficient credits' }, false, 402));

    const onProgress = vi.fn();
    const result = await runTranslationJob(makeFile(), TEST_CONFIG, { onProgress, pollIntervalMs: 10 });

    expect(result.status).toBe('failed');
    expect(result.error).toBe('Insufficient credits');
    expect(fetchMock).toHaveBeenCalledTimes(1); // no polling started
    expect(onProgress).not.toHaveBeenCalled();
  });

  it('aborts between polls, cancels server-side, and never reports completion', async () => {
    const fetchMock = vi.mocked(fetch);
    fetchMock
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_3' } }))
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_3', status: 'processing', progress_percent: 30 } }))
      // cancel call
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_3', status: 'cancelled' } }))
      // a late poll that must never be consumed
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_3', status: 'completed', progress_percent: 100 } }));

    let calls = 0;
    const result = await runTranslationJob(makeFile(), TEST_CONFIG, {
      shouldAbort: () => calls++ > 1, // abort after submit + one poll
      pollIntervalMs: 10,
    });

    expect(result.status).toBe('aborted');
    expect(result.jobId).toBe('job_3');
    // a cancel was sent for the in-flight job
    const cancelCall = fetchMock.mock.calls.find((c) => String(c[0]).includes('/cancel'));
    expect(cancelCall).toBeDefined();
    // and the trailing "completed" poll was never reached
    const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1];
    expect(String(lastCall?.[0])).not.toContain('completed');
  });

  it('stops polling after repeated network failures', async () => {
    const fetchMock = vi.mocked(fetch);
    fetchMock
      .mockResolvedValueOnce(jsonResponse({ data: { id: 'job_4' } }))
      .mockRejectedValue(new TypeError('network down'));

    const result = await runTranslationJob(makeFile(), TEST_CONFIG, { pollIntervalMs: 10 });

    expect(result.status).toBe('failed');
    expect(result.error).toContain('Lost connection');
  });
});
