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

import { _resetRagConfigForTests } from '../../../../lib/helpers/rag_config';
import { uploadFile } from './upload_file_direct';

const RAG_URL = 'http://rag:8000';

beforeAll(() => {
  process.env.RAG_URL = RAG_URL;
  process.env.RAG_AUTH_TOKEN = 'test-token';
  _resetRagConfigForTests();
});
const FILE_ID = 'doc-abc-123';

function defaultArgs() {
  return {
    file: new Blob(['test-content'], { type: 'text/plain' }),
    filename: 'test.txt',
    contentType: 'text/plain',
    fileId: FILE_ID,
    orgSlug: 'default',
  };
}

describe('uploadFile', () => {
  let fetchSpy: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    fetchSpy = vi.fn();
    vi.stubGlobal('fetch', fetchSpy);
  });

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

  function mockFetchOk(body: object = {}) {
    fetchSpy.mockResolvedValue({
      ok: true,
      status: 200,
      json: () =>
        Promise.resolve({
          success: true,
          file_id: 'rag-doc-1',
          chunks_created: 5,
          ...body,
        }),
    });
  }

  function mockFetchError(status: number, statusText: string, body = '') {
    fetchSpy.mockResolvedValue({
      ok: false,
      status,
      statusText,
      text: () => Promise.resolve(body),
    });
  }

  function getCalledFormData(): FormData {
    return fetchSpy.mock.calls[0][1].body;
  }

  it('sends multipart form data with correct filename', async () => {
    mockFetchOk();

    await uploadFile(defaultArgs());

    const formData = getCalledFormData();
    const file = formData.get('file');
    expect(file).toBeInstanceOf(Blob);
    expect((file as File).name).toBe('test.txt');
  });

  it('includes file_id in form data', async () => {
    mockFetchOk();

    await uploadFile(defaultArgs());

    const formData = getCalledFormData();
    expect(formData.get('file_id')).toBe(FILE_ID);
  });

  it('includes metadata with content_type in form data', async () => {
    mockFetchOk();

    await uploadFile(defaultArgs());

    const formData = getCalledFormData();
    const metadata = JSON.parse(formData.get('metadata') as string);
    expect(metadata.content_type).toBe('text/plain');
  });

  it('merges custom metadata with content_type', async () => {
    mockFetchOk();

    await uploadFile({
      ...defaultArgs(),
      metadata: { source: 'upload', category: 'legal' },
    });

    const formData = getCalledFormData();
    const metadata = JSON.parse(formData.get('metadata') as string);
    expect(metadata.content_type).toBe('text/plain');
    expect(metadata.source).toBe('upload');
    expect(metadata.category).toBe('legal');
  });

  it('appends ?sync=true to URL when sync is true', async () => {
    mockFetchOk();

    await uploadFile({ ...defaultArgs(), sync: true });

    const calledUrl: string = fetchSpy.mock.calls[0][0];
    expect(calledUrl).toBe(`${RAG_URL}/api/v1/documents/upload?sync=true`);
  });

  it('does not append ?sync=true when sync is false', async () => {
    mockFetchOk();

    await uploadFile({ ...defaultArgs(), sync: false });

    const calledUrl: string = fetchSpy.mock.calls[0][0];
    expect(calledUrl).toBe(`${RAG_URL}/api/v1/documents/upload`);
  });

  it('does not append ?sync=true when sync is omitted', async () => {
    mockFetchOk();

    await uploadFile(defaultArgs());

    const calledUrl: string = fetchSpy.mock.calls[0][0];
    expect(calledUrl).toBe(`${RAG_URL}/api/v1/documents/upload`);
  });

  it('throws UpstreamHttpError with sanitized body snippet on non-ok response', async () => {
    mockFetchError(500, 'Internal Server Error', 'something broke');

    const err = await uploadFile(defaultArgs()).then(
      () => null,
      (e: unknown) => e,
    );
    expect(err).toBeInstanceOf(Error);
    expect((err as Error).name).toBe('UpstreamHttpError');
    // `.message` carries the safe summary only (the sanitized body
    // lives on `.bodySnippet` so it does not cross the Convex client
    // boundary as a default error toast).
    expect((err as Error).message).toMatch(/HTTP 500/);
    expect((err as { bodySnippet?: string }).bodySnippet).toMatch(
      /something broke/,
    );
    // Retryable for 5xx — caller can decide whether to bounce.
    expect((err as { retryable?: boolean }).retryable).toBe(true);
  });

  it('throws UpstreamHttpError on non-ok response with empty body', async () => {
    mockFetchError(502, 'Bad Gateway');

    const err = await uploadFile(defaultArgs()).then(
      () => null,
      (e: unknown) => e,
    );
    expect(err).toBeInstanceOf(Error);
    expect((err as Error).name).toBe('UpstreamHttpError');
    expect((err as Error).message).toMatch(/HTTP 502/);
    expect((err as { retryable?: boolean }).retryable).toBe(true);
  });

  it('returns correct RagUploadResult shape on success', async () => {
    mockFetchOk({
      success: true,
      file_id: 'rag-doc-42',
      chunks_created: 7,
    });

    const result = await uploadFile(defaultArgs());

    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        fileId: FILE_ID,
        ragDocumentId: 'rag-doc-42',
        chunksCreated: 7,
      }),
    );
    expect(result.processingTimeMs).toBeTypeOf('number');
    expect(result.timestamp).toBeTypeOf('number');
  });

  it('uses id field as fallback when file_id is absent', async () => {
    mockFetchOk({ file_id: undefined, id: 'fallback-id' });

    const result = await uploadFile(defaultArgs());

    expect(result.ragDocumentId).toBe('fallback-id');
  });

  it('defaults chunksCreated to 0 when not in response', async () => {
    mockFetchOk({ chunks_created: undefined });

    const result = await uploadFile(defaultArgs());

    expect(result.chunksCreated).toBe(0);
  });

  it('defaults success to true when not in response', async () => {
    mockFetchOk({ success: undefined });

    const result = await uploadFile(defaultArgs());

    expect(result.success).toBe(true);
  });

  it('sends POST method', async () => {
    mockFetchOk();

    await uploadFile(defaultArgs());

    expect(fetchSpy.mock.calls[0][1].method).toBe('POST');
  });

  it('passes abort signal for timeout', async () => {
    mockFetchOk();

    await uploadFile(defaultArgs());

    expect(fetchSpy.mock.calls[0][1].signal).toBeInstanceOf(AbortSignal);
  });
});
