import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getPresets, traceImage, simplifyVector, exportAsDxf } from '../engine';

// ---------- helpers ----------

function mockFetchOk(body: unknown) {
  return vi.fn().mockResolvedValue({
    ok: true,
    status: 200,
    json: () => Promise.resolve(body),
    text: () => Promise.resolve(JSON.stringify(body)),
  });
}

function mockFetchFail(status: number, statusText: string) {
  return vi.fn().mockResolvedValue({
    ok: false,
    status,
    statusText,
    text: () => Promise.resolve(statusText),
  });
}

// ---------- setup ----------

const originalFetch = globalThis.fetch;

beforeEach(() => {
  // reset to a no-op fetch so tests that forget to mock still fail clearly
  globalThis.fetch = vi.fn().mockRejectedValue(new Error('fetch not mocked'));
});

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

// ---------- getPresets ----------

describe('getPresets', () => {
  it('calls GET /engine/presets and returns parsed JSON', async () => {
    const payload = { presets: { sign: { name: 'sign' } } };
    globalThis.fetch = mockFetchOk(payload);

    const result = await getPresets();

    expect(globalThis.fetch).toHaveBeenCalledOnce();
    const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(url).toBe('/engine/presets');
    expect(opts?.signal).toBeUndefined();
    expect(result).toEqual(payload);
  });

  it('passes AbortSignal when provided', async () => {
    globalThis.fetch = mockFetchOk({ presets: {} });
    const controller = new AbortController();

    await getPresets(controller.signal);

    const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(opts.signal).toBe(controller.signal);
  });

  it('throws on non-ok response', async () => {
    globalThis.fetch = mockFetchFail(500, 'Internal Server Error');

    await expect(getPresets()).rejects.toThrow(/failed.*500/i);
  });
});

// ---------- traceImage ----------

describe('traceImage', () => {
  it('sends FormData with file, preset, output_format, and JSON params', async () => {
    globalThis.fetch = mockFetchOk({ output: '<svg/>', format: 'svg', metadata: {} });

    const file = new File(['pixels'], 'test.png', { type: 'image/png' });
    const params = { epsilon: 2.5, turdsize: 10 };

    await traceImage(file, 'sign', params);

    expect(globalThis.fetch).toHaveBeenCalledOnce();
    const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(url).toBe('/engine/trace');
    expect(opts.method).toBe('POST');

    const body: FormData = opts.body;
    expect(body).toBeInstanceOf(FormData);
    expect(body.get('file')).toBeInstanceOf(File);
    expect(body.get('preset')).toBe('sign');
    expect(body.get('output_format')).toBe('svg');
    expect(body.get('params')).toBe(JSON.stringify(params));
  });

  it('passes AbortSignal when provided', async () => {
    globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} });
    const controller = new AbortController();

    const file = new File(['px'], 'img.png', { type: 'image/png' });
    await traceImage(file, 'sign', {}, controller.signal);

    const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(opts.signal).toBe(controller.signal);
  });

  it('throws with detail on non-ok response', async () => {
    globalThis.fetch = mockFetchFail(422, 'Unprocessable Entity');

    const file = new File(['px'], 'bad.png', { type: 'image/png' });
    await expect(traceImage(file, 'sign', {})).rejects.toThrow(/failed.*422/i);
  });
});

// ---------- simplifyVector ----------

describe('simplifyVector', () => {
  it('sends FormData with file, epsilon, and output_format', async () => {
    globalThis.fetch = mockFetchOk({ output: '<svg/>', format: 'svg', metadata: {} });

    const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
    await simplifyVector(file, 3.0);

    const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(url).toBe('/engine/simplify');
    expect(opts.method).toBe('POST');

    const body: FormData = opts.body;
    expect(body.get('file')).toBeInstanceOf(File);
    expect(body.get('epsilon')).toBe('3');
    expect(body.get('output_format')).toBe('svg');
  });

  it('passes AbortSignal when provided', async () => {
    globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} });
    const controller = new AbortController();

    const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
    await simplifyVector(file, 1.0, controller.signal);

    const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(opts.signal).toBe(controller.signal);
  });

  it('throws on non-ok response', async () => {
    globalThis.fetch = mockFetchFail(400, 'Bad Request');

    const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
    await expect(simplifyVector(file, 1.0)).rejects.toThrow(/failed.*400/i);
  });
});

// ---------- exportAsDxf ----------

function mockFetchBlob(blobContent: string) {
  return vi.fn().mockResolvedValue({
    ok: true,
    status: 200,
    blob: () => Promise.resolve(new Blob([blobContent], { type: 'application/octet-stream' })),
    text: () => Promise.resolve(blobContent),
  });
}

describe('exportAsDxf', () => {
  it('sends FormData with SVG content, output_format=dxf, units, and scale_factor', async () => {
    globalThis.fetch = mockFetchBlob('DXF_CONTENT_BINARY');

    const svgContent = '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>';
    const result = await exportAsDxf(svgContent, 'inches', 0.010416667);

    expect(globalThis.fetch).toHaveBeenCalledOnce();
    const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(url).toBe('/engine/simplify');
    expect(opts.method).toBe('POST');

    const body: FormData = opts.body;
    expect(body).toBeInstanceOf(FormData);
    expect(body.get('output_format')).toBe('dxf');
    expect(body.get('units')).toBe('inches');
    expect(body.get('scale_factor')).toBe('0.010416667');

    // File should be present as a Blob
    const file = body.get('file');
    expect(file).toBeInstanceOf(Blob);

    // Result should be a Blob
    expect(result).toBeInstanceOf(Blob);
    const text = await result.text();
    expect(text).toBe('DXF_CONTENT_BINARY');
  });

  it('sends mm units correctly', async () => {
    globalThis.fetch = mockFetchBlob('DXF_MM');

    await exportAsDxf('<svg/>', 'mm', 0.264583333);

    const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    const body: FormData = opts.body;
    expect(body.get('units')).toBe('mm');
    expect(body.get('scale_factor')).toBe('0.264583333');
  });

  it('passes AbortSignal when provided', async () => {
    globalThis.fetch = mockFetchBlob('DXF');
    const controller = new AbortController();

    await exportAsDxf('<svg/>', 'inches', 1.0, controller.signal);

    const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(opts.signal).toBe(controller.signal);
  });

  it('throws on non-ok response', async () => {
    globalThis.fetch = mockFetchFail(500, 'Internal Server Error');

    await expect(
      exportAsDxf('<svg/>', 'inches', 1.0),
    ).rejects.toThrow(/DXF export.*failed.*500/i);
  });
});
